// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016 Datadog, Inc.

package httptrace

import (
	"context"
	"fmt"
	"net/http"
	"net/http/httptest"
	"net/netip"
	"net/url"
	"os"
	"runtime"
	"strconv"
	"testing"

	"github.com/DataDog/dd-trace-go/v2/ddtrace/baggage"
	"github.com/DataDog/dd-trace-go/v2/ddtrace/ext"
	"github.com/DataDog/dd-trace-go/v2/ddtrace/mocktracer"
	"github.com/DataDog/dd-trace-go/v2/ddtrace/tracer"
	"github.com/DataDog/dd-trace-go/v2/internal"
	"github.com/DataDog/dd-trace-go/v2/internal/appsec"
	"github.com/DataDog/dd-trace-go/v2/internal/log"
	"github.com/DataDog/dd-trace-go/v2/internal/normalizer"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestGetErrorCodesFromInput(t *testing.T) {
	codesOnly := "400,401,402"
	rangesOnly := "400-405,408-410"
	mixed := "400,403-405,407-410,412"
	invalid1 := "1,100-200-300-"
	invalid2 := "abc:@3$5^,"
	empty := ""
	t.Run("codesOnly", func(t *testing.T) {
		fn := GetErrorCodesFromInput(codesOnly)
		for i := 400; i <= 402; i++ {
			assert.True(t, fn(i))
		}
		assert.False(t, fn(500))
		assert.False(t, fn(0))
	})
	t.Run("rangesOnly", func(t *testing.T) {
		fn := GetErrorCodesFromInput(rangesOnly)
		for i := 400; i <= 405; i++ {
			assert.True(t, fn(i))
		}
		for i := 408; i <= 410; i++ {
			assert.True(t, fn(i))
		}
		assert.False(t, fn(406))
		assert.False(t, fn(411))
		assert.False(t, fn(500))
	})
	t.Run("mixed", func(t *testing.T) {
		fn := GetErrorCodesFromInput(mixed)
		assert.True(t, fn(400))
		assert.False(t, fn(401))
		for i := 403; i <= 405; i++ {
			assert.True(t, fn(i))
		}
		assert.False(t, fn(406))
		for i := 407; i <= 410; i++ {
			assert.True(t, fn(i))
		}
		assert.False(t, fn(411))
		assert.False(t, fn(500))
	})
	// invalid entries below should result in nils
	t.Run("invalid1", func(t *testing.T) {
		fn := GetErrorCodesFromInput(invalid1)
		assert.Nil(t, fn)
	})
	t.Run("invalid2", func(t *testing.T) {
		fn := GetErrorCodesFromInput(invalid2)
		assert.Nil(t, fn)
	})
	t.Run("empty", func(t *testing.T) {
		fn := GetErrorCodesFromInput(empty)
		assert.Nil(t, fn)
	})
}

func TestConfiguredErrorStatuses(t *testing.T) {
	defer os.Unsetenv("DD_TRACE_HTTP_SERVER_ERROR_STATUSES")
	t.Run("configured", func(t *testing.T) {
		mt := mocktracer.Start()
		defer mt.Stop()

		os.Setenv("DD_TRACE_HTTP_SERVER_ERROR_STATUSES", "199-399,400,501")

		// re-run config defaults based on new DD_TRACE_HTTP_SERVER_ERROR_STATUSES value
		ResetCfg()

		statuses := []int{0, 200, 400, 500}
		r := httptest.NewRequest(http.MethodGet, "/test", nil)
		for i, status := range statuses {
			sp, _, _ := StartRequestSpan(r)
			FinishRequestSpan(sp, status, nil)
			spans := mt.FinishedSpans()
			require.Len(t, spans, i+1)

			switch status {
			case 0:
				assert.Equal(t, "200", spans[i].Tag(ext.HTTPCode))
				assert.Nil(t, spans[i].Tag(ext.ErrorMsg))
			case 200, 400:
				assert.Equal(t, strconv.Itoa(status), spans[i].Tag(ext.HTTPCode))
				assert.Equal(t, fmt.Sprintf("%s: %s", strconv.Itoa(status), http.StatusText(status)), spans[i].Tag(ext.ErrorMsg))
			case 500:
				assert.Equal(t, strconv.Itoa(status), spans[i].Tag(ext.HTTPCode))
				assert.Nil(t, spans[i].Tag(ext.ErrorMsg))
			}
		}
	})
	t.Run("zero", func(t *testing.T) {
		mt := mocktracer.Start()
		defer mt.Stop()

		os.Setenv("DD_TRACE_HTTP_SERVER_ERROR_STATUSES", "0")

		// re-run config defaults based on new DD_TRACE_HTTP_SERVER_ERROR_STATUSES value
		ResetCfg()

		r := httptest.NewRequest(http.MethodGet, "/test", nil)
		sp, _, _ := StartRequestSpan(r)
		FinishRequestSpan(sp, 0, nil)
		spans := mt.FinishedSpans()
		require.Len(t, spans, 1)
		assert.Equal(t, "0", spans[0].Tag(ext.HTTPCode))
		assert.Equal(t, fmt.Sprintf("0: %s", http.StatusText(0)), spans[0].Tag(ext.ErrorMsg))
	})
}

func TestHeaderTagsFromRequest(t *testing.T) {
	mt := mocktracer.Start()
	defer mt.Stop()

	r := httptest.NewRequest(http.MethodGet, "/test", nil)
	r.Header.Set("header1", "val1")
	r.Header.Set("header2", " val2 ")
	r.Header.Set("header3", "v a l 3")
	r.Header.Set("x-datadog-header", "val4")

	expectedHeaderTags := map[string]string{
		"tag1": "val1",
		"tag2": "val2",
		"tag3": "v a l 3",
		"tag4": "val4",
	}

	hs := []string{"header1:tag1", "header2:tag2", "header3:tag3", "x-datadog-header:tag4"}
	ht := internal.NewLockMap(normalizer.HeaderTagSlice(hs))
	s, _, _ := StartRequestSpan(r, HeaderTagsFromRequest(r, ht))
	s.Finish()
	spans := mt.FinishedSpans()
	require.Len(t, spans, 1)

	for expectedTag, expectedTagVal := range expectedHeaderTags {
		assert.Equal(t, expectedTagVal, spans[0].Tags()[expectedTag])
	}
}

func TestStartRequestSpan(t *testing.T) {
	mt := mocktracer.Start()
	defer mt.Stop()
	r := httptest.NewRequest(http.MethodGet, "/somePath", nil)
	s, _, _ := StartRequestSpan(r)
	s.Finish()
	spans := mt.FinishedSpans()
	require.Len(t, spans, 1)
	assert.Equal(t, "example.com", spans[0].Tag("http.host"))
}

// TestClientIP tests behavior of StartRequestSpan based on
// the DD_TRACE_CLIENT_IP_ENABLED environment variable
func TestTraceClientIPFlag(t *testing.T) {
	mt := mocktracer.Start()
	defer mt.Stop()

	tp := new(log.RecordLogger)
	defer log.UseLogger(tp)()

	// use 0.0.0.0 as ip address of all test cases
	// more comprehensive ip address testing is done in testing
	// of ClientIPTags in appsec/dyngo/instrumentation/httpsec
	validIPAddr := "0.0.0.0"

	type ipTestCase struct {
		name                string
		remoteAddr          string
		traceClientIPEnvVal string
		expectTrace         bool
		expectedIP          netip.Addr
	}

	oldConfig := cfg
	defer func() { cfg = oldConfig }()

	for _, tc := range []ipTestCase{
		{
			name:                "Trace client IP set to true",
			remoteAddr:          validIPAddr,
			expectedIP:          netip.MustParseAddr(validIPAddr),
			traceClientIPEnvVal: "true",
			expectTrace:         true,
		},
		{
			name:                "Trace client IP set to false",
			remoteAddr:          validIPAddr,
			expectedIP:          netip.MustParseAddr(validIPAddr),
			traceClientIPEnvVal: "false",
			expectTrace:         false,
		},
		{
			name:                "Trace client IP unset",
			remoteAddr:          validIPAddr,
			expectedIP:          netip.MustParseAddr(validIPAddr),
			traceClientIPEnvVal: "",
			expectTrace:         false,
		},
		{
			name:                "Trace client IP set to non-boolean value",
			remoteAddr:          validIPAddr,
			expectedIP:          netip.MustParseAddr(validIPAddr),
			traceClientIPEnvVal: "asdadsasd",
			expectTrace:         false,
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			t.Setenv(envTraceClientIPEnabled, tc.traceClientIPEnvVal)

			// reset config based on new DD_TRACE_CLIENT_IP_ENABLED value
			cfg = newConfig()

			r := httptest.NewRequest(http.MethodGet, "/somePath", nil)
			r.RemoteAddr = tc.remoteAddr
			s, _, _ := StartRequestSpan(r)
			s.Finish()
			spans := mt.FinishedSpans()
			targetSpan := spans[0]

			if tc.expectTrace {
				assert.Equal(t, tc.expectedIP.String(), targetSpan.Tag(ext.HTTPClientIP))
			} else {
				assert.NotContains(t, targetSpan.Tags(), ext.HTTPClientIP)
				if _, err := strconv.ParseBool(tc.traceClientIPEnvVal); err != nil && tc.traceClientIPEnvVal != "" {
					logs := tp.Logs()
					assert.Contains(t, logs[len(logs)-1], "Non-boolean value for env var DD_TRACE_CLIENT_IP_ENABLED")
					tp.Reset()
				}
			}
			mt.Reset()
		})
	}
}

func TestURLTag(t *testing.T) {
	type URLTestCase struct {
		name, expectedURL, host, port, path, query, fragment string
	}
	for _, tc := range []URLTestCase{
		{
			name:        "no-host",
			expectedURL: "/test",
			path:        "/test",
		},
		{
			name:        "basic",
			expectedURL: "http://example.com",
			host:        "example.com",
		},
		{
			name:        "basic-path",
			expectedURL: "http://example.com/test",
			host:        "example.com",
			path:        "/test",
		},
		{
			name:        "basic-port",
			expectedURL: "http://example.com:8080",
			host:        "example.com",
			port:        "8080",
		},
		{
			name:        "basic-fragment",
			expectedURL: "http://example.com#test",
			host:        "example.com",
			fragment:    "test",
		},
		{
			name:        "query1",
			expectedURL: "http://example.com?test1=test2",
			host:        "example.com",
			query:       "test1=test2",
		},
		{
			name:        "query2",
			expectedURL: "http://example.com?test1=test2&test3=test4",
			host:        "example.com",
			query:       "test1=test2&test3=test4",
		},
		{
			name:        "combined",
			expectedURL: "http://example.com:7777/test?test1=test2&test3=test4#test",
			host:        "example.com",
			path:        "/test",
			query:       "test1=test2&test3=test4",
			port:        "7777",
			fragment:    "test",
		},
		{
			name:        "basic-obfuscation1",
			expectedURL: "http://example.com?<redacted>",
			host:        "example.com",
			query:       "token=value",
		},
		{
			name:        "basic-obfuscation2",
			expectedURL: "http://example.com?test0=test1&<redacted>&test1=test2",
			host:        "example.com",
			query:       "test0=test1&token=value&test1=test2",
		},
		{
			name:        "combined-obfuscation",
			expectedURL: "http://example.com:7777/test?test1=test2&<redacted>&test3=test4#test",
			host:        "example.com",
			path:        "/test",
			query:       "test1=test2&token=value&test3=test4",
			port:        "7777",
			fragment:    "test",
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			r := http.Request{
				URL: &url.URL{
					Path:     tc.path,
					RawQuery: tc.query,
					Fragment: tc.fragment,
				},
				Host: tc.host,
			}
			if tc.port != "" {
				r.Host += ":" + tc.port
			}
			url := URLFromRequest(&r, true)
			require.Equal(t, tc.expectedURL, url)
		})
	}
}

func BenchmarkStartRequestSpan(b *testing.B) {
	b.ReportAllocs()
	r, err := http.NewRequest("GET", "http://example.com", nil)
	if err != nil {
		b.Errorf("Failed to create request: %v", err)
		return
	}
	log.UseLogger(log.DiscardLogger{})
	opts := []tracer.StartSpanOption{
		tracer.ServiceName("SomeService"),
		tracer.ResourceName("SomeResource"),
		tracer.Tag(ext.HTTPRoute, "/some/route/?"),
	}
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		StartRequestSpan(r, opts...)
	}
}

func TestStartRequestSpanWithBaggage(t *testing.T) {
	t.Setenv("DD_TRACE_PROPAGATION_STYLE", "datadog,tracecontext,baggage")
	tracer.Start()
	defer tracer.Stop()

	r := httptest.NewRequest(http.MethodGet, "/somePath", nil)
	r.Header.Set("baggage", "key1=value1,key2=value2")
	s, ctx, _ := StartRequestSpan(r)
	s.Finish()
	// TODO: This behavior is not ideal. We want baggage headers accessible with r.Context (baggage.All(r.Context())) -- not the generated span's context.
	spanBm := make(map[string]string)
	s.Context().ForeachBaggageItem(func(k, v string) bool {
		spanBm[k] = v
		return true
	})
	assert.Equal(t, "value1", spanBm["key1"])
	assert.Equal(t, "value2", spanBm["key2"])
	baggageMap := baggage.All(ctx)
	assert.Equal(t, "value1", baggageMap["key1"], "should propagate baggage from header to context")
	assert.Equal(t, "value2", baggageMap["key2"], "should propagate baggage from header to context")
}

func TestBeforeHandleHTTPEndpoint(t *testing.T) {
	mt := mocktracer.Start()
	defer mt.Stop()

	type tc struct {
		name   string
		trn    string // DD_TRACE_RESOURCE_RENAMING_ENABLED
		always string // DD_TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT
		route  string
		expect func(*testing.T, *mocktracer.Span, *http.Request)
	}

	route := "/api/v1/users/{id}"
	cases := []tc{
		{
			name:   "no route, TRN=false, ALWAYS=false",
			trn:    "false",
			always: "false",
			route:  "",
			expect: func(t *testing.T, s *mocktracer.Span, r *http.Request) {
				assert.Equal(t, nil, s.Tag(ext.HTTPEndpoint))
			},
		},
		{
			name:   "no route, TRN=true, ALWAYS=false",
			trn:    "true",
			always: "false",
			route:  "",
			expect: func(t *testing.T, s *mocktracer.Span, r *http.Request) {
				expected := simplifyHTTPUrl(URLFromRequest(r, true))
				assert.Equal(t, expected, s.Tag(ext.HTTPEndpoint))
			},
		},
		{
			name:   "route present, TRN=true, ALWAYS=false",
			trn:    "true",
			always: "false",
			route:  route,
			expect: func(t *testing.T, s *mocktracer.Span, r *http.Request) {
				assert.Equal(t, route, s.Tag(ext.HTTPEndpoint))
			},
		},
		{
			name:   "route present, TRN=true, ALWAYS=true",
			trn:    "true",
			always: "true",
			route:  "/a/b/{id}",
			expect: func(t *testing.T, s *mocktracer.Span, r *http.Request) {
				expected := simplifyHTTPUrl(URLFromRequest(r, true))
				assert.Equal(t, expected, s.Tag(ext.HTTPEndpoint))
			},
		},
	}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			t.Setenv("DD_TRACE_RESOURCE_RENAMING_ENABLED", c.trn)
			t.Setenv("DD_TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT", c.always)
			ResetCfg()

			r := httptest.NewRequest(http.MethodGet, "https://example.com/api/v1/users/123?foo=bar", nil)
			w := httptest.NewRecorder()
			cfg := &ServeConfig{Route: c.route}

			rw, rt, after, handled := BeforeHandle(cfg, w, r)
			assert.False(t, handled)
			http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }).ServeHTTP(rw, rt)
			after()

			spans := mt.FinishedSpans()
			require.Len(t, spans, 1)
			c.expect(t, spans[0], r)
			mt.Reset()
		})
	}
}

func TestResourceRenamingActivation(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skipf("cgo disabled / no appsec tag")
	}

	type tc struct {
		name        string
		trn         *string // nil => unset
		appsecStart bool
		expectSet   bool
	}

	makeReq := func() (*http.Request, *httptest.ResponseRecorder, *ServeConfig) {
		r := httptest.NewRequest(http.MethodGet, "https://example.com/a/b/123", nil)
		w := httptest.NewRecorder()
		cfg := &ServeConfig{Route: "/a/b/{id}"}
		return r, w, cfg
	}

	trueStr := "true"
	falseStr := "false"

	cases := []tc{
		{name: "TRN true -> enabled", trn: &trueStr, appsecStart: false, expectSet: true},
		{name: "TRN false -> disabled", trn: &falseStr, appsecStart: false, expectSet: false},
		{name: "APPSEC true, TRN unset -> enabled", trn: nil, appsecStart: true, expectSet: true},
		{name: "APPSEC false, TRN unset -> disabled", trn: nil, appsecStart: false, expectSet: false},
	}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			os.Unsetenv("DD_TRACE_RESOURCE_RENAMING_ENABLED")
			os.Unsetenv("DD_TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT")
			os.Unsetenv("DD_APPSEC_ENABLED")

			mt := mocktracer.Start()
			defer mt.Stop()

			if c.trn != nil {
				t.Setenv("DD_TRACE_RESOURCE_RENAMING_ENABLED", *c.trn)
			}

			if c.appsecStart {
				os.Setenv("DD_APPSEC_ENABLED", "true")
				appsec.Start()
			}
			defer appsec.Stop()

			ResetCfg()

			r, w, cfg := makeReq()
			rw, rt, after, handled := BeforeHandle(cfg, w, r)
			assert.False(t, handled)
			http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }).ServeHTTP(rw, rt)
			after()

			spans := mt.FinishedSpans()
			require.Len(t, spans, 1)
			if c.expectSet {
				assert.NotEmpty(t, spans[0].Tag(ext.HTTPEndpoint))
			} else {
				assert.Empty(t, spans[0].Tag(ext.HTTPEndpoint))
			}
		})
	}
}

// Ensure the resource renaming is enabled only if appsec was enabled at the startup with the env var.
func TestResourceRenamingActivationAppSecNotStartup(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skipf("cgo disabled / no appsec tag")
	}

	mt := mocktracer.Start()
	defer mt.Stop()

	r := httptest.NewRequest(http.MethodGet, "https://example.com/a/b/123", nil)
	w := httptest.NewRecorder()
	cfg := &ServeConfig{Route: "/a/b/{id}"}

	rw, rt, after, handled := BeforeHandle(cfg, w, r)
	assert.False(t, handled)
	http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }).ServeHTTP(rw, rt)
	after()

	spans := mt.FinishedSpans()
	require.Len(t, spans, 1)
	assert.Empty(t, spans[0].Tag(ext.HTTPEndpoint))

	t.Setenv("DD_APPSEC_ENABLED", "true") // Force activation
	appsec.Start()
	defer appsec.Stop()

	rw, rt, after, handled = BeforeHandle(cfg, w, r)
	assert.False(t, handled)
	http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }).ServeHTTP(rw, rt)
	after()

	spans = mt.FinishedSpans()
	require.Len(t, spans, 2)
	assert.Empty(t, spans[1].Tag(ext.HTTPEndpoint))
}

func TestRenamedRouteSelection(t *testing.T) {
	type tc struct {
		name     string
		route    string
		endpoint string
		url      string // escaped path
		expect   string
	}

	cases := []tc{
		{
			name:   "route used when available",
			route:  "/users/{id}",
			url:    "/users/123",
			expect: "/users/{id}",
		},
		{
			name:     "endpoint used when route empty",
			endpoint: "/users/{id}",
			url:      "/users/123",
			expect:   "/users/{id}",
		},
		{
			name:   "fallback to simplified url when both empty",
			url:    "/a/b/123456",
			expect: simplifyHTTPUrl("/a/b/123456"),
		},
	}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			got := renamedRoute(c.route, c.endpoint, c.url)
			assert.Equal(t, c.expect, got)
		})
	}
}

func TestStartRequestSpanMergedBaggage(t *testing.T) {
	t.Setenv("DD_TRACE_PROPAGATION_STYLE", "datadog,tracecontext,baggage")
	tracer.Start()
	defer tracer.Stop()

	// Create a base context with pre-set baggage.
	baseCtx := baggage.Set(context.Background(), "pre_key", "pre_value")

	// Create an HTTP request with that context.
	req := httptest.NewRequest(http.MethodGet, "/somePath", nil).WithContext(baseCtx)

	// Set the baggage header with additional baggage items.
	req.Header.Set("baggage", "header_key=header_value,another_header=another_value")

	// Start the request span, which will extract header baggage and merge it with the context's baggage.
	span, ctx, _ := StartRequestSpan(req)
	span.Finish()

	// Retrieve the merged baggage from the span's context.
	mergedBaggage := baggage.All(ctx)

	// Verify that both pre-set and header baggage items are present.
	assert.Equal(t, "pre_value", mergedBaggage["pre_key"], "should contain pre-set baggage")
	assert.Equal(t, "header_value", mergedBaggage["header_key"], "should contain header baggage")
	assert.Equal(t, "another_value", mergedBaggage["another_header"], "should contain header baggage")
}

func TestBaggageSpanTagsOpentracer(t *testing.T) {
	tracer.Start()
	defer tracer.Stop()
	ctx := context.Background()

	// Create an HTTP request with no additional baggage context
	req := httptest.NewRequest(http.MethodGet, "/somePath", nil).WithContext(ctx)
	req.Header.Set("x-datadog-trace-id", "1")
	req.Header.Set("x-datadog-parent-id", "2")
	req.Header.Set("baggage", "session.id=789")  // w3c baggage header
	req.Header.Set("ot-baggage-user.id", "1234") // opentracer baggage header

	// Start the request span, which will extract baggage and add it as span tags
	reqSpan, _, _ := StartRequestSpan(req)
	m := reqSpan.AsMap()

	// Keys that SHOULD be present:
	assert.Contains(t, m, "baggage.session.id", "baggage.session.id should be included in span tags")
	assert.Equal(t, "789", m["baggage.session.id"], "should contain session.id value")

	// Keys that should NOT be present (user.id is ot-baggage header)
	// This assertion WILL FAIL until baggage revamp is complete; therefore, commented out
	// Baggage revamp Jira card: APMAPI-1442
	// assert.NotContains(t, m, "baggage.user.id", "baggage.user.id should not be included in span tags")

	reqSpan.Finish()
}

// baggageSpanTagTest represents a test case for baggage span tag functionality
type baggageSpanTagTest struct {
	name           string
	envValue       string            // DD_TRACE_BAGGAGE_TAG_KEYS value
	baggageHeader  string            // baggage header value
	preSetBaggage  map[string]string // baggage to set in context before request
	expectedTags   map[string]string // tags that should be present with their values
	unexpectedTags []string          // tag keys that should not be present
	needsResetCfg  bool              // whether to call ResetCfg()
}

// runBaggageSpanTagTest is a helper function that runs a baggage span tag test case
func runBaggageSpanTagTest(t *testing.T, tc baggageSpanTagTest) {
	t.Helper()

	// Set up environment variable if specified
	if tc.envValue != "" {
		os.Setenv("DD_TRACE_BAGGAGE_TAG_KEYS", tc.envValue)
		defer os.Unsetenv("DD_TRACE_BAGGAGE_TAG_KEYS")
		if tc.needsResetCfg {
			ResetCfg()
		}
	}

	tracer.Start()
	defer tracer.Stop()

	// Create base context with pre-set baggage if specified
	ctx := context.Background()
	for key, value := range tc.preSetBaggage {
		ctx = baggage.Set(ctx, key, value)
	}

	// Create HTTP request with context
	req := httptest.NewRequest(http.MethodGet, "/somePath", nil).WithContext(ctx)

	// Set baggage header
	if tc.baggageHeader != "" {
		req.Header.Set("baggage", tc.baggageHeader)
	}

	// Start request span and get span tags
	span, _, _ := StartRequestSpan(req)
	m := span.AsMap()

	// Check expected tags
	for key, expectedValue := range tc.expectedTags {
		assert.Contains(t, m, key, fmt.Sprintf("%s should be included in span tags", key))
		assert.Equal(t, expectedValue, m[key], fmt.Sprintf("should contain %s value", key))
	}

	// Check unexpected tags
	for _, key := range tc.unexpectedTags {
		assert.NotContains(t, m, key, fmt.Sprintf("%s should not be included in span tags", key))
	}

	span.Finish()
}

func TestBaggageSpanTags(t *testing.T) {
	tests := []baggageSpanTagTest{
		{
			name:          "default",
			baggageHeader: "header_key=header_value,account.id=456,session.id=789",
			preSetBaggage: map[string]string{"user.id": "1234"},
			expectedTags: map[string]string{
				"baggage.account.id": "456",
				"baggage.session.id": "789",
			},
			unexpectedTags: []string{"baggage.header_key", "baggage.user.id"},
		},
		{
			name:          "wildcard",
			envValue:      "*",
			needsResetCfg: true,
			baggageHeader: "user.id=abcd,account.id=456,session.id=789,color=blue,foo=bar",
			expectedTags: map[string]string{
				"baggage.account.id": "456",
				"baggage.user.id":    "abcd",
				"baggage.session.id": "789",
				"baggage.color":      "blue",
				"baggage.foo":        "bar",
			},
		},
		{
			name:          "disabled",
			envValue:      " ",
			needsResetCfg: true,
			baggageHeader: "user.id=abcd,account.id=456,session.id=789,color=blue",
			unexpectedTags: []string{
				"baggage.account.id",
				"baggage.user.id",
				"baggage.session.id",
				"baggage.color",
			},
		},
		{
			name:          "specify_keys",
			envValue:      "device,os.version,app.version",
			needsResetCfg: true,
			baggageHeader: "device=mobile,os.version=14.2,app.version=5.3.1,account.id=456,session.id=789,color=blue",
			expectedTags: map[string]string{
				"baggage.device":      "mobile",
				"baggage.os.version":  "14.2",
				"baggage.app.version": "5.3.1",
			},
			unexpectedTags: []string{
				"baggage.account.id",
				"baggage.session.id",
				"baggage.color",
			},
		},
		{
			name:          "asterisk_key",
			envValue:      "user.id,*version",
			needsResetCfg: true,
			baggageHeader: "usr.id=fakeuser,*version=9.4,app.version=9.1.2",
			expectedTags: map[string]string{
				"baggage.*version": "9.4",
			},
			unexpectedTags: []string{
				"baggage.user.id",
				"baggage.usr.id",
				"baggage.app.version",
			},
		},
		{
			name:          "malformed_header",
			baggageHeader: "user.id=,account.id=456,session.id=789,foo=bar",
			unexpectedTags: []string{
				"baggage.account.id",
				"baggage.user.id",
				"baggage.session.id",
				"baggage.foo",
			},
		},
		{
			name:          "case_sensitive",
			baggageHeader: "user.id=doggo,ACCOUNT.id=456,seSsIon.id=789",
			expectedTags: map[string]string{
				"baggage.user.id": "doggo",
			},
			unexpectedTags: []string{
				"baggage.account.id",
				"baggage.session.id",
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			runBaggageSpanTagTest(t, tt)
		})
	}
}

// TestStartRequestSpanOnlyBaggageCreatesNewTrace verifies that when only baggage headers are present
// (no trace/span IDs), a new trace is created with a non-zero trace ID while still preserving the baggage.
func TestStartRequestSpanOnlyBaggageCreatesNewTrace(t *testing.T) {
	tracer.Start()
	defer tracer.Stop()

	// Create a request with only baggage header, no trace context
	req := httptest.NewRequest(http.MethodGet, "/somePath", nil).WithContext(context.Background())
	req.Header.Set("baggage", "foo=bar")

	span, ctx, _ := StartRequestSpan(req)
	span.Finish()

	// Verify that a new trace was created with a non-zero trace ID
	sc := span.Context()
	lower := sc.TraceIDLower()
	assert.NotZero(
		t,
		lower,
		"expected a new non‐zero TraceIDLower when only baggage header is present",
	)

	// Verify that baggage is still propagated despite the new trace
	baggageMap := baggage.All(ctx)
	assert.Equal(t, "bar", baggageMap["foo"], "should propagate baggage even when it's the only header")

}
