// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package trace

import (
	"context"
	"errors"
	"fmt"
	"math"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/codes"
	"go.opentelemetry.io/otel/sdk"
	"go.opentelemetry.io/otel/sdk/instrumentation"
	"go.opentelemetry.io/otel/sdk/metric"
	"go.opentelemetry.io/otel/sdk/metric/metricdata"
	"go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest"
	"go.opentelemetry.io/otel/sdk/resource"
	"go.opentelemetry.io/otel/sdk/trace/internal/observ"
	semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
	"go.opentelemetry.io/otel/semconv/v1.40.0/otelconv"
	"go.opentelemetry.io/otel/trace"
)

const envVarResourceAttributes = "OTEL_RESOURCE_ATTRIBUTES"

type storingHandler struct {
	errs []error
}

func (s *storingHandler) Handle(err error) {
	s.errs = append(s.errs, err)
}

func (s *storingHandler) Reset() {
	s.errs = nil
}

var (
	tid trace.TraceID
	sid trace.SpanID
	sc  trace.SpanContext
	ts  trace.TraceState

	handler = &storingHandler{}
)

func init() {
	tid, _ = trace.TraceIDFromHex("01020304050607080102040810203040")
	sid, _ = trace.SpanIDFromHex("0102040810203040")
	sc = trace.NewSpanContext(trace.SpanContextConfig{
		TraceID:    tid,
		SpanID:     sid,
		TraceFlags: 0x1,
	})
	ts, _ = trace.ParseTraceState("k=v")

	otel.SetErrorHandler(handler)
}

func TestTracerFollowsExpectedAPIBehaviour(t *testing.T) {
	harness := newHarness(t)

	harness.testTracerProvider(func() trace.TracerProvider {
		return NewTracerProvider(WithSampler(TraceIDRatioBased(0)))
	})

	tp := NewTracerProvider(WithSampler(TraceIDRatioBased(0)))
	harness.testTracer(func() trace.Tracer {
		return tp.Tracer("")
	})
}

type testExporter struct {
	mu    sync.RWMutex
	idx   map[string]int
	spans []*snapshot
}

func NewTestExporter() *testExporter {
	return &testExporter{idx: make(map[string]int)}
}

func (te *testExporter) ExportSpans(_ context.Context, spans []ReadOnlySpan) error {
	te.mu.Lock()
	defer te.mu.Unlock()

	i := len(te.spans)
	for _, s := range spans {
		te.idx[s.Name()] = i
		te.spans = append(te.spans, s.(*snapshot))
		i++
	}
	return nil
}

func (te *testExporter) Spans() []*snapshot {
	te.mu.RLock()
	defer te.mu.RUnlock()

	cp := make([]*snapshot, len(te.spans))
	copy(cp, te.spans)
	return cp
}

func (te *testExporter) GetSpan(name string) (*snapshot, bool) {
	te.mu.RLock()
	defer te.mu.RUnlock()
	i, ok := te.idx[name]
	if !ok {
		return nil, false
	}
	return te.spans[i], true
}

func (te *testExporter) Len() int {
	te.mu.RLock()
	defer te.mu.RUnlock()
	return len(te.spans)
}

func (te *testExporter) Shutdown(context.Context) error {
	te.Reset()
	return nil
}

func (te *testExporter) Reset() {
	te.mu.Lock()
	defer te.mu.Unlock()
	te.idx = make(map[string]int)
	te.spans = te.spans[:0]
}

type testSampler struct {
	callCount int
	prefix    string
	t         *testing.T
}

func (ts *testSampler) ShouldSample(p SamplingParameters) SamplingResult {
	ts.callCount++
	ts.t.Logf("called sampler for name %q", p.Name)
	decision := Drop
	if strings.HasPrefix(p.Name, ts.prefix) {
		decision = RecordAndSample
	}
	return SamplingResult{
		Decision:   decision,
		Attributes: []attribute.KeyValue{attribute.Int("callCount", ts.callCount)},
	}
}

func (testSampler) Description() string {
	return "testSampler"
}

func TestSetName(t *testing.T) {
	tp := NewTracerProvider()

	type testCase struct {
		name    string
		newName string
	}
	for idx, tt := range []testCase{
		{ // 0
			name:    "foobar",
			newName: "foobaz",
		},
		{ // 1
			name:    "foobar",
			newName: "barbaz",
		},
		{ // 2
			name:    "barbar",
			newName: "barbaz",
		},
		{ // 3
			name:    "barbar",
			newName: "foobar",
		},
	} {
		sp := startNamedSpan(tp, "SetName", tt.name)
		if sdkspan, ok := sp.(*recordingSpan); ok {
			if sdkspan.Name() != tt.name {
				t.Errorf("%d: invalid name at span creation, expected %v, got %v", idx, tt.name, sdkspan.Name())
			}
		} else {
			t.Errorf("%d: unable to coerce span to SDK span, is type %T", idx, sp)
		}
		sp.SetName(tt.newName)
		if sdkspan, ok := sp.(*recordingSpan); ok {
			if sdkspan.Name() != tt.newName {
				t.Errorf("%d: span name not changed, expected %v, got %v", idx, tt.newName, sdkspan.Name())
			}
		} else {
			t.Errorf("%d: unable to coerce span to SDK span, is type %T", idx, sp)
		}
		sp.End()
	}
}

func TestSpanIsRecording(t *testing.T) {
	t.Run("while Span active", func(t *testing.T) {
		for name, tc := range map[string]struct {
			sampler Sampler
			want    bool
		}{
			"Always sample, recording on": {sampler: AlwaysSample(), want: true},
			"Never sample recording off":  {sampler: NeverSample(), want: false},
		} {
			tp := NewTracerProvider(WithSampler(tc.sampler))
			_, span := tp.Tracer(name).Start(t.Context(), "StartSpan")
			got := span.IsRecording()
			span.End()
			assert.Equal(t, tc.want, got, name)
		}
	})

	t.Run("after Span end", func(t *testing.T) {
		for name, tc := range map[string]Sampler{
			"Always Sample": AlwaysSample(),
			"Never Sample":  NeverSample(),
		} {
			tp := NewTracerProvider(WithSampler(tc))
			_, span := tp.Tracer(name).Start(t.Context(), "StartSpan")
			span.End()
			got := span.IsRecording()
			assert.False(t, got, name)
		}
	})
}

func TestSampling(t *testing.T) {
	idg := defaultIDGenerator()
	const total = 10000
	for name, tc := range map[string]struct {
		sampler       Sampler
		expect        float64
		parent        bool
		sampledParent bool
	}{
		// Span w/o a parent
		"NeverSample":           {sampler: NeverSample(), expect: 0},
		"AlwaysSample":          {sampler: AlwaysSample(), expect: 1.0},
		"TraceIdRatioBased_-1":  {sampler: TraceIDRatioBased(-1.0), expect: 0},
		"TraceIdRatioBased_.25": {sampler: TraceIDRatioBased(0.25), expect: .25},
		"TraceIdRatioBased_.50": {sampler: TraceIDRatioBased(0.50), expect: .5},
		"TraceIdRatioBased_.75": {sampler: TraceIDRatioBased(0.75), expect: .75},
		"TraceIdRatioBased_2.0": {sampler: TraceIDRatioBased(2.0), expect: 1},

		// Spans w/o a parent and using ParentBased(DelegateSampler()) Sampler, receive DelegateSampler's sampling decision
		"ParentNeverSample":           {sampler: ParentBased(NeverSample()), expect: 0},
		"ParentAlwaysSample":          {sampler: ParentBased(AlwaysSample()), expect: 1},
		"ParentTraceIdRatioBased_.50": {sampler: ParentBased(TraceIDRatioBased(0.50)), expect: .5},

		// An unadorned TraceIDRatioBased sampler ignores parent spans
		"UnsampledParentSpanWithTraceIdRatioBased_.25": {sampler: TraceIDRatioBased(0.25), expect: .25, parent: true},
		"SampledParentSpanWithTraceIdRatioBased_.25":   {sampler: TraceIDRatioBased(0.25), expect: .25, parent: true, sampledParent: true},
		"UnsampledParentSpanWithTraceIdRatioBased_.50": {sampler: TraceIDRatioBased(0.50), expect: .5, parent: true},
		"SampledParentSpanWithTraceIdRatioBased_.50":   {sampler: TraceIDRatioBased(0.50), expect: .5, parent: true, sampledParent: true},
		"UnsampledParentSpanWithTraceIdRatioBased_.75": {sampler: TraceIDRatioBased(0.75), expect: .75, parent: true},
		"SampledParentSpanWithTraceIdRatioBased_.75":   {sampler: TraceIDRatioBased(0.75), expect: .75, parent: true, sampledParent: true},

		// Spans with a sampled parent but using NeverSample Sampler, are not sampled
		"SampledParentSpanWithNeverSample": {sampler: NeverSample(), expect: 0, parent: true, sampledParent: true},

		// Spans with a sampled parent and using ParentBased(DelegateSampler()) Sampler, inherit the parent span's sampling status
		"SampledParentSpanWithParentNeverSample":             {sampler: ParentBased(NeverSample()), expect: 1, parent: true, sampledParent: true},
		"UnsampledParentSpanWithParentNeverSampler":          {sampler: ParentBased(NeverSample()), expect: 0, parent: true, sampledParent: false},
		"SampledParentSpanWithParentAlwaysSampler":           {sampler: ParentBased(AlwaysSample()), expect: 1, parent: true, sampledParent: true},
		"UnsampledParentSpanWithParentAlwaysSampler":         {sampler: ParentBased(AlwaysSample()), expect: 0, parent: true, sampledParent: false},
		"SampledParentSpanWithParentTraceIdRatioBased_.50":   {sampler: ParentBased(TraceIDRatioBased(0.50)), expect: 1, parent: true, sampledParent: true},
		"UnsampledParentSpanWithParentTraceIdRatioBased_.50": {sampler: ParentBased(TraceIDRatioBased(0.50)), expect: 0, parent: true, sampledParent: false},
	} {
		t.Run(name, func(t *testing.T) {
			t.Parallel()
			p := NewTracerProvider(WithSampler(tc.sampler))
			tr := p.Tracer("test")
			var sampled int
			for range total {
				ctx := t.Context()
				if tc.parent {
					tid, sid := idg.NewIDs(ctx)
					psc := trace.NewSpanContext(trace.SpanContextConfig{
						TraceID: tid,
						SpanID:  sid,
					})
					if tc.sampledParent {
						psc = psc.WithTraceFlags(trace.FlagsSampled)
					}
					ctx = trace.ContextWithRemoteSpanContext(ctx, psc)
				}
				_, span := tr.Start(ctx, "test")
				if span.SpanContext().IsSampled() {
					sampled++
				}
			}
			tolerance := 0.0
			got := float64(sampled) / float64(total)

			if tc.expect > 0 && tc.expect < 1 {
				// See https://en.wikipedia.org/wiki/Binomial_proportion_confidence_interval
				const z = 4.75342 // This should succeed 99.9999% of the time
				tolerance = z * math.Sqrt(got*(1-got)/total)
			}

			diff := math.Abs(got - tc.expect)
			if diff > tolerance {
				t.Errorf("got %f (diff: %f), expected %f (w/tolerance: %f)", got, diff, tc.expect, tolerance)
			}
		})
	}
}

func TestStartSpanWithParent(t *testing.T) {
	tp := NewTracerProvider()
	tr := tp.Tracer("SpanWithParent")
	ctx := t.Context()

	_, s1 := tr.Start(trace.ContextWithRemoteSpanContext(ctx, sc), "span1-unsampled-parent1")
	if err := checkChild(t, sc, s1); err != nil {
		t.Error(err)
	}

	_, s2 := tr.Start(trace.ContextWithRemoteSpanContext(ctx, sc), "span2-unsampled-parent1")
	if err := checkChild(t, sc, s2); err != nil {
		t.Error(err)
	}

	sc2 := sc.WithTraceState(ts)
	_, s3 := tr.Start(trace.ContextWithRemoteSpanContext(ctx, sc2), "span3-sampled-parent2")
	if err := checkChild(t, sc2, s3); err != nil {
		t.Error(err)
	}

	ctx2, s4 := tr.Start(trace.ContextWithRemoteSpanContext(ctx, sc2), "span4-sampled-parent2")
	if err := checkChild(t, sc2, s4); err != nil {
		t.Error(err)
	}

	s4Sc := s4.SpanContext()
	_, s5 := tr.Start(ctx2, "span5-implicit-childof-span4")
	if err := checkChild(t, s4Sc, s5); err != nil {
		t.Error(err)
	}
}

// Test we get a successful span as a new root if a nil context is sent in, as opposed to a panic.
// See https://github.com/open-telemetry/opentelemetry-go/issues/3109
func TestStartSpanWithNilContext(t *testing.T) {
	tp := NewTracerProvider()
	tr := tp.Tracer("NoPanic")

	// nolint:staticcheck // no nil context, but that's the point of the test.
	assert.NotPanics(t, func() { tr.Start(nil, "should-not-panic") })
}

func TestStartSpanNewRootNotSampled(t *testing.T) {
	alwaysSampleTp := NewTracerProvider()
	sampledTr := alwaysSampleTp.Tracer("AlwaysSampled")
	neverSampleTp := NewTracerProvider(WithSampler(ParentBased(NeverSample())))
	neverSampledTr := neverSampleTp.Tracer("ParentBasedNeverSample")
	ctx := t.Context()

	ctx, s1 := sampledTr.Start(trace.ContextWithRemoteSpanContext(ctx, sc), "span1-sampled")
	if err := checkChild(t, sc, s1); err != nil {
		t.Error(err)
	}

	_, s2 := neverSampledTr.Start(ctx, "span2-no-newroot")
	if !s2.SpanContext().IsSampled() {
		t.Error(
			fmt.Errorf(
				"got child span is not sampled, want child span with sampler: ParentBased(NeverSample()) to be sampled",
			),
		)
	}

	// Adding WithNewRoot causes child spans to not sample based on parent context
	_, s3 := neverSampledTr.Start(ctx, "span3-newroot", trace.WithNewRoot())
	if s3.SpanContext().IsSampled() {
		t.Error(
			fmt.Errorf(
				"got child span is sampled, want child span WithNewRoot() and with sampler: ParentBased(NeverSample()) to not be sampled",
			),
		)
	}
}

func TestSetSpanAttributesOnStart(t *testing.T) {
	te := NewTestExporter()
	tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty()))
	span := startSpan(tp,
		"StartSpanAttribute",
		trace.WithAttributes(attribute.String("key1", "value1")),
		trace.WithAttributes(attribute.String("key2", "value2")),
	)
	got, err := endSpan(te, span)
	if err != nil {
		t.Fatal(err)
	}

	want := &snapshot{
		spanContext: trace.NewSpanContext(trace.SpanContextConfig{
			TraceID:    tid,
			TraceFlags: 0x1,
		}),
		parent: sc.WithRemote(true),
		name:   "span0",
		attributes: []attribute.KeyValue{
			attribute.String("key1", "value1"),
			attribute.String("key2", "value2"),
		},
		spanKind:             trace.SpanKindInternal,
		instrumentationScope: instrumentation.Scope{Name: "StartSpanAttribute"},
	}
	if diff := cmpDiff(got, want); diff != "" {
		t.Errorf("SetSpanAttributesOnStart: -got +want %s", diff)
	}
}

func TestSamplerAttributesLocalChildSpan(t *testing.T) {
	sampler := &testSampler{prefix: "span", t: t}
	te := NewTestExporter()
	tp := NewTracerProvider(WithSampler(sampler), WithSyncer(te), WithResource(resource.Empty()))

	ctx := t.Context()
	ctx, span := startLocalSpan(ctx, tp, "SpanOne", "span0")
	_, spanTwo := startLocalSpan(ctx, tp, "SpanTwo", "span1")

	spanTwo.End()
	span.End()

	got := te.Spans()
	require.Len(t, got, 2)
	// FILO order above means spanTwo <-> gotSpan0 and span <-> gotSpan1.
	gotSpan0, gotSpan1 := got[0], got[1]
	// Ensure sampler is called for local child spans by verifying the
	// attributes set by the sampler are set on the child span.
	assert.Equal(t, []attribute.KeyValue{attribute.Int("callCount", 2)}, gotSpan0.Attributes())
	assert.Equal(t, []attribute.KeyValue{attribute.Int("callCount", 1)}, gotSpan1.Attributes())
}

func TestSpanSetAttributes(t *testing.T) {
	attrs := [...]attribute.KeyValue{
		attribute.String("key1", "value1"),
		attribute.String("key2", "value2"),
		attribute.String("key3", "value3"),
		attribute.String("key4", "value4"),
		attribute.String("key1", "value5"),
		attribute.String("key2", "value6"),
		attribute.String("key3", "value7"),
	}
	invalid := attribute.KeyValue{}

	tests := []struct {
		name        string
		input       [][]attribute.KeyValue
		wantAttrs   []attribute.KeyValue
		wantDropped int
	}{
		{
			name:      "array",
			input:     [][]attribute.KeyValue{attrs[:3]},
			wantAttrs: attrs[:3],
		},
		{
			name:      "single_value:array",
			input:     [][]attribute.KeyValue{attrs[:1], attrs[1:3]},
			wantAttrs: attrs[:3],
		},
		{
			name:      "array:single_value",
			input:     [][]attribute.KeyValue{attrs[:2], attrs[2:3]},
			wantAttrs: attrs[:3],
		},
		{
			name:      "single_values",
			input:     [][]attribute.KeyValue{attrs[:1], attrs[1:2], attrs[2:3]},
			wantAttrs: attrs[:3],
		},

		// The tracing specification states:
		//
		//   For each unique attribute key, addition of which would result in
		//   exceeding the limit, SDK MUST discard that key/value pair
		//
		// Therefore, adding attributes after the capacity is reached should
		// result in those attributes being dropped.

		{
			name:        "drop_last_added",
			input:       [][]attribute.KeyValue{attrs[:3], attrs[3:4], attrs[3:4]},
			wantAttrs:   attrs[:3],
			wantDropped: 2,
		},

		// The tracing specification states:
		//
		//   Setting an attribute with the same key as an existing attribute
		//   SHOULD overwrite the existing attribute's value.
		//
		// Therefore, attributes are updated regardless of capacity state.

		{
			name:      "single_value_update",
			input:     [][]attribute.KeyValue{attrs[:1], attrs[:3]},
			wantAttrs: attrs[:3],
		},
		{
			name:      "all_update",
			input:     [][]attribute.KeyValue{attrs[:3], attrs[4:7]},
			wantAttrs: attrs[4:7],
		},
		{
			name:      "all_update/multi",
			input:     [][]attribute.KeyValue{attrs[:3], attrs[4:7], attrs[:3]},
			wantAttrs: attrs[:3],
		},
		{
			name:      "deduplicate/under_capacity",
			input:     [][]attribute.KeyValue{attrs[:1], attrs[:1], attrs[:1]},
			wantAttrs: attrs[:1],
		},
		{
			name:      "deduplicate/over_capacity",
			input:     [][]attribute.KeyValue{attrs[:1], attrs[:1], attrs[:1], attrs[:3]},
			wantAttrs: attrs[:3],
		},
		{
			name: "deduplicate/added",
			input: [][]attribute.KeyValue{
				attrs[:2],
				{attrs[2], attrs[2], attrs[2]},
			},
			wantAttrs: attrs[:3],
		},
		{
			name: "deduplicate/added_at_cappacity",
			input: [][]attribute.KeyValue{
				attrs[:3],
				{attrs[2], attrs[2], attrs[2]},
			},
			wantAttrs: attrs[:3],
		},
		{
			name: "invalid",
			input: [][]attribute.KeyValue{
				{invalid},
			},
			wantDropped: 1,
		},
		{
			name: "invalid_with_valid",
			input: [][]attribute.KeyValue{
				{invalid, attrs[0]},
			},
			wantAttrs:   attrs[:1],
			wantDropped: 1,
		},
		{
			name: "invalid_over_capacity",
			input: [][]attribute.KeyValue{
				{invalid, invalid, invalid, invalid, attrs[0]},
			},
			wantAttrs:   attrs[:1],
			wantDropped: 4,
		},
		{
			name: "valid:invalid/under_capacity",
			input: [][]attribute.KeyValue{
				attrs[:1],
				{invalid},
			},
			wantAttrs:   attrs[:1],
			wantDropped: 1,
		},
		{
			name: "valid:invalid/over_capacity",
			input: [][]attribute.KeyValue{
				attrs[:1],
				{invalid, invalid, invalid, invalid},
			},
			wantAttrs:   attrs[:1],
			wantDropped: 4,
		},
		{
			name: "valid_at_capacity:invalid",
			input: [][]attribute.KeyValue{
				attrs[:3],
				{invalid, invalid, invalid, invalid},
			},
			wantAttrs:   attrs[:3],
			wantDropped: 4,
		},
	}

	const (
		capacity = 3
		instName = "TestSpanAttributeCapacity"
		spanName = "test span"
	)

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			te := NewTestExporter()
			sl := NewSpanLimits()
			sl.AttributeCountLimit = capacity
			tp := NewTracerProvider(WithSyncer(te), WithSpanLimits(sl))
			_, span := tp.Tracer(instName).Start(t.Context(), spanName)
			for _, a := range test.input {
				span.SetAttributes(a...)
			}
			span.End()

			require.Implements(t, (*ReadOnlySpan)(nil), span)
			roSpan := span.(ReadOnlySpan)

			// Ensure the span itself is valid.
			assert.ElementsMatch(t, test.wantAttrs, roSpan.Attributes(), "expected attributes")
			assert.Equal(t, test.wantDropped, roSpan.DroppedAttributes(), "dropped attributes")

			snap, ok := te.GetSpan(spanName)
			require.Truef(t, ok, "span %s not exported", spanName)

			// Ensure the exported span snapshot is valid.
			assert.ElementsMatch(t, test.wantAttrs, snap.Attributes(), "expected attributes")
			assert.Equal(t, test.wantDropped, snap.DroppedAttributes(), "dropped attributes")
		})
	}
}

func TestEvents(t *testing.T) {
	te := NewTestExporter()
	tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty()))

	span := startSpan(tp, "Events")
	k1v1 := attribute.String("key1", "value1")
	k2v2 := attribute.Bool("key2", true)
	k3v3 := attribute.Int64("key3", 3)

	span.AddEvent("foo", trace.WithAttributes(attribute.String("key1", "value1")))
	span.AddEvent("bar", trace.WithAttributes(
		attribute.Bool("key2", true),
		attribute.Int64("key3", 3),
	))
	got, err := endSpan(te, span)
	if err != nil {
		t.Fatal(err)
	}

	for i := range got.Events() {
		if !checkTime(&got.Events()[i].Time) {
			t.Error("exporting span: expected nonzero Event Time")
		}
	}

	want := &snapshot{
		spanContext: trace.NewSpanContext(trace.SpanContextConfig{
			TraceID:    tid,
			TraceFlags: 0x1,
		}),
		parent: sc.WithRemote(true),
		name:   "span0",
		events: []Event{
			{Name: "foo", Attributes: []attribute.KeyValue{k1v1}},
			{Name: "bar", Attributes: []attribute.KeyValue{k2v2, k3v3}},
		},
		spanKind:             trace.SpanKindInternal,
		instrumentationScope: instrumentation.Scope{Name: "Events"},
	}
	if diff := cmpDiff(got, want); diff != "" {
		t.Errorf("Message Events: -got +want %s", diff)
	}
}

func TestEventsOverLimit(t *testing.T) {
	te := NewTestExporter()
	sl := NewSpanLimits()
	sl.EventCountLimit = 2
	tp := NewTracerProvider(WithSpanLimits(sl), WithSyncer(te), WithResource(resource.Empty()))

	span := startSpan(tp, "EventsOverLimit")
	k1v1 := attribute.String("key1", "value1")
	k2v2 := attribute.Bool("key2", false)
	k3v3 := attribute.String("key3", "value3")

	span.AddEvent("fooDrop", trace.WithAttributes(attribute.String("key1", "value1")))
	span.AddEvent("barDrop", trace.WithAttributes(
		attribute.Bool("key2", true),
		attribute.String("key3", "value3"),
	))
	span.AddEvent("foo", trace.WithAttributes(attribute.String("key1", "value1")))
	span.AddEvent("bar", trace.WithAttributes(
		attribute.Bool("key2", false),
		attribute.String("key3", "value3"),
	))
	got, err := endSpan(te, span)
	if err != nil {
		t.Fatal(err)
	}

	for i := range got.Events() {
		if !checkTime(&got.Events()[i].Time) {
			t.Error("exporting span: expected nonzero Event Time")
		}
	}

	want := &snapshot{
		spanContext: trace.NewSpanContext(trace.SpanContextConfig{
			TraceID:    tid,
			TraceFlags: 0x1,
		}),
		parent: sc.WithRemote(true),
		name:   "span0",
		events: []Event{
			{Name: "foo", Attributes: []attribute.KeyValue{k1v1}},
			{Name: "bar", Attributes: []attribute.KeyValue{k2v2, k3v3}},
		},
		droppedEventCount:    2,
		spanKind:             trace.SpanKindInternal,
		instrumentationScope: instrumentation.Scope{Name: "EventsOverLimit"},
	}
	if diff := cmpDiff(got, want); diff != "" {
		t.Errorf("Message Event over limit: -got +want %s", diff)
	}
}

func TestLinks(t *testing.T) {
	te := NewTestExporter()
	tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty()))

	k1v1 := attribute.String("key1", "value1")
	k2v2 := attribute.String("key2", "value2")
	k3v3 := attribute.String("key3", "value3")

	sc1 := trace.NewSpanContext(
		trace.SpanContextConfig{TraceID: trace.TraceID([16]byte{1, 1}), SpanID: trace.SpanID{3}},
	)
	sc2 := trace.NewSpanContext(
		trace.SpanContextConfig{TraceID: trace.TraceID([16]byte{1, 1}), SpanID: trace.SpanID{3}},
	)

	l1 := trace.Link{SpanContext: sc1, Attributes: []attribute.KeyValue{k1v1}}
	l2 := trace.Link{SpanContext: sc2, Attributes: []attribute.KeyValue{k2v2, k3v3}}

	links := []trace.Link{l1, l2}
	span := startSpan(tp, "Links", trace.WithLinks(links...))

	got, err := endSpan(te, span)
	if err != nil {
		t.Fatal(err)
	}

	want := &snapshot{
		spanContext: trace.NewSpanContext(trace.SpanContextConfig{
			TraceID:    tid,
			TraceFlags: 0x1,
		}),
		parent:               sc.WithRemote(true),
		name:                 "span0",
		links:                []Link{{l1.SpanContext, l1.Attributes, 0}, {l2.SpanContext, l2.Attributes, 0}},
		spanKind:             trace.SpanKindInternal,
		instrumentationScope: instrumentation.Scope{Name: "Links"},
	}
	if diff := cmpDiff(got, want); diff != "" {
		t.Errorf("Link: -got +want %s", diff)
	}
	sc1 = trace.NewSpanContext(trace.SpanContextConfig{TraceID: trace.TraceID([16]byte{1, 1}), SpanID: trace.SpanID{3}})

	span1 := startSpan(tp, "name", trace.WithLinks([]trace.Link{
		{SpanContext: trace.SpanContext{}},
		{SpanContext: sc1},
	}...))

	sdkspan, _ := span1.(*recordingSpan)
	require.Len(t, sdkspan.Links(), 1)
}

func TestLinksOverLimit(t *testing.T) {
	te := NewTestExporter()

	sc1 := trace.NewSpanContext(
		trace.SpanContextConfig{TraceID: trace.TraceID([16]byte{1, 1}), SpanID: trace.SpanID{3}},
	)
	sc2 := trace.NewSpanContext(
		trace.SpanContextConfig{TraceID: trace.TraceID([16]byte{1, 1}), SpanID: trace.SpanID{3}},
	)
	sc3 := trace.NewSpanContext(
		trace.SpanContextConfig{TraceID: trace.TraceID([16]byte{1, 1}), SpanID: trace.SpanID{3}},
	)

	sl := NewSpanLimits()
	sl.LinkCountLimit = 2
	tp := NewTracerProvider(WithSpanLimits(sl), WithSyncer(te), WithResource(resource.Empty()))

	span := startSpan(tp, "LinksOverLimit",
		trace.WithLinks(
			trace.Link{SpanContext: sc1, Attributes: []attribute.KeyValue{attribute.String("key1", "value1")}},
			trace.Link{SpanContext: sc2, Attributes: []attribute.KeyValue{attribute.String("key2", "value2")}},
			trace.Link{SpanContext: sc3, Attributes: []attribute.KeyValue{attribute.String("key3", "value3")}},
		),
	)

	k2v2 := attribute.String("key2", "value2")
	k3v3 := attribute.String("key3", "value3")

	got, err := endSpan(te, span)
	if err != nil {
		t.Fatal(err)
	}

	want := &snapshot{
		spanContext: trace.NewSpanContext(trace.SpanContextConfig{
			TraceID:    tid,
			TraceFlags: 0x1,
		}),
		parent: sc.WithRemote(true),
		name:   "span0",
		links: []Link{
			{SpanContext: sc2, Attributes: []attribute.KeyValue{k2v2}, DroppedAttributeCount: 0},
			{SpanContext: sc3, Attributes: []attribute.KeyValue{k3v3}, DroppedAttributeCount: 0},
		},
		droppedLinkCount:     1,
		spanKind:             trace.SpanKindInternal,
		instrumentationScope: instrumentation.Scope{Name: "LinksOverLimit"},
	}
	if diff := cmpDiff(got, want); diff != "" {
		t.Errorf("Link over limit: -got +want %s", diff)
	}
}

func TestSetSpanName(t *testing.T) {
	te := NewTestExporter()
	tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty()))
	ctx := t.Context()

	want := "SpanName-1"
	ctx = trace.ContextWithRemoteSpanContext(ctx, sc)
	_, span := tp.Tracer("SetSpanName").Start(ctx, "SpanName-1")
	got, err := endSpan(te, span)
	if err != nil {
		t.Fatal(err)
	}

	if got.Name() != want {
		t.Errorf("span.Name: got %q; want %q", got.Name(), want)
	}
}

func TestSetSpanStatus(t *testing.T) {
	te := NewTestExporter()
	tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty()))

	span := startSpan(tp, "SpanStatus")
	span.SetStatus(codes.Error, "Error")
	got, err := endSpan(te, span)
	if err != nil {
		t.Fatal(err)
	}

	want := &snapshot{
		spanContext: trace.NewSpanContext(trace.SpanContextConfig{
			TraceID:    tid,
			TraceFlags: 0x1,
		}),
		parent:   sc.WithRemote(true),
		name:     "span0",
		spanKind: trace.SpanKindInternal,
		status: Status{
			Code:        codes.Error,
			Description: "Error",
		},
		instrumentationScope: instrumentation.Scope{Name: "SpanStatus"},
	}
	if diff := cmpDiff(got, want); diff != "" {
		t.Errorf("SetSpanStatus: -got +want %s", diff)
	}
}

func TestSetSpanStatusWithoutMessageWhenStatusIsNotError(t *testing.T) {
	te := NewTestExporter()
	tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty()))

	span := startSpan(tp, "SpanStatus")
	span.SetStatus(codes.Ok, "This message will be ignored")
	got, err := endSpan(te, span)
	if err != nil {
		t.Fatal(err)
	}

	want := &snapshot{
		spanContext: trace.NewSpanContext(trace.SpanContextConfig{
			TraceID:    tid,
			TraceFlags: 0x1,
		}),
		parent:   sc.WithRemote(true),
		name:     "span0",
		spanKind: trace.SpanKindInternal,
		status: Status{
			Code:        codes.Ok,
			Description: "",
		},
		instrumentationScope: instrumentation.Scope{Name: "SpanStatus"},
	}
	if diff := cmpDiff(got, want); diff != "" {
		t.Errorf("SetSpanStatus: -got +want %s", diff)
	}
}

func cmpDiff(x, y any) string {
	return cmp.Diff(x, y,
		cmp.AllowUnexported(snapshot{}),
		cmp.AllowUnexported(attribute.Value{}),
		cmp.AllowUnexported(Event{}),
		cmp.AllowUnexported(trace.TraceState{}),
		cmp.Comparer(func(x, y attribute.Set) bool {
			return x.Equals(&y)
		}),
	)
}

// checkChild is test utility function that tests that c has fields set appropriately,
// given that it is a child span of p.
func checkChild(t *testing.T, p trace.SpanContext, apiSpan trace.Span) error {
	s := apiSpan.(*recordingSpan)
	if s == nil {
		return fmt.Errorf("got nil child span, want non-nil")
	}
	if got, want := s.spanContext.TraceID().String(), p.TraceID().String(); got != want {
		return fmt.Errorf("got child trace ID %s, want %s", got, want)
	}
	if childID, parentID := s.spanContext.SpanID().String(), p.SpanID().String(); childID == parentID {
		return fmt.Errorf("got child span ID %s, parent span ID %s; want unequal IDs", childID, parentID)
	}
	if got, want := s.spanContext.TraceFlags(), p.TraceFlags(); got != want {
		return fmt.Errorf("got child trace options %d, want %d", got, want)
	}
	got, want := s.spanContext.TraceState(), p.TraceState()
	assert.Equal(t, want, got)
	return nil
}

// startSpan starts a span with a name "span0". See startNamedSpan for
// details.
func startSpan(tp *TracerProvider, trName string, args ...trace.SpanStartOption) trace.Span {
	return startNamedSpan(tp, trName, "span0", args...)
}

// startNamed Span is a test utility func that starts a span with a
// passed name and with remote span context as parent. The remote span
// context contains TraceFlags with sampled bit set. This allows the
// span to be automatically sampled.
func startNamedSpan(tp *TracerProvider, trName, name string, args ...trace.SpanStartOption) trace.Span {
	_, span := tp.Tracer(trName).Start(
		trace.ContextWithRemoteSpanContext(context.Background(), sc),
		name,
		args...,
	)
	return span
}

// startLocalSpan is a test utility func that starts a span with a
// passed name and with the passed context. The context is returned
// along with the span so this parent can be used to create child
// spans.
func startLocalSpan(
	ctx context.Context,
	tp *TracerProvider,
	trName, name string,
	args ...trace.SpanStartOption,
) (context.Context, trace.Span) {
	ctx, span := tp.Tracer(trName).Start(
		ctx,
		name,
		args...,
	)
	return ctx, span
}

// endSpan is a test utility function that ends the span in the context and
// returns the exported span.
// It requires that span be sampled using one of these methods
//  1. Passing parent span context in context
//  2. Use WithSampler(AlwaysSample())
//  3. Configuring AlwaysSample() as default sampler
//
// It also does some basic tests on the span.
// It also clears spanID in the to make the comparison easier.
func endSpan(te *testExporter, span trace.Span) (*snapshot, error) {
	if !span.IsRecording() {
		return nil, fmt.Errorf("method IsRecording: got false, want true")
	}
	if !span.SpanContext().IsSampled() {
		return nil, fmt.Errorf("method IsSampled: got false, want true")
	}
	span.End()
	if te.Len() != 1 {
		return nil, fmt.Errorf("got %d exported spans, want one span", te.Len())
	}
	got := te.Spans()[0]
	if !got.SpanContext().SpanID().IsValid() {
		return nil, fmt.Errorf("exporting span: expected nonzero SpanID")
	}
	got.spanContext = got.SpanContext().WithSpanID(trace.SpanID{})
	if !checkTime(&got.startTime) {
		return nil, fmt.Errorf("exporting span: expected nonzero StartTime")
	}
	if !checkTime(&got.endTime) {
		return nil, fmt.Errorf("exporting span: expected nonzero EndTime")
	}
	return got, nil
}

// checkTime checks that a nonzero time was set in x, then clears it.
func checkTime(x *time.Time) bool {
	if x.IsZero() {
		return false
	}
	*x = time.Time{}
	return true
}

func TestEndSpanTwice(t *testing.T) {
	te := NewTestExporter()
	tp := NewTracerProvider(WithSyncer(te))

	st := time.Now()
	et1 := st.Add(100 * time.Millisecond)
	et2 := st.Add(200 * time.Millisecond)

	span := startSpan(tp, "EndSpanTwice", trace.WithTimestamp(st))
	span.End(trace.WithTimestamp(et1))
	span.End(trace.WithTimestamp(et2))

	if te.Len() != 1 {
		t.Fatalf("expected only a single span, got %#v", te.Spans())
	}

	ro := span.(ReadOnlySpan)
	if ro.EndTime() != et1 {
		t.Fatalf("2nd call to End() should not modify end time")
	}
}

func TestStartSpanAfterEnd(t *testing.T) {
	te := NewTestExporter()
	tp := NewTracerProvider(WithSampler(AlwaysSample()), WithSyncer(te))
	ctx := t.Context()

	tr := tp.Tracer("SpanAfterEnd")
	ctx, span0 := tr.Start(trace.ContextWithRemoteSpanContext(ctx, sc), "parent")
	ctx1, span1 := tr.Start(ctx, "span-1")
	span1.End()
	// Start a new span with the context containing span-1
	// even though span-1 is ended, we still add this as a new child of span-1
	_, span2 := tr.Start(ctx1, "span-2")
	span2.End()
	span0.End()
	if got, want := te.Len(), 3; got != want {
		t.Fatalf("len(%#v) = %d; want %d", te.Spans(), got, want)
	}

	gotParent, ok := te.GetSpan("parent")
	if !ok {
		t.Fatal("parent not recorded")
	}
	gotSpan1, ok := te.GetSpan("span-1")
	if !ok {
		t.Fatal("span-1 not recorded")
	}
	gotSpan2, ok := te.GetSpan("span-2")
	if !ok {
		t.Fatal("span-2 not recorded")
	}

	if got, want := gotSpan1.SpanContext().TraceID(), gotParent.SpanContext().TraceID(); got != want {
		t.Errorf("span-1.TraceID=%q; want %q", got, want)
	}
	if got, want := gotSpan2.SpanContext().TraceID(), gotParent.SpanContext().TraceID(); got != want {
		t.Errorf("span-2.TraceID=%q; want %q", got, want)
	}
	if got, want := gotSpan1.Parent().SpanID(), gotParent.SpanContext().SpanID(); got != want {
		t.Errorf("span-1.ParentSpanID=%q; want %q (parent.SpanID)", got, want)
	}
	if got, want := gotSpan2.Parent().SpanID(), gotSpan1.SpanContext().SpanID(); got != want {
		t.Errorf("span-2.ParentSpanID=%q; want %q (span1.SpanID)", got, want)
	}
}

func TestChildSpanCount(t *testing.T) {
	te := NewTestExporter()
	tp := NewTracerProvider(WithSampler(AlwaysSample()), WithSyncer(te))

	tr := tp.Tracer("ChidSpanCount")
	ctx, span0 := tr.Start(t.Context(), "parent")
	ctx1, span1 := tr.Start(ctx, "span-1")
	_, span2 := tr.Start(ctx1, "span-2")
	span2.End()
	span1.End()

	_, span3 := tr.Start(ctx, "span-3")
	span3.End()
	span0.End()
	if got, want := te.Len(), 4; got != want {
		t.Fatalf("len(%#v) = %d; want %d", te.Spans(), got, want)
	}

	gotParent, ok := te.GetSpan("parent")
	if !ok {
		t.Fatal("parent not recorded")
	}
	gotSpan1, ok := te.GetSpan("span-1")
	if !ok {
		t.Fatal("span-1 not recorded")
	}
	gotSpan2, ok := te.GetSpan("span-2")
	if !ok {
		t.Fatal("span-2 not recorded")
	}
	gotSpan3, ok := te.GetSpan("span-3")
	if !ok {
		t.Fatal("span-3 not recorded")
	}

	if got, want := gotSpan3.ChildSpanCount(), 0; got != want {
		t.Errorf("span-3.ChildSpanCount=%d; want %d", got, want)
	}
	if got, want := gotSpan2.ChildSpanCount(), 0; got != want {
		t.Errorf("span-2.ChildSpanCount=%d; want %d", got, want)
	}
	if got, want := gotSpan1.ChildSpanCount(), 1; got != want {
		t.Errorf("span-1.ChildSpanCount=%d; want %d", got, want)
	}
	if got, want := gotParent.ChildSpanCount(), 2; got != want {
		t.Errorf("parent.ChildSpanCount=%d; want %d", got, want)
	}
}

func TestNilSpanEnd(*testing.T) {
	var span *recordingSpan
	span.End()
}

func TestSpanWithCanceledContext(t *testing.T) {
	te := NewTestExporter()
	tp := NewTracerProvider(WithSyncer(te))

	ctx, cancel := context.WithCancel(t.Context())
	cancel()
	_, span := tp.Tracer(t.Name()).Start(ctx, "span")
	span.End()

	assert.Equal(t, 1, te.Len(), "span recording must ignore context cancellation")
}

func TestNonRecordingSpanDoesNotTrackRuntimeTracerTask(t *testing.T) {
	tp := NewTracerProvider(WithSampler(NeverSample()))
	tr := tp.Tracer("TestNonRecordingSpanDoesNotTrackRuntimeTracerTask")

	_, apiSpan := tr.Start(t.Context(), "foo")
	if _, ok := apiSpan.(runtimeTracer); ok {
		t.Fatalf("non recording span implements runtime trace task tracking")
	}
}

func TestRecordingSpanRuntimeTracerTaskEnd(t *testing.T) {
	tp := NewTracerProvider(WithSampler(AlwaysSample()))
	tr := tp.Tracer("TestRecordingSpanRuntimeTracerTaskEnd")

	var n atomic.Uint64
	executionTracerTaskEnd := func() {
		n.Add(1)
	}
	_, apiSpan := tr.Start(t.Context(), "foo")
	s, ok := apiSpan.(*recordingSpan)
	if !ok {
		t.Fatal("recording span not returned from always sampled Tracer")
	}

	s.executionTracerTaskEnd = executionTracerTaskEnd
	s.End()

	if n.Load() != 1 {
		t.Error("recording span did not end runtime trace task")
	}
}

func TestCustomStartEndTime(t *testing.T) {
	te := NewTestExporter()
	tp := NewTracerProvider(WithSyncer(te), WithSampler(AlwaysSample()))

	startTime := time.Date(2019, time.August, 27, 14, 42, 0, 0, time.UTC)
	endTime := startTime.Add(time.Second * 20)
	_, span := tp.Tracer("Custom Start and End time").Start(
		t.Context(),
		"testspan",
		trace.WithTimestamp(startTime),
	)
	span.End(trace.WithTimestamp(endTime))

	if te.Len() != 1 {
		t.Fatalf("got %d exported spans, want one span", te.Len())
	}
	got := te.Spans()[0]
	if !got.StartTime().Equal(startTime) {
		t.Errorf("expected start time to be %s, got %s", startTime, got.StartTime())
	}
	if !got.EndTime().Equal(endTime) {
		t.Errorf("expected end time to be %s, got %s", endTime, got.EndTime())
	}
}

func TestRecordError(t *testing.T) {
	scenarios := []struct {
		err error
		typ string
		msg string
	}{
		{
			err: newTestError("test error"),
			typ: "go.opentelemetry.io/otel/sdk/trace.testError",
			msg: "test error",
		},
		{
			err: errors.New("test error 2"),
			typ: "*errors.errorString",
			msg: "test error 2",
		},
	}

	for _, s := range scenarios {
		te := NewTestExporter()
		tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty()))
		span := startSpan(tp, "RecordError")

		errTime := time.Now()
		span.RecordError(s.err, trace.WithTimestamp(errTime))

		got, err := endSpan(te, span)
		if err != nil {
			t.Fatal(err)
		}

		want := &snapshot{
			spanContext: trace.NewSpanContext(trace.SpanContextConfig{
				TraceID:    tid,
				TraceFlags: 0x1,
			}),
			parent:   sc.WithRemote(true),
			name:     "span0",
			status:   Status{Code: codes.Unset},
			spanKind: trace.SpanKindInternal,
			events: []Event{
				{
					Name: semconv.ExceptionEventName,
					Time: errTime,
					Attributes: []attribute.KeyValue{
						semconv.ExceptionType(s.typ),
						semconv.ExceptionMessage(s.msg),
					},
				},
			},
			instrumentationScope: instrumentation.Scope{Name: "RecordError"},
		}
		if diff := cmpDiff(got, want); diff != "" {
			t.Errorf("SpanErrorOptions: -got +want %s", diff)
		}
	}
}

func TestRecordErrorWithStackTrace(t *testing.T) {
	err := newTestError("test error")
	typ := "go.opentelemetry.io/otel/sdk/trace.testError"
	msg := "test error"

	te := NewTestExporter()
	tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty()))
	span := startSpan(tp, "RecordError")

	errTime := time.Now()
	span.RecordError(err, trace.WithTimestamp(errTime), trace.WithStackTrace(true))

	got, err := endSpan(te, span)
	if err != nil {
		t.Fatal(err)
	}

	want := &snapshot{
		spanContext: trace.NewSpanContext(trace.SpanContextConfig{
			TraceID:    tid,
			TraceFlags: 0x1,
		}),
		parent:   sc.WithRemote(true),
		name:     "span0",
		status:   Status{Code: codes.Unset},
		spanKind: trace.SpanKindInternal,
		events: []Event{
			{
				Name: semconv.ExceptionEventName,
				Time: errTime,
				Attributes: []attribute.KeyValue{
					semconv.ExceptionType(typ),
					semconv.ExceptionMessage(msg),
				},
			},
		},
		instrumentationScope: instrumentation.Scope{Name: "RecordError"},
	}

	assert.Equal(t, want.spanContext, got.spanContext)
	assert.Equal(t, want.parent, got.parent)
	assert.Equal(t, want.name, got.name)
	assert.Equal(t, want.status, got.status)
	assert.Equal(t, want.spanKind, got.spanKind)
	assert.Equal(t, got.events[0].Attributes[0].Value.AsString(), want.events[0].Attributes[0].Value.AsString())
	assert.Equal(t, got.events[0].Attributes[1].Value.AsString(), want.events[0].Attributes[1].Value.AsString())
	gotStackTraceFunctionName := strings.Split(got.events[0].Attributes[2].Value.AsString(), "\n")

	assert.Truef(
		t,
		strings.HasPrefix(gotStackTraceFunctionName[1], "go.opentelemetry.io/otel/sdk/trace.recordStackTrace"),
		"%q not prefixed with go.opentelemetry.io/otel/sdk/trace.recordStackTrace",
		gotStackTraceFunctionName[1],
	)
	assert.Truef(
		t,
		strings.HasPrefix(
			gotStackTraceFunctionName[3],
			"go.opentelemetry.io/otel/sdk/trace.(*recordingSpan).RecordError",
		),
		"%q not prefixed with go.opentelemetry.io/otel/sdk/trace.(*recordingSpan).RecordError",
		gotStackTraceFunctionName[3],
	)
}

func TestRecordErrorNil(t *testing.T) {
	te := NewTestExporter()
	tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty()))
	span := startSpan(tp, "RecordErrorNil")

	span.RecordError(nil)

	got, err := endSpan(te, span)
	if err != nil {
		t.Fatal(err)
	}

	want := &snapshot{
		spanContext: trace.NewSpanContext(trace.SpanContextConfig{
			TraceID:    tid,
			TraceFlags: 0x1,
		}),
		parent:   sc.WithRemote(true),
		name:     "span0",
		spanKind: trace.SpanKindInternal,
		status: Status{
			Code:        codes.Unset,
			Description: "",
		},
		instrumentationScope: instrumentation.Scope{Name: "RecordErrorNil"},
	}
	if diff := cmpDiff(got, want); diff != "" {
		t.Errorf("SpanErrorOptions: -got +want %s", diff)
	}
}

func TestWithSpanKind(t *testing.T) {
	te := NewTestExporter()
	tp := NewTracerProvider(WithSyncer(te), WithSampler(AlwaysSample()), WithResource(resource.Empty()))
	tr := tp.Tracer("withSpanKind")

	_, span := tr.Start(t.Context(), "WithoutSpanKind")
	spanData, err := endSpan(te, span)
	if err != nil {
		t.Error(err.Error())
	}

	if spanData.SpanKind() != trace.SpanKindInternal {
		t.Errorf(
			"Default value of Spankind should be Internal: got %+v, want %+v\n",
			spanData.SpanKind(),
			trace.SpanKindInternal,
		)
	}

	sks := []trace.SpanKind{
		trace.SpanKindInternal,
		trace.SpanKindServer,
		trace.SpanKindClient,
		trace.SpanKindProducer,
		trace.SpanKindConsumer,
	}

	for _, sk := range sks {
		te.Reset()

		_, span := tr.Start(t.Context(), fmt.Sprintf("SpanKind-%v", sk), trace.WithSpanKind(sk))
		spanData, err := endSpan(te, span)
		if err != nil {
			t.Error(err.Error())
		}

		if spanData.SpanKind() != sk {
			t.Errorf("WithSpanKind check: got %+v, want %+v\n", spanData.SpanKind(), sks)
		}
	}
}

func mergeResource(t *testing.T, r1, r2 *resource.Resource) *resource.Resource {
	r, err := resource.Merge(r1, r2)
	assert.NoError(t, err)
	return r
}

func TestWithResource(t *testing.T) {
	t.Setenv(envVarResourceAttributes, "key=value,rk5=7")
	cases := []struct {
		name    string
		options []TracerProviderOption
		want    *resource.Resource
		msg     string
	}{
		{
			name:    "explicitly empty resource",
			options: []TracerProviderOption{WithResource(resource.Empty())},
			want:    resource.Environment(),
		},
		{
			name:    "uses default if no resource option",
			options: []TracerProviderOption{},
			want:    resource.Default(),
		},
		{
			name: "explicit resource",
			options: []TracerProviderOption{
				WithResource(resource.NewSchemaless(attribute.String("rk1", "rv1"), attribute.Int64("rk2", 5))),
			},
			want: mergeResource(
				t,
				resource.Environment(),
				resource.NewSchemaless(attribute.String("rk1", "rv1"), attribute.Int64("rk2", 5)),
			),
		},
		{
			name: "last resource wins",
			options: []TracerProviderOption{
				WithResource(resource.NewSchemaless(attribute.String("rk1", "vk1"), attribute.Int64("rk2", 5))),
				WithResource(resource.NewSchemaless(attribute.String("rk3", "rv3"), attribute.Int64("rk4", 10))),
			},
			want: mergeResource(
				t,
				resource.Environment(),
				resource.NewSchemaless(attribute.String("rk3", "rv3"), attribute.Int64("rk4", 10)),
			),
		},
		{
			name: "overlapping attributes with environment resource",
			options: []TracerProviderOption{
				WithResource(resource.NewSchemaless(attribute.String("rk1", "rv1"), attribute.Int64("rk5", 10))),
			},
			want: mergeResource(
				t,
				resource.Environment(),
				resource.NewSchemaless(attribute.String("rk1", "rv1"), attribute.Int64("rk5", 10)),
			),
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			te := NewTestExporter()
			defaultOptions := []TracerProviderOption{WithSyncer(te), WithSampler(AlwaysSample())}
			tp := NewTracerProvider(append(defaultOptions, tc.options...)...)
			span := startSpan(tp, "WithResource")
			span.SetAttributes(attribute.String("key1", "value1"))
			got, err := endSpan(te, span)
			if err != nil {
				t.Error(err.Error())
			}
			want := &snapshot{
				spanContext: trace.NewSpanContext(trace.SpanContextConfig{
					TraceID:    tid,
					TraceFlags: 0x1,
				}),
				parent: sc.WithRemote(true),
				name:   "span0",
				attributes: []attribute.KeyValue{
					attribute.String("key1", "value1"),
				},
				spanKind:             trace.SpanKindInternal,
				resource:             tc.want,
				instrumentationScope: instrumentation.Scope{Name: "WithResource"},
			}
			if diff := cmpDiff(got, want); diff != "" {
				t.Errorf("WithResource:\n  -got +want %s", diff)
			}
		})
	}
}

func TestWithInstrumentationVersionAndSchema(t *testing.T) {
	te := NewTestExporter()
	tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty()))

	ctx := t.Context()
	ctx = trace.ContextWithRemoteSpanContext(ctx, sc)
	_, span := tp.Tracer(
		"WithInstrumentationVersion",
		trace.WithInstrumentationVersion("v0.1.0"),
		trace.WithSchemaURL("https://opentelemetry.io/schemas/1.21.0"),
	).Start(ctx, "span0")
	got, err := endSpan(te, span)
	if err != nil {
		t.Error(err.Error())
	}

	want := &snapshot{
		spanContext: trace.NewSpanContext(trace.SpanContextConfig{
			TraceID:    tid,
			TraceFlags: 0x1,
		}),
		parent:   sc.WithRemote(true),
		name:     "span0",
		spanKind: trace.SpanKindInternal,
		instrumentationScope: instrumentation.Scope{
			Name:      "WithInstrumentationVersion",
			Version:   "v0.1.0",
			SchemaURL: "https://opentelemetry.io/schemas/1.21.0",
		},
	}
	if diff := cmpDiff(got, want); diff != "" {
		t.Errorf("WithResource:\n  -got +want %s", diff)
	}
}

func TestSpanCapturesPanic(t *testing.T) {
	te := NewTestExporter()
	tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty()))
	_, span := tp.Tracer("CatchPanic").Start(
		t.Context(),
		"span",
	)

	f := func() {
		defer span.End()
		panic(errors.New("error message"))
	}
	require.PanicsWithError(t, "error message", f)
	spans := te.Spans()
	require.Len(t, spans, 1)
	require.Len(t, spans[0].Events(), 1)
	assert.Equal(t, semconv.ExceptionEventName, spans[0].Events()[0].Name)
	assert.Equal(t, []attribute.KeyValue{
		semconv.ExceptionType("*errors.errorString"),
		semconv.ExceptionMessage("error message"),
	}, spans[0].Events()[0].Attributes)
}

func TestSpanCapturesPanicWithStackTrace(t *testing.T) {
	te := NewTestExporter()
	tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty()))
	_, span := tp.Tracer("CatchPanic").Start(
		t.Context(),
		"span",
	)

	f := func() {
		defer span.End(trace.WithStackTrace(true))
		panic(errors.New("error message"))
	}
	require.PanicsWithError(t, "error message", f)
	spans := te.Spans()
	require.Len(t, spans, 1)
	require.Len(t, spans[0].Events(), 1)
	assert.Equal(t, semconv.ExceptionEventName, spans[0].Events()[0].Name)
	assert.Equal(t, "*errors.errorString", spans[0].Events()[0].Attributes[0].Value.AsString())
	assert.Equal(t, "error message", spans[0].Events()[0].Attributes[1].Value.AsString())

	gotStackTraceFunctionName := strings.Split(spans[0].Events()[0].Attributes[2].Value.AsString(), "\n")
	assert.Truef(
		t,
		strings.HasPrefix(gotStackTraceFunctionName[1], "go.opentelemetry.io/otel/sdk/trace.recordStackTrace"),
		"%q not prefixed with go.opentelemetry.io/otel/sdk/trace.recordStackTrace",
		gotStackTraceFunctionName[1],
	)
	assert.Truef(
		t,
		strings.HasPrefix(gotStackTraceFunctionName[3], "go.opentelemetry.io/otel/sdk/trace.(*recordingSpan).End"),
		"%q not prefixed with go.opentelemetry.io/otel/sdk/trace.(*recordingSpan).End",
		gotStackTraceFunctionName[3],
	)
}

func TestReadOnlySpan(t *testing.T) {
	kv := attribute.String("foo", "bar")

	tp := NewTracerProvider(WithResource(resource.NewSchemaless(kv)))
	tr := tp.Tracer("ReadOnlySpan", trace.WithInstrumentationVersion("3"))

	// Initialize parent context.
	tID, sID := tp.idGenerator.NewIDs(t.Context())
	parent := trace.NewSpanContext(trace.SpanContextConfig{
		TraceID:    tID,
		SpanID:     sID,
		TraceFlags: 0x1,
		Remote:     true,
	})
	ctx := trace.ContextWithRemoteSpanContext(t.Context(), parent)

	// Initialize linked context.
	tID, sID = tp.idGenerator.NewIDs(t.Context())
	linked := trace.NewSpanContext(trace.SpanContextConfig{
		TraceID:    tID,
		SpanID:     sID,
		TraceFlags: 0x1,
	})

	st := time.Now()
	ctx, s := tr.Start(ctx, "foo", trace.WithTimestamp(st),
		trace.WithLinks(trace.Link{SpanContext: linked}))
	s.SetAttributes(kv)
	s.AddEvent("foo", trace.WithAttributes(kv))
	s.SetStatus(codes.Ok, "foo")

	// Verify span implements ReadOnlySpan.
	ro, ok := s.(ReadOnlySpan)
	require.True(t, ok)

	assert.Equal(t, "foo", ro.Name())
	assert.Equal(t, trace.SpanContextFromContext(ctx), ro.SpanContext())
	assert.Equal(t, parent, ro.Parent())
	assert.Equal(t, trace.SpanKindInternal, ro.SpanKind())
	assert.Equal(t, st, ro.StartTime())
	assert.True(t, ro.EndTime().IsZero())
	assert.Equal(t, kv.Key, ro.Attributes()[0].Key)
	assert.Equal(t, kv.Value, ro.Attributes()[0].Value)
	assert.Equal(t, linked, ro.Links()[0].SpanContext)
	assert.Equal(t, kv.Key, ro.Events()[0].Attributes[0].Key)
	assert.Equal(t, kv.Value, ro.Events()[0].Attributes[0].Value)
	assert.Equal(t, codes.Ok, ro.Status().Code)
	assert.Empty(t, ro.Status().Description)
	assert.Equal(t, "ReadOnlySpan", ro.InstrumentationLibrary().Name)
	assert.Equal(t, "3", ro.InstrumentationLibrary().Version)
	assert.Equal(t, "ReadOnlySpan", ro.InstrumentationScope().Name)
	assert.Equal(t, "3", ro.InstrumentationScope().Version)
	assert.Equal(t, kv.Key, ro.Resource().Attributes()[0].Key)
	assert.Equal(t, kv.Value, ro.Resource().Attributes()[0].Value)

	// Verify changes to the original span are reflected in the ReadOnlySpan.
	s.SetName("bar")
	assert.Equal(t, "bar", ro.Name())

	// Verify snapshot() returns snapshots that are independent from the
	// original span and from one another.
	d1 := s.(*recordingSpan).snapshot()
	s.AddEvent("baz")
	d2 := s.(*recordingSpan).snapshot()
	for _, e := range d1.Events() {
		if e.Name == "baz" {
			t.Errorf("Didn't expect to find 'baz' event")
		}
	}
	var exists bool
	for _, e := range d2.Events() {
		if e.Name == "baz" {
			exists = true
		}
	}
	if !exists {
		t.Errorf("Expected to find 'baz' event")
	}

	et := st.Add(time.Millisecond)
	s.End(trace.WithTimestamp(et))
	assert.Equal(t, et, ro.EndTime())
}

func TestReadWriteSpan(t *testing.T) {
	tp := NewTracerProvider(WithResource(resource.Empty()))
	tr := tp.Tracer("ReadWriteSpan")

	// Initialize parent context.
	tID, sID := tp.idGenerator.NewIDs(t.Context())
	parent := trace.NewSpanContext(trace.SpanContextConfig{
		TraceID:    tID,
		SpanID:     sID,
		TraceFlags: 0x1,
	})
	ctx := trace.ContextWithRemoteSpanContext(t.Context(), parent)

	_, span := tr.Start(ctx, "foo")
	defer span.End()

	// Verify span implements ReadOnlySpan.
	rw, ok := span.(ReadWriteSpan)
	require.True(t, ok)

	// Verify the span can be read from.
	assert.False(t, rw.StartTime().IsZero())

	// Verify the span can be written to.
	rw.SetName("bar")
	assert.Equal(t, "bar", rw.Name())
	// NOTE: This function tests ReadWriteSpan which is an interface which
	// embeds trace.Span and ReadOnlySpan. Since both of these interfaces have
	// their own tests, there is no point in testing all the possible methods
	// available via ReadWriteSpan as doing so would mean creating a lot of
	// duplication.
}

func TestAddEventsWithMoreAttributesThanLimit(t *testing.T) {
	te := NewTestExporter()
	sl := NewSpanLimits()
	sl.AttributePerEventCountLimit = 2
	tp := NewTracerProvider(
		WithSpanLimits(sl),
		WithSyncer(te),
		WithResource(resource.Empty()),
	)

	span := startSpan(tp, "AddSpanEventWithOverLimitedAttributes")
	span.AddEvent("test1", trace.WithAttributes(
		attribute.Bool("key1", true),
		attribute.String("key2", "value2"),
	))
	// Parts of the attribute should be discard
	span.AddEvent("test2", trace.WithAttributes(
		attribute.Bool("key1", true),
		attribute.String("key2", "value2"),
		attribute.String("key3", "value3"),
		attribute.String("key4", "value4"),
	))
	got, err := endSpan(te, span)
	if err != nil {
		t.Fatal(err)
	}

	for i := range got.Events() {
		if !checkTime(&got.Events()[i].Time) {
			t.Error("exporting span: expected nonzero Event Time")
		}
	}

	want := &snapshot{
		spanContext: trace.NewSpanContext(trace.SpanContextConfig{
			TraceID:    tid,
			TraceFlags: 0x1,
		}),
		parent:     sc.WithRemote(true),
		name:       "span0",
		attributes: nil,
		events: []Event{
			{
				Name: "test1",
				Attributes: []attribute.KeyValue{
					attribute.Bool("key1", true),
					attribute.String("key2", "value2"),
				},
			},
			{
				Name: "test2",
				Attributes: []attribute.KeyValue{
					attribute.Bool("key1", true),
					attribute.String("key2", "value2"),
				},
				DroppedAttributeCount: 2,
			},
		},
		spanKind:             trace.SpanKindInternal,
		instrumentationScope: instrumentation.Scope{Name: "AddSpanEventWithOverLimitedAttributes"},
	}
	if diff := cmpDiff(got, want); diff != "" {
		t.Errorf("SetSpanAttributesOverLimit: -got +want %s", diff)
	}
}

type stateSampler struct {
	prefix string
	f      func(trace.TraceState) trace.TraceState
}

func (s *stateSampler) ShouldSample(p SamplingParameters) SamplingResult {
	decision := Drop
	if strings.HasPrefix(p.Name, s.prefix) {
		decision = RecordAndSample
	}
	ts := s.f(trace.SpanContextFromContext(p.ParentContext).TraceState())
	return SamplingResult{Decision: decision, Tracestate: ts}
}

func (stateSampler) Description() string {
	return "stateSampler"
}

// Check that a new span propagates the SamplerResult.TraceState.
func TestSamplerTraceState(t *testing.T) {
	mustTS := func(ts trace.TraceState, err error) trace.TraceState {
		require.NoError(t, err)
		return ts
	}
	makeInserter := func(k, v, prefix string) Sampler {
		return &stateSampler{
			prefix: prefix,
			f:      func(t trace.TraceState) trace.TraceState { return mustTS(t.Insert(k, v)) },
		}
	}
	makeDeleter := func(k, prefix string) Sampler {
		return &stateSampler{
			prefix: prefix,
			f:      func(t trace.TraceState) trace.TraceState { return t.Delete(k) },
		}
	}
	clearer := func(prefix string) Sampler {
		return &stateSampler{
			prefix: prefix,
			f:      func(trace.TraceState) trace.TraceState { return trace.TraceState{} },
		}
	}

	tests := []struct {
		name       string
		sampler    Sampler
		spanName   string
		input      trace.TraceState
		want       trace.TraceState
		exportSpan bool
	}{
		{
			name:       "alwaysOn",
			sampler:    AlwaysSample(),
			input:      mustTS(trace.ParseTraceState("k1=v1")),
			want:       mustTS(trace.ParseTraceState("k1=v1")),
			exportSpan: true,
		},
		{
			name:       "alwaysOff",
			sampler:    NeverSample(),
			input:      mustTS(trace.ParseTraceState("k1=v1")),
			want:       mustTS(trace.ParseTraceState("k1=v1")),
			exportSpan: false,
		},
		{
			name:       "insertKeySampled",
			sampler:    makeInserter("k2", "v2", "span"),
			spanName:   "span0",
			input:      mustTS(trace.ParseTraceState("k1=v1")),
			want:       mustTS(trace.ParseTraceState("k2=v2,k1=v1")),
			exportSpan: true,
		},
		{
			name:       "insertKeyDropped",
			sampler:    makeInserter("k2", "v2", "span"),
			spanName:   "nospan0",
			input:      mustTS(trace.ParseTraceState("k1=v1")),
			want:       mustTS(trace.ParseTraceState("k2=v2,k1=v1")),
			exportSpan: false,
		},
		{
			name:       "deleteKeySampled",
			sampler:    makeDeleter("k1", "span"),
			spanName:   "span0",
			input:      mustTS(trace.ParseTraceState("k1=v1,k2=v2")),
			want:       mustTS(trace.ParseTraceState("k2=v2")),
			exportSpan: true,
		},
		{
			name:       "deleteKeyDropped",
			sampler:    makeDeleter("k1", "span"),
			spanName:   "nospan0",
			input:      mustTS(trace.ParseTraceState("k1=v1,k2=v2,k3=v3")),
			want:       mustTS(trace.ParseTraceState("k2=v2,k3=v3")),
			exportSpan: false,
		},
		{
			name:       "clearer",
			sampler:    clearer("span"),
			spanName:   "span0",
			input:      mustTS(trace.ParseTraceState("k1=v1,k3=v3")),
			want:       mustTS(trace.ParseTraceState("")),
			exportSpan: true,
		},
	}

	for _, ts := range tests {
		t.Run(ts.name, func(t *testing.T) {
			te := NewTestExporter()
			tp := NewTracerProvider(WithSampler(ts.sampler), WithSyncer(te), WithResource(resource.Empty()))
			tr := tp.Tracer("TraceState")

			sc1 := trace.NewSpanContext(trace.SpanContextConfig{
				TraceID:    tid,
				SpanID:     sid,
				TraceFlags: trace.FlagsSampled,
				TraceState: ts.input,
			})
			ctx := trace.ContextWithRemoteSpanContext(t.Context(), sc1)
			_, span := tr.Start(ctx, ts.spanName)

			// span's TraceState should be set regardless of Sampled/NonSampled state.
			require.Equal(t, ts.want, span.SpanContext().TraceState())

			span.End()

			got := te.Spans()
			if len(got) > 0 != ts.exportSpan {
				t.Errorf("unexpected number of exported spans %d", len(got))
			}
			if len(got) == 0 {
				return
			}

			receivedState := got[0].SpanContext().TraceState()

			if diff := cmpDiff(receivedState, ts.want); diff != "" {
				t.Errorf("TraceState not propagated: -got +want %s", diff)
			}
		})
	}
}

type testIDGenerator struct {
	traceIDHigh uint64
	traceIDLow  uint64
	spanID      uint64
}

func (gen *testIDGenerator) NewIDs(ctx context.Context) (trace.TraceID, trace.SpanID) {
	traceIDHex := fmt.Sprintf("%016x%016x", gen.traceIDHigh, gen.traceIDLow)
	traceID, _ := trace.TraceIDFromHex(traceIDHex)
	gen.traceIDLow++

	spanID := gen.NewSpanID(ctx, traceID)
	return traceID, spanID
}

func (gen *testIDGenerator) NewSpanID(context.Context, trace.TraceID) trace.SpanID {
	spanIDHex := fmt.Sprintf("%016x", gen.spanID)
	spanID, _ := trace.SpanIDFromHex(spanIDHex)
	gen.spanID++
	return spanID
}

var _ IDGenerator = (*testIDGenerator)(nil)

func TestWithIDGenerator(t *testing.T) {
	const (
		startTraceIDHigh uint64 = 0x1001_1001_1001_1001
		startTraceIDLow  uint64 = 0x2002_2002_2002_2002
		startSpanID      uint64 = 0x3003_3003_3003_3003
		numSpan                 = 5
	)

	gen := &testIDGenerator{traceIDHigh: startTraceIDHigh, traceIDLow: startTraceIDLow, spanID: startSpanID}
	te := NewTestExporter()
	tp := NewTracerProvider(
		WithSyncer(te),
		WithIDGenerator(gen),
	)
	for i := range numSpan {
		func() {
			_, span := tp.Tracer(t.Name()).Start(t.Context(), strconv.Itoa(i))
			defer span.End()

			gotSpanID, err := strconv.ParseUint(span.SpanContext().SpanID().String(), 16, 64)
			require.NoError(t, err)
			assert.Equal(t, startSpanID+uint64(i), gotSpanID)

			traceIdStr := span.SpanContext().TraceID().String()
			highBitsStr := traceIdStr[:16]
			lowBitsStr := traceIdStr[16:]

			traceIdValidator := func(t *testing.T, id string, expected uint64) {
				gotTraceID, err := strconv.ParseUint(id, 16, 64)
				require.NoError(t, err)
				assert.Equal(t, expected, gotTraceID)
			}
			traceIdValidator(t, highBitsStr, startTraceIDHigh)
			traceIdValidator(t, lowBitsStr, startTraceIDLow+uint64(i))
		}()
	}
}

func TestIDsRoundTrip(t *testing.T) {
	gen := defaultIDGenerator()

	for range 1000 {
		traceID, spanID := gen.NewIDs(t.Context())
		gotTraceID, err := trace.TraceIDFromHex(traceID.String())
		assert.NoError(t, err)
		assert.Equal(t, traceID, gotTraceID)
		gotSpanID, err := trace.SpanIDFromHex(spanID.String())
		assert.NoError(t, err)
		assert.Equal(t, spanID, gotSpanID)
	}
}

func TestIDConversionErrors(t *testing.T) {
	for _, tt := range []struct {
		name         string
		spanIDStr    string
		traceIDStr   string
		spanIDError  string
		traceIDError string
	}{
		{
			name:         "slightly too long",
			spanIDStr:    sid.String() + "0",
			spanIDError:  "hex encoded span-id must have length equals to 16",
			traceIDStr:   tid.String() + "0",
			traceIDError: "hex encoded trace-id must have length equals to 32",
		},
		{
			name:         "blank input",
			spanIDStr:    "",
			spanIDError:  "hex encoded span-id must have length equals to 16",
			traceIDStr:   "",
			traceIDError: "hex encoded trace-id must have length equals to 32",
		},
		{
			name:         "not hex",
			spanIDStr:    "unacceptablechar",
			spanIDError:  "trace-id and span-id can only contain [0-9a-f] characters, all lowercase",
			traceIDStr:   "completely unacceptablecharacter",
			traceIDError: "trace-id and span-id can only contain [0-9a-f] characters, all lowercase",
		},
		{
			name:         "upper-case hex",
			spanIDStr:    "DEADBEEFBAD0CAFE",
			spanIDError:  "trace-id and span-id can only contain [0-9a-f] characters, all lowercase",
			traceIDStr:   "DEADBEEFBAD0CAFEDEADBEEFBAD0CAFE",
			traceIDError: "trace-id and span-id can only contain [0-9a-f] characters, all lowercase",
		},
		{
			name:         "all zero",
			spanIDStr:    "0000000000000000",
			spanIDError:  "span-id can't be all zero",
			traceIDStr:   "00000000000000000000000000000000",
			traceIDError: "trace-id can't be all zero",
		},
	} {
		t.Run(tt.name, func(t *testing.T) {
			_, err := trace.SpanIDFromHex(tt.spanIDStr)
			assert.ErrorContains(t, err, tt.spanIDError)
			_, err = trace.TraceIDFromHex(tt.traceIDStr)
			assert.ErrorContains(t, err, tt.traceIDError)
		})
	}
}

func TestEmptyRecordingSpanAttributes(t *testing.T) {
	assert.Nil(t, (&recordingSpan{}).Attributes())
}

func TestEmptyRecordingSpanDroppedAttributes(t *testing.T) {
	assert.Equal(t, 0, (&recordingSpan{}).DroppedAttributes())
}

func TestSpanAddLink(t *testing.T) {
	tests := []struct {
		name               string
		attrLinkCountLimit int
		link               trace.Link
		want               *snapshot
	}{
		{
			name:               "AddLinkWithInvalidSpanContext",
			attrLinkCountLimit: 128,
			link: trace.Link{
				SpanContext: trace.NewSpanContext(
					trace.SpanContextConfig{TraceID: trace.TraceID([16]byte{}), SpanID: [8]byte{}},
				),
			},
			want: &snapshot{
				name: "span0",
				spanContext: trace.NewSpanContext(trace.SpanContextConfig{
					TraceID:    tid,
					TraceFlags: 0x1,
				}),
				parent:               sc.WithRemote(true),
				links:                nil,
				spanKind:             trace.SpanKindInternal,
				instrumentationScope: instrumentation.Scope{Name: "AddLinkWithInvalidSpanContext"},
			},
		},
		{
			name:               "AddLink",
			attrLinkCountLimit: 128,
			link: trace.Link{
				SpanContext: sc,
				Attributes:  []attribute.KeyValue{{Key: "k1", Value: attribute.StringValue("v1")}},
			},
			want: &snapshot{
				name: "span0",
				spanContext: trace.NewSpanContext(trace.SpanContextConfig{
					TraceID:    tid,
					TraceFlags: 0x1,
				}),
				parent: sc.WithRemote(true),
				links: []Link{
					{
						SpanContext: sc,
						Attributes:  []attribute.KeyValue{{Key: "k1", Value: attribute.StringValue("v1")}},
					},
				},
				spanKind:             trace.SpanKindInternal,
				instrumentationScope: instrumentation.Scope{Name: "AddLink"},
			},
		},
		{
			name:               "AddLinkWithMoreAttributesThanLimit",
			attrLinkCountLimit: 1,
			link: trace.Link{
				SpanContext: sc,
				Attributes: []attribute.KeyValue{
					{Key: "k1", Value: attribute.StringValue("v1")},
					{Key: "k2", Value: attribute.StringValue("v2")},
					{Key: "k3", Value: attribute.StringValue("v3")},
					{Key: "k4", Value: attribute.StringValue("v4")},
				},
			},
			want: &snapshot{
				name: "span0",
				spanContext: trace.NewSpanContext(trace.SpanContextConfig{
					TraceID:    tid,
					TraceFlags: 0x1,
				}),
				parent: sc.WithRemote(true),
				links: []Link{
					{
						SpanContext:           sc,
						Attributes:            []attribute.KeyValue{{Key: "k1", Value: attribute.StringValue("v1")}},
						DroppedAttributeCount: 3,
					},
				},
				spanKind:             trace.SpanKindInternal,
				instrumentationScope: instrumentation.Scope{Name: "AddLinkWithMoreAttributesThanLimit"},
			},
		},
		{
			name:               "AddLinkWithAttributesEmptySpanContext",
			attrLinkCountLimit: 128,
			link: trace.Link{
				Attributes: []attribute.KeyValue{{Key: "k1", Value: attribute.StringValue("v1")}},
			},
			want: &snapshot{
				name: "span0",
				spanContext: trace.NewSpanContext(trace.SpanContextConfig{
					TraceID:    tid,
					TraceFlags: 0x1,
				}),
				parent: sc.WithRemote(true),
				links: []Link{
					{
						Attributes: []attribute.KeyValue{{Key: "k1", Value: attribute.StringValue("v1")}},
					},
				},
				spanKind:             trace.SpanKindInternal,
				instrumentationScope: instrumentation.Scope{Name: "AddLinkWithAttributesEmptySpanContext"},
			},
		},
		{
			name:               "AddLinkWithTraceStateEmptySpanContext",
			attrLinkCountLimit: 128,
			link: trace.Link{
				SpanContext: trace.SpanContext{}.WithTraceState(ts),
			},
			want: &snapshot{
				name: "span0",
				spanContext: trace.NewSpanContext(trace.SpanContextConfig{
					TraceID:    tid,
					TraceFlags: 0x1,
				}),
				parent: sc.WithRemote(true),
				links: []Link{
					{
						SpanContext: trace.SpanContext{}.WithTraceState(ts),
					},
				},
				spanKind:             trace.SpanKindInternal,
				instrumentationScope: instrumentation.Scope{Name: "AddLinkWithTraceStateEmptySpanContext"},
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			te := NewTestExporter()
			sl := NewSpanLimits()
			sl.AttributePerLinkCountLimit = tc.attrLinkCountLimit

			tp := NewTracerProvider(WithSpanLimits(sl), WithSyncer(te), WithResource(resource.Empty()))

			span := startSpan(tp, tc.name)
			span.AddLink(tc.link)

			got, err := endSpan(te, span)
			if err != nil {
				t.Fatal(err)
			}

			if diff := cmpDiff(got, tc.want); diff != "" {
				t.Errorf("-got +want %s", diff)
			}
		})
	}
}

func TestAddLinkToNonRecordingSpan(t *testing.T) {
	te := NewTestExporter()
	sl := NewSpanLimits()
	tp := NewTracerProvider(
		WithSpanLimits(sl),
		WithSyncer(te),
		WithResource(resource.Empty()),
	)

	attrs := []attribute.KeyValue{{Key: "k", Value: attribute.StringValue("v")}}

	span := startSpan(tp, "AddLinkToNonRecordingSpan")
	_, err := endSpan(te, span)
	require.NoError(t, err)

	// Add link to ended, non-recording, span. The link should be dropped.
	span.AddLink(trace.Link{
		SpanContext: sc,
		Attributes:  attrs,
	})

	require.Equal(t, 1, te.Len())
	got := te.Spans()[0]
	want := &snapshot{
		name: "span0",
		spanContext: trace.NewSpanContext(trace.SpanContextConfig{
			TraceID:    tid,
			TraceFlags: 0x1,
		}),
		parent:               sc.WithRemote(true),
		links:                nil,
		spanKind:             trace.SpanKindInternal,
		instrumentationScope: instrumentation.Scope{Name: "AddLinkToNonRecordingSpan"},
	}

	if diff := cmpDiff(got, want); diff != "" {
		t.Errorf("AddLinkToNonRecordingSpan: -got +want %s", diff)
	}
}

func TestObservability(t *testing.T) {
	testCases := []struct {
		name string
		test func(t *testing.T, scopeMetrics func() metricdata.ScopeMetrics)
	}{
		{
			name: "SampledSpan",
			test: func(t *testing.T, scopeMetrics func() metricdata.ScopeMetrics) {
				tp := NewTracerProvider()
				_, span := tp.Tracer("").Start(t.Context(), "StartSpan")

				want := metricdata.ScopeMetrics{
					Scope: instrumentation.Scope{
						Name:      observ.ScopeName,
						Version:   sdk.Version(),
						SchemaURL: observ.SchemaURL,
					},
					Metrics: []metricdata.Metrics{
						{
							Name:        otelconv.SDKSpanLive{}.Name(),
							Description: otelconv.SDKSpanLive{}.Description(),
							Unit:        otelconv.SDKSpanLive{}.Unit(),
							Data: metricdata.Sum[int64]{
								Temporality: metricdata.CumulativeTemporality,
								DataPoints: []metricdata.DataPoint[int64]{
									{
										Attributes: attribute.NewSet(
											otelconv.SDKSpanLive{}.AttrSpanSamplingResult(
												otelconv.SpanSamplingResultRecordAndSample,
											),
										),
										Value: 1,
									},
								},
							},
						},
						{
							Name:        otelconv.SDKSpanStarted{}.Name(),
							Description: otelconv.SDKSpanStarted{}.Description(),
							Unit:        otelconv.SDKSpanStarted{}.Unit(),
							Data: metricdata.Sum[int64]{
								Temporality: metricdata.CumulativeTemporality,
								IsMonotonic: true,
								DataPoints: []metricdata.DataPoint[int64]{
									{
										Attributes: attribute.NewSet(
											otelconv.SDKSpanStarted{}.AttrSpanParentOrigin(
												otelconv.SpanParentOriginNone,
											),
											otelconv.SDKSpanStarted{}.AttrSpanSamplingResult(
												otelconv.SpanSamplingResultRecordAndSample,
											),
										),
										Value: 1,
									},
								},
							},
						},
					},
				}

				got := scopeMetrics()
				metricdatatest.AssertEqual(
					t,
					want,
					got,
					metricdatatest.IgnoreTimestamp(),
					metricdatatest.IgnoreExemplars(),
				)

				span.End()

				want = metricdata.ScopeMetrics{
					Scope: instrumentation.Scope{
						Name:      observ.ScopeName,
						Version:   sdk.Version(),
						SchemaURL: observ.SchemaURL,
					},
					Metrics: []metricdata.Metrics{
						{
							Name:        otelconv.SDKSpanLive{}.Name(),
							Description: otelconv.SDKSpanLive{}.Description(),
							Unit:        otelconv.SDKSpanLive{}.Unit(),
							Data: metricdata.Sum[int64]{
								Temporality: metricdata.CumulativeTemporality,
								DataPoints: []metricdata.DataPoint[int64]{
									{
										Attributes: attribute.NewSet(
											otelconv.SDKSpanLive{}.AttrSpanSamplingResult(
												otelconv.SpanSamplingResultRecordAndSample,
											),
										),
										Value: 0, // No live spans at this point.
									},
								},
							},
						},
						{
							Name:        otelconv.SDKSpanStarted{}.Name(),
							Description: otelconv.SDKSpanStarted{}.Description(),
							Unit:        otelconv.SDKSpanStarted{}.Unit(),
							Data: metricdata.Sum[int64]{
								Temporality: metricdata.CumulativeTemporality,
								IsMonotonic: true,
								DataPoints: []metricdata.DataPoint[int64]{
									{
										Attributes: attribute.NewSet(
											otelconv.SDKSpanStarted{}.AttrSpanParentOrigin(
												otelconv.SpanParentOriginNone,
											),
											otelconv.SDKSpanStarted{}.AttrSpanSamplingResult(
												otelconv.SpanSamplingResultRecordAndSample,
											),
										),
										Value: 1,
									},
								},
							},
						},
					},
				}
				got = scopeMetrics()
				metricdatatest.AssertEqual(
					t,
					want,
					got,
					metricdatatest.IgnoreTimestamp(),
					metricdatatest.IgnoreExemplars(),
				)
			},
		},
		{
			name: "NonRecordingSpan",
			test: func(t *testing.T, scopeMetrics func() metricdata.ScopeMetrics) {
				// Create a tracer provider with NeverSample sampler to get non-recording spans.
				tp := NewTracerProvider(WithSampler(NeverSample()))
				tp.Tracer("").Start(t.Context(), "NonRecordingSpan")

				want := metricdata.ScopeMetrics{
					Scope: instrumentation.Scope{
						Name:      observ.ScopeName,
						Version:   sdk.Version(),
						SchemaURL: observ.SchemaURL,
					},
					Metrics: []metricdata.Metrics{
						{
							Name:        otelconv.SDKSpanStarted{}.Name(),
							Description: otelconv.SDKSpanStarted{}.Description(),
							Unit:        otelconv.SDKSpanStarted{}.Unit(),
							Data: metricdata.Sum[int64]{
								Temporality: metricdata.CumulativeTemporality,
								IsMonotonic: true,
								DataPoints: []metricdata.DataPoint[int64]{
									{
										Attributes: attribute.NewSet(
											otelconv.SDKSpanStarted{}.AttrSpanParentOrigin(
												otelconv.SpanParentOriginNone,
											),
											otelconv.SDKSpanStarted{}.AttrSpanSamplingResult(
												otelconv.SpanSamplingResultDrop,
											),
										),
										Value: 1,
									},
								},
							},
						},
					},
				}

				got := scopeMetrics()
				metricdatatest.AssertEqual(
					t,
					want,
					got,
					metricdatatest.IgnoreTimestamp(),
					metricdatatest.IgnoreExemplars(),
				)
			},
		},
		{
			name: "OnlyRecordingSpan",
			test: func(t *testing.T, scopeMetrics func() metricdata.ScopeMetrics) {
				// Create a tracer provider with NeverSample sampler to get non-recording spans.
				tp := NewTracerProvider(WithSampler(RecordingOnly()))
				tp.Tracer("").Start(t.Context(), "OnlyRecordingSpan")

				want := metricdata.ScopeMetrics{
					Scope: instrumentation.Scope{
						Name:      observ.ScopeName,
						Version:   sdk.Version(),
						SchemaURL: observ.SchemaURL,
					},
					Metrics: []metricdata.Metrics{
						{
							Name:        otelconv.SDKSpanLive{}.Name(),
							Description: otelconv.SDKSpanLive{}.Description(),
							Unit:        otelconv.SDKSpanLive{}.Unit(),
							Data: metricdata.Sum[int64]{
								Temporality: metricdata.CumulativeTemporality,
								DataPoints: []metricdata.DataPoint[int64]{
									{
										Attributes: attribute.NewSet(
											otelconv.SDKSpanLive{}.AttrSpanSamplingResult(
												otelconv.SpanSamplingResultRecordOnly,
											),
										),
										Value: 1,
									},
								},
							},
						},
						{
							Name:        otelconv.SDKSpanStarted{}.Name(),
							Description: otelconv.SDKSpanStarted{}.Description(),
							Unit:        otelconv.SDKSpanStarted{}.Unit(),
							Data: metricdata.Sum[int64]{
								Temporality: metricdata.CumulativeTemporality,
								IsMonotonic: true,
								DataPoints: []metricdata.DataPoint[int64]{
									{
										Attributes: attribute.NewSet(
											otelconv.SDKSpanStarted{}.AttrSpanParentOrigin(
												otelconv.SpanParentOriginNone,
											),
											otelconv.SDKSpanStarted{}.AttrSpanSamplingResult(
												otelconv.SpanSamplingResultRecordOnly,
											),
										),
										Value: 1,
									},
								},
							},
						},
					},
				}

				got := scopeMetrics()
				metricdatatest.AssertEqual(
					t,
					want,
					got,
					metricdatatest.IgnoreTimestamp(),
					metricdatatest.IgnoreExemplars(),
				)
			},
		},
		{
			name: "RemoteParentSpan",
			test: func(t *testing.T, scopeMetrics func() metricdata.ScopeMetrics) {
				// Create a remote parent context
				tid, _ := trace.TraceIDFromHex("01020304050607080102040810203040")
				sid, _ := trace.SpanIDFromHex("0102040810203040")
				remoteCtx := trace.ContextWithRemoteSpanContext(t.Context(),
					trace.NewSpanContext(trace.SpanContextConfig{
						TraceID:    tid,
						SpanID:     sid,
						TraceFlags: 0x1,
						Remote:     true,
					}))

				tp := NewTracerProvider()
				tp.Tracer("").Start(remoteCtx, "ChildSpan")

				want := metricdata.ScopeMetrics{
					Scope: instrumentation.Scope{
						Name:      observ.ScopeName,
						Version:   sdk.Version(),
						SchemaURL: observ.SchemaURL,
					},
					Metrics: []metricdata.Metrics{
						{
							Name:        otelconv.SDKSpanLive{}.Name(),
							Description: otelconv.SDKSpanLive{}.Description(),
							Unit:        otelconv.SDKSpanLive{}.Unit(),
							Data: metricdata.Sum[int64]{
								Temporality: metricdata.CumulativeTemporality,
								DataPoints: []metricdata.DataPoint[int64]{
									{
										Attributes: attribute.NewSet(
											otelconv.SDKSpanLive{}.AttrSpanSamplingResult(
												otelconv.SpanSamplingResultRecordAndSample,
											),
										),
										Value: 1,
									},
								},
							},
						},
						{
							Name:        otelconv.SDKSpanStarted{}.Name(),
							Description: otelconv.SDKSpanStarted{}.Description(),
							Unit:        otelconv.SDKSpanStarted{}.Unit(),
							Data: metricdata.Sum[int64]{
								Temporality: metricdata.CumulativeTemporality,
								IsMonotonic: true,
								DataPoints: []metricdata.DataPoint[int64]{
									{
										Attributes: attribute.NewSet(
											otelconv.SDKSpanStarted{}.AttrSpanParentOrigin(
												otelconv.SpanParentOriginRemote,
											),
											otelconv.SDKSpanStarted{}.AttrSpanSamplingResult(
												otelconv.SpanSamplingResultRecordAndSample,
											),
										),
										Value: 1,
									},
								},
							},
						},
					},
				}
				got := scopeMetrics()
				metricdatatest.AssertEqual(
					t,
					want,
					got,
					metricdatatest.IgnoreTimestamp(),
					metricdatatest.IgnoreExemplars(),
				)
			},
		},
		{
			name: "LocalParentSpan",
			test: func(t *testing.T, scopeMetrics func() metricdata.ScopeMetrics) {
				tp := NewTracerProvider()
				ctx, parentSpan := tp.Tracer("").Start(t.Context(), "ParentSpan")
				_, childSpan := tp.Tracer("").Start(ctx, "ChildSpan")

				want := metricdata.ScopeMetrics{
					Scope: instrumentation.Scope{
						Name:      observ.ScopeName,
						Version:   sdk.Version(),
						SchemaURL: observ.SchemaURL,
					},
					Metrics: []metricdata.Metrics{
						{
							Name:        otelconv.SDKSpanLive{}.Name(),
							Description: otelconv.SDKSpanLive{}.Description(),
							Unit:        otelconv.SDKSpanLive{}.Unit(),
							Data: metricdata.Sum[int64]{
								Temporality: metricdata.CumulativeTemporality,
								DataPoints: []metricdata.DataPoint[int64]{
									{
										Attributes: attribute.NewSet(
											otelconv.SDKSpanLive{}.AttrSpanSamplingResult(
												otelconv.SpanSamplingResultRecordAndSample,
											),
										),
										Value: 2, // Both parent and child spans are active.
									},
								},
							},
						},
						{
							Name:        otelconv.SDKSpanStarted{}.Name(),
							Description: otelconv.SDKSpanStarted{}.Description(),
							Unit:        otelconv.SDKSpanStarted{}.Unit(),
							Data: metricdata.Sum[int64]{
								Temporality: metricdata.CumulativeTemporality,
								IsMonotonic: true,
								DataPoints: []metricdata.DataPoint[int64]{
									{
										Attributes: attribute.NewSet(
											otelconv.SDKSpanStarted{}.AttrSpanParentOrigin(
												otelconv.SpanParentOriginNone,
											),
											otelconv.SDKSpanStarted{}.AttrSpanSamplingResult(
												otelconv.SpanSamplingResultRecordAndSample,
											),
										),
										Value: 1, // Parent span with no parent of its own.
									},
									{
										Attributes: attribute.NewSet(
											otelconv.SDKSpanStarted{}.AttrSpanParentOrigin(
												otelconv.SpanParentOriginLocal,
											),
											otelconv.SDKSpanStarted{}.AttrSpanSamplingResult(
												otelconv.SpanSamplingResultRecordAndSample,
											),
										),
										Value: 1, // Child span with local parent.
									},
								},
							},
						},
					},
				}

				got := scopeMetrics()
				metricdatatest.AssertEqual(
					t,
					want,
					got,
					metricdatatest.IgnoreTimestamp(),
					metricdatatest.IgnoreExemplars(),
				)

				childSpan.End()
				parentSpan.End()

				want = metricdata.ScopeMetrics{
					Scope: instrumentation.Scope{
						Name:      observ.ScopeName,
						Version:   sdk.Version(),
						SchemaURL: observ.SchemaURL,
					},
					Metrics: []metricdata.Metrics{
						{
							Name:        otelconv.SDKSpanLive{}.Name(),
							Description: otelconv.SDKSpanLive{}.Description(),
							Unit:        otelconv.SDKSpanLive{}.Unit(),
							Data: metricdata.Sum[int64]{
								Temporality: metricdata.CumulativeTemporality,
								DataPoints: []metricdata.DataPoint[int64]{
									{
										Attributes: attribute.NewSet(
											otelconv.SDKSpanLive{}.AttrSpanSamplingResult(
												otelconv.SpanSamplingResultRecordAndSample,
											),
										),
										Value: 0, // No live spans after ending both.
									},
								},
							},
						},
						{
							Name:        otelconv.SDKSpanStarted{}.Name(),
							Description: otelconv.SDKSpanStarted{}.Description(),
							Unit:        otelconv.SDKSpanStarted{}.Unit(),
							Data: metricdata.Sum[int64]{
								Temporality: metricdata.CumulativeTemporality,
								IsMonotonic: true,
								DataPoints: []metricdata.DataPoint[int64]{
									{
										Attributes: attribute.NewSet(
											otelconv.SDKSpanStarted{}.AttrSpanParentOrigin(
												otelconv.SpanParentOriginNone,
											),
											otelconv.SDKSpanStarted{}.AttrSpanSamplingResult(
												otelconv.SpanSamplingResultRecordAndSample,
											),
										),
										Value: 1,
									},
									{
										Attributes: attribute.NewSet(
											otelconv.SDKSpanStarted{}.AttrSpanParentOrigin(
												otelconv.SpanParentOriginLocal,
											),
											otelconv.SDKSpanStarted{}.AttrSpanSamplingResult(
												otelconv.SpanSamplingResultRecordAndSample,
											),
										),
										Value: 1,
									},
								},
							},
						},
					},
				}

				got = scopeMetrics()
				metricdatatest.AssertEqual(
					t,
					want,
					got,
					metricdatatest.IgnoreTimestamp(),
					metricdatatest.IgnoreExemplars(),
				)
			},
		},
	}
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			t.Setenv("OTEL_GO_X_OBSERVABILITY", "True")
			prev := otel.GetMeterProvider()
			t.Cleanup(func() { otel.SetMeterProvider(prev) })

			r := metric.NewManualReader()
			mp := metric.NewMeterProvider(metric.WithReader(r))
			otel.SetMeterProvider(mp)

			scopeMetrics := func() metricdata.ScopeMetrics {
				var got metricdata.ResourceMetrics
				err := r.Collect(t.Context(), &got)
				require.NoError(t, err)
				require.Len(t, got.ScopeMetrics, 1)
				return got.ScopeMetrics[0]
			}
			tc.test(t, scopeMetrics)
		})
	}
}

// ctxKeyT is a custom context value type used for testing context propagation.
type ctxKeyT string

// ctxKey is a context key used to store and retrieve values in the context.
var ctxKey = ctxKeyT("testKey")

func TestObservabilityContextPropagation(t *testing.T) {
	t.Setenv("OTEL_GO_X_OBSERVABILITY", "True")
	prev := otel.GetMeterProvider()
	t.Cleanup(func() { otel.SetMeterProvider(prev) })

	// Approximate number of expected measuresments. This is not a strict
	// requirement, but it should be enough to ensure no backpressure.
	const count = 3 * 2 // 3 measurements per span, 2 spans (parent and child).
	ctxCh, fltr := filterFn(count)

	const want = "testValue"
	n := make(chan int)
	go func() {
		// Validate the span context is propagated to all measurements by
		// testing the context passed to the registered exemplar filter. This
		// filter receives the measurement context in the standard metric SDK
		// that we have registered.

		// Count of how many contexts were received.
		var count int

		for ctx := range ctxCh {
			count++

			s := trace.SpanFromContext(ctx)

			// All spans should have a valid span context. This should be
			// passed to the measurements in all cases.
			isValid := s.SpanContext().IsValid()
			assert.True(t, isValid, "Context should have a valid span")

			got := ctx.Value(ctxKey)
			assert.Equal(t, want, got, "Context value not propagated")
		}
		n <- count
	}()

	// At least one reader is required to not get a no-op MeterProvider and
	// short-circuit any instrumentation measurements.
	r := metric.NewManualReader()
	mp := metric.NewMeterProvider(
		metric.WithExemplarFilter(fltr),
		metric.WithReader(r),
	)
	otel.SetMeterProvider(mp)

	tp := NewTracerProvider()

	wrap := func(parentCtx context.Context, name string, fn func(context.Context)) {
		const tracer = "TestObservabilityContextPropagation"
		ctx, s := tp.Tracer(tracer).Start(parentCtx, name)
		defer s.End()
		fn(ctx)
	}

	ctx := context.WithValue(t.Context(), ctxKey, want)
	wrap(ctx, "parent", func(ctx context.Context) {
		wrap(ctx, "child", func(context.Context) {})
	})

	require.NoError(t, tp.Shutdown(t.Context()))

	// The TracerProvider shutdown returned, no more measurements will be sent
	// to the exemplar filter.
	close(ctxCh)

	assert.Positive(t, <-n, "Expected at least 1 context propagations")
}

// filterFn returns a channel that receives contexts passed to the returned
// exemplar filter function.
func filterFn(n int) (chan context.Context, func(ctx context.Context) bool) {
	out := make(chan context.Context, n)
	return out, func(ctx context.Context) bool {
		out <- ctx
		return true
	}
}

// RecordingOnly creates a Sampler that samples no traces, but enables recording.
// The created sampler maintains any tracestate from the parent span context.
func RecordingOnly() Sampler {
	return recordOnlySampler{}
}

type recordOnlySampler struct{}

// ShouldSample implements Sampler interface. It always returns Record but not Sample.
func (recordOnlySampler) ShouldSample(p SamplingParameters) SamplingResult {
	psc := trace.SpanContextFromContext(p.ParentContext)
	return SamplingResult{
		Decision:   RecordOnly,
		Tracestate: psc.TraceState(),
	}
}

// Description returns description of the sampler.
func (recordOnlySampler) Description() string {
	return "RecordingOnly"
}

func TestRecordOnlySampler(t *testing.T) {
	te := NewTestExporter()
	tp := NewTracerProvider(WithSyncer(te), WithSampler(RecordingOnly()))

	_, span := tp.Tracer("RecordOnly").Start(t.Context(), "test-span")

	assert.True(t, span.IsRecording(), "span should be recording")
	assert.False(t, span.SpanContext().IsSampled(), "span should not be sampled")

	span.End()

	assert.Zero(t, te.Len(), "no spans should be exported")
}

func BenchmarkTraceStart(b *testing.B) {
	ctx := trace.ContextWithSpanContext(b.Context(), trace.SpanContext{})

	l1 := trace.Link{SpanContext: trace.SpanContext{}, Attributes: []attribute.KeyValue{}}
	l2 := trace.Link{SpanContext: trace.SpanContext{}, Attributes: []attribute.KeyValue{}}

	links := []trace.Link{l1, l2}

	for _, tt := range []struct {
		name    string
		env     map[string]string
		options []trace.SpanStartOption
	}{
		{
			name: "with a simple span",
		},
		{
			name: "with several links",
			options: []trace.SpanStartOption{
				trace.WithLinks(links...),
			},
		},
		{
			name: "with attributes",
			options: []trace.SpanStartOption{
				trace.WithAttributes(
					attribute.String("key1", "value1"),
					attribute.String("key2", "value2"),
				),
			},
		},
		{
			name: "ObservabilityEnabled",
			env: map[string]string{
				"OTEL_GO_X_OBSERVABILITY": "True",
			},
		},
	} {
		b.Run(tt.name, func(b *testing.B) {
			for k, v := range tt.env {
				b.Setenv(k, v)
			}

			tracer := NewTracerProvider().Tracer("")

			spans := make([]trace.Span, b.N)
			b.ReportAllocs()
			b.ResetTimer()

			for i := 0; i < b.N; i++ {
				_, span := tracer.Start(ctx, "", tt.options...)
				spans[i] = span
			}

			b.StopTimer()
			for i := 0; i < b.N; i++ {
				spans[i].End()
			}
		})
	}
}
