package tracing

import (
	"context"
	"errors"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"
	"go.opentelemetry.io/otel/attribute"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	"go.opentelemetry.io/otel/sdk/trace/tracetest"
	"go.opentelemetry.io/otel/trace"
)

func TestWrapHTTPClientInjectsTraceparentAndRecordsChildSpan(t *testing.T) {
	recorder := tracetest.NewSpanRecorder()
	tracerProvider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder))
	tracer := tracerProvider.Tracer("test")

	var traceparent string
	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		traceparent = request.Header.Get("traceparent")
		writer.WriteHeader(http.StatusAccepted)
		_, _ = writer.Write([]byte("ok"))
	}))
	defer server.Close()

	client := WrapHTTPClient(server.Client(), tracer, NewPropagator(), "test-upstream")

	ctx, parent := tracer.Start(context.Background(), "parent")
	request, err := http.NewRequestWithContext(ctx, http.MethodGet, server.URL+"/v1/resource?secret=hidden", nil)
	require.NoError(t, err)
	request.Header.Set("Authorization", "Bearer hidden")

	response, err := client.Do(request)
	require.NoError(t, err)
	_, err = io.ReadAll(response.Body)
	require.NoError(t, err)
	require.NoError(t, response.Body.Close())
	parent.End()

	require.NotEmpty(t, traceparent)
	ended := recorder.Ended()
	require.Len(t, ended, 2)
	clientSpan := ended[0]
	require.Equal(t, "HTTP GET", clientSpan.Name())
	require.Equal(t, trace.SpanKindClient, clientSpan.SpanKind())
	require.Equal(t, trace.SpanContextFromContext(ctx).SpanID(), clientSpan.Parent().SpanID())
	require.Equal(t, http.StatusAccepted, spanIntAttribute(clientSpan, "http.response.status_code"))
	require.Equal(t, "test-upstream", httpClientSpanStringAttribute(clientSpan, "agentteam.http.client"))
	require.Empty(t, httpClientSpanStringAttribute(clientSpan, "url.full"))
	require.NotContains(t, spanAttributeValues(clientSpan), "secret=hidden")
	require.NotContains(t, spanAttributeValues(clientSpan), "Bearer hidden")
}

func TestWrapHTTPClientEndsSpanOnBodyEOF(t *testing.T) {
	recorder := tracetest.NewSpanRecorder()
	tracerProvider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder))
	client := WrapHTTPClient(&http.Client{
		Transport: roundTripFunc(func(request *http.Request) (*http.Response, error) {
			return &http.Response{
				StatusCode: http.StatusOK,
				Header:     make(http.Header),
				Body:       io.NopCloser(strings.NewReader("stream")),
			}, nil
		}),
	}, tracerProvider.Tracer("test"), NewPropagator(), "stream")

	response, err := client.Get("https://upstream.example/stream")
	require.NoError(t, err)
	require.Empty(t, recorder.Ended())

	_, err = io.ReadAll(response.Body)
	require.NoError(t, err)
	require.Len(t, recorder.Ended(), 1)
	require.NoError(t, response.Body.Close())
	require.Len(t, recorder.Ended(), 1)
}

func TestWrapHTTPClientEndsSpanOnBodyClose(t *testing.T) {
	recorder := tracetest.NewSpanRecorder()
	tracerProvider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder))
	client := WrapHTTPClient(&http.Client{
		Transport: roundTripFunc(func(request *http.Request) (*http.Response, error) {
			return &http.Response{
				StatusCode: http.StatusOK,
				Header:     make(http.Header),
				Body:       io.NopCloser(strings.NewReader("stream")),
			}, nil
		}),
	}, tracerProvider.Tracer("test"), NewPropagator(), "stream")

	response, err := client.Get("https://upstream.example/stream")
	require.NoError(t, err)
	require.Empty(t, recorder.Ended())

	require.NoError(t, response.Body.Close())
	require.Len(t, recorder.Ended(), 1)
}

func TestWrapHTTPClientRecordsTransportError(t *testing.T) {
	recorder := tracetest.NewSpanRecorder()
	tracerProvider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder))
	transportErr := errors.New("dial failed")
	client := WrapHTTPClient(&http.Client{
		Transport: roundTripFunc(func(request *http.Request) (*http.Response, error) {
			return nil, transportErr
		}),
	}, tracerProvider.Tracer("test"), NewPropagator(), "broken")

	_, err := client.Get("https://upstream.example/fail")
	require.ErrorIs(t, err, transportErr)

	ended := recorder.Ended()
	require.Len(t, ended, 1)
	require.Equal(t, "HTTP GET", ended[0].Name())
	require.Equal(t, "broken", httpClientSpanStringAttribute(ended[0], "agentteam.http.client"))
	require.Equal(t, "Error", ended[0].Status().Code.String())
}

func TestWrapHTTPClientPreservesClientConfigAndAvoidsDoubleWrap(t *testing.T) {
	base := &http.Client{
		Transport:     roundTripFunc(func(request *http.Request) (*http.Response, error) { return nil, errors.New("unused") }),
		CheckRedirect: func(request *http.Request, via []*http.Request) error { return http.ErrUseLastResponse },
		Timeout:       1,
	}
	wrapped := WrapHTTPClient(base, nil, nil, "first")
	wrappedAgain := WrapHTTPClient(wrapped, nil, nil, "second")

	require.NotSame(t, base, wrapped)
	require.Equal(t, base.Timeout, wrapped.Timeout)
	require.NotNil(t, wrapped.CheckRedirect)
	require.NotNil(t, wrapped.Transport)
	require.Same(t, wrapped.Transport, wrappedAgain.Transport)
}

type roundTripFunc func(*http.Request) (*http.Response, error)

func (fn roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) {
	return fn(request)
}

func httpClientSpanStringAttribute(span sdktrace.ReadOnlySpan, key string) string {
	for _, attr := range span.Attributes() {
		if string(attr.Key) == key {
			return attr.Value.AsString()
		}
	}
	return ""
}

func spanIntAttribute(span sdktrace.ReadOnlySpan, key string) int {
	for _, attr := range span.Attributes() {
		if string(attr.Key) == key {
			return int(attr.Value.AsInt64())
		}
	}
	return 0
}

func spanAttributeValues(span sdktrace.ReadOnlySpan) string {
	var values []string
	for _, attr := range span.Attributes() {
		switch attr.Value.Type() {
		case attribute.STRING:
			values = append(values, attr.Value.AsString())
		}
	}
	return strings.Join(values, "\n")
}
