package observability

import (
	"bytes"
	"context"
	"io"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/config"
)

func TestProviderRunServesMetrics(t *testing.T) {
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	provider, err := New(config.ObservabilityConfig{MetricsAddress: "127.0.0.1:0"}, logger, nil)
	require.NoError(t, err)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	errCh := make(chan error, 1)
	go func() { errCh <- provider.Run(ctx) }()

	require.Eventually(t, func() bool {
		return provider.Address() != ""
	}, time.Second, 10*time.Millisecond)

	response, err := http.Get("http://" + provider.Address() + "/metrics")
	require.NoError(t, err)
	defer response.Body.Close()
	body, err := io.ReadAll(response.Body)
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Contains(t, string(body), "go_goroutines")

	cancel()
	require.Eventually(t, func() bool {
		select {
		case err := <-errCh:
			require.ErrorIs(t, err, http.ErrServerClosed)
			return true
		default:
			return false
		}
	}, time.Second, 10*time.Millisecond)
}

func TestProviderRunDisabledMetricsReturnsNil(t *testing.T) {
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	provider, err := New(config.ObservabilityConfig{MetricsAddress: ""}, logger, nil)
	require.NoError(t, err)

	err = provider.Run(context.Background())
	require.NoError(t, err)
}

func TestProviderTracerIsAvailableBeforeTracingExporter(t *testing.T) {
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	provider, err := New(config.ObservabilityConfig{MetricsAddress: ""}, logger, nil)
	require.NoError(t, err)

	require.NotNil(t, provider.Tracer())
	require.NotNil(t, provider.HTTPTracing())
	require.NotNil(t, provider.DaemonTransportMetrics())
	require.NotNil(t, provider.SQLOpener())
	require.NotNil(t, (*Provider)(nil).Tracer())
	require.Nil(t, (*Provider)(nil).HTTPTracing())
	require.Nil(t, (*Provider)(nil).DaemonTransportMetrics())
	require.Nil(t, (*Provider)(nil).SQLOpener())
}

func TestProviderWarnsWhenDBStatementTracingEnabledInProduction(t *testing.T) {
	var logs bytes.Buffer
	logger := slog.New(slog.NewTextHandler(&logs, nil))

	provider, err := NewBase(
		config.ObservabilityConfig{
			MetricsAddress:     "",
			DBStatementEnabled: true,
		},
		logger,
		WithEnvironment("production"),
	)
	require.NoError(t, err)
	require.NoError(t, provider.Shutdown(context.Background()))

	require.Contains(t, logs.String(), "database SQL statement tracing is enabled")
}

func TestProviderShutdownFlushesOTLPTraces(t *testing.T) {
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	traceRequests := make(chan []byte, 4)
	tracePaths := make(chan string, 4)
	traceAuthHeaders := make(chan string, 4)
	collector := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		body, err := io.ReadAll(r.Body)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		tracePaths <- r.URL.Path
		traceAuthHeaders <- r.Header.Get("Authorization")
		traceRequests <- body
		w.WriteHeader(http.StatusOK)
	}))
	defer collector.Close()

	provider, err := NewBase(
		config.ObservabilityConfig{
			MetricsAddress:   "",
			OTLPEndpoint:     collector.URL,
			OTLPHeaders:      map[string]string{"Authorization": "Bearer test-token"},
			TraceSampleRatio: 1,
		},
		logger,
		WithEnvironment("test"),
	)
	require.NoError(t, err)

	_, span := provider.Tracer().Start(context.Background(), "test-span")
	span.End()

	shutdownCtx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	require.NoError(t, provider.Shutdown(shutdownCtx))
	select {
	case path := <-tracePaths:
		require.Equal(t, "/v1/traces", path)
	case <-time.After(time.Second):
		t.Fatal("timed out waiting for OTLP trace export")
	}
	require.Equal(t, "Bearer test-token", <-traceAuthHeaders)
	require.NotEmpty(t, <-traceRequests)
}

func TestProviderShutdownRetriesTraceAfterExpiredContext(t *testing.T) {
	traceShutdownCalls := 0
	provider := &Provider{
		traceShutdown: func(ctx context.Context) error {
			traceShutdownCalls++
			return ctx.Err()
		},
	}

	expiredCtx, cancel := context.WithCancel(context.Background())
	cancel()
	err := provider.Shutdown(expiredCtx)
	require.ErrorIs(t, err, context.Canceled)
	require.Equal(t, 1, traceShutdownCalls)

	err = provider.Shutdown(context.Background())
	require.NoError(t, err)
	require.Equal(t, 2, traceShutdownCalls)

	err = provider.Shutdown(context.Background())
	require.NoError(t, err)
	require.Equal(t, 2, traceShutdownCalls)
}

func TestOTLPTraceEndpointURL(t *testing.T) {
	tests := []struct {
		name     string
		raw      string
		expected string
	}{
		{
			name:     "root base url",
			raw:      "http://collector:4318",
			expected: "http://collector:4318/v1/traces",
		},
		{
			name:     "base url path prefix",
			raw:      "https://collector.example.test/otel/",
			expected: "https://collector.example.test/otel/v1/traces",
		},
		{
			name:     "full traces endpoint",
			raw:      "https://collector.example.test/otel/v1/traces",
			expected: "https://collector.example.test/otel/v1/traces",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := otlpTraceEndpointURL(tt.raw)
			require.NoError(t, err)
			require.Equal(t, tt.expected, got)
		})
	}
}

func TestProviderRunReturnsListenError(t *testing.T) {
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	provider, err := New(config.ObservabilityConfig{MetricsAddress: "127.0.0.1:not-a-port"}, logger, nil)
	require.NoError(t, err)

	err = provider.Run(context.Background())
	require.Error(t, err)
}
