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

package trace

import (
	"context"
	"encoding/json"
	"errors"
	"math"
	"strconv"
	"sync"
	"testing"
	"time"

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

	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/codes"
	semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
	"go.opentelemetry.io/otel/trace/internal/telemetry"
)

const tName = "tracer.name"

var (
	attrs = []attribute.KeyValue{
		attribute.Bool("bool", true),
		attribute.Int("int", -1),
		attribute.Int64("int64", 43),
		attribute.Float64("float64", 0.3),
		attribute.String("string", "value"),
		attribute.BoolSlice("bool slice", []bool{true, false, true}),
		attribute.IntSlice("int slice", []int{-1, -30, 328}),
		attribute.Int64Slice("int64 slice", []int64{1030, 0, 0}),
		attribute.Float64Slice("float64 slice", []float64{1e9}),
		attribute.StringSlice("string slice", []string{"one", "two"}),
	}

	tAttrs = []telemetry.Attr{
		telemetry.Bool("bool", true),
		telemetry.Int("int", -1),
		telemetry.Int64("int64", 43),
		telemetry.Float64("float64", 0.3),
		telemetry.String("string", "value"),
		telemetry.Slice(
			"bool slice",
			telemetry.BoolValue(true),
			telemetry.BoolValue(false),
			telemetry.BoolValue(true),
		),
		telemetry.Slice(
			"int slice",
			telemetry.IntValue(-1),
			telemetry.IntValue(-30),
			telemetry.IntValue(328),
		),
		telemetry.Slice(
			"int64 slice",
			telemetry.Int64Value(1030),
			telemetry.Int64Value(0),
			telemetry.Int64Value(0),
		),
		telemetry.Slice("float64 slice", telemetry.Float64Value(1e9)),
		telemetry.Slice(
			"string slice",
			telemetry.StringValue("one"),
			telemetry.StringValue("two"),
		),
	}

	spanContext0 = NewSpanContext(SpanContextConfig{
		TraceID:    TraceID{0x1},
		SpanID:     SpanID{0x1},
		TraceFlags: FlagsSampled,
	})
	spanContext1 = NewSpanContext(SpanContextConfig{
		TraceID:    TraceID{0x2},
		SpanID:     SpanID{0x2},
		TraceFlags: FlagsSampled,
	})

	link0 = Link{
		SpanContext: spanContext0,
		Attributes: []attribute.KeyValue{
			attribute.Int("n", 0),
		},
	}
	link1 = Link{
		SpanContext: spanContext1,
		Attributes: []attribute.KeyValue{
			attribute.Int("n", 1),
		},
	}

	tLink0 = &telemetry.SpanLink{
		TraceID: telemetry.TraceID(spanContext0.TraceID()),
		SpanID:  telemetry.SpanID(spanContext0.SpanID()),
		Flags:   uint32(spanContext0.TraceFlags()),
		Attrs:   []telemetry.Attr{telemetry.Int("n", 0)},
	}
	tLink1 = &telemetry.SpanLink{
		TraceID: telemetry.TraceID(spanContext1.TraceID()),
		SpanID:  telemetry.SpanID(spanContext1.SpanID()),
		Flags:   uint32(spanContext1.TraceFlags()),
		Attrs:   []telemetry.Attr{telemetry.Int("n", 1)},
	}
)

func TestTracerProviderInstance(t *testing.T) {
	t.Parallel()

	tp0, tp1 := newAutoTracerProvider(), newAutoTracerProvider()

	assert.Same(t, tracerProviderInstance, tp0)
	assert.Same(t, tracerProviderInstance, tp1)
}

func TestTracerProviderConcurrentSafe(t *testing.T) {
	t.Parallel()

	const goroutines = 10

	run := func(tp TracerProvider) <-chan struct{} {
		done := make(chan struct{})
		go func(tp TracerProvider) {
			defer close(done)

			var wg sync.WaitGroup
			for i := range goroutines {
				wg.Add(1)
				go func(name, version string) {
					defer wg.Done()
					_ = tp.Tracer(name, WithInstrumentationVersion(version))
				}("tracer"+strconv.Itoa(i%4), strconv.Itoa(i%2))
			}

			wg.Wait()
		}(tp)
		return done
	}

	assert.NotPanics(t, func() {
		done0, done1 := run(newAutoTracerProvider()), run(newAutoTracerProvider())

		<-done0
		<-done1
	})
}

func TestSpanKindTransform(t *testing.T) {
	tests := map[SpanKind]telemetry.SpanKind{
		SpanKind(-1):          telemetry.SpanKind(0),
		SpanKindUnspecified:   telemetry.SpanKind(0),
		SpanKind(math.MaxInt): telemetry.SpanKind(0),

		SpanKindInternal: telemetry.SpanKindInternal,
		SpanKindServer:   telemetry.SpanKindServer,
		SpanKindClient:   telemetry.SpanKindClient,
		SpanKindProducer: telemetry.SpanKindProducer,
		SpanKindConsumer: telemetry.SpanKindConsumer,
	}

	for in, want := range tests {
		assert.Equal(t, want, spanKind(in), in.String())
	}
}

func TestConvAttrValueBytes(t *testing.T) {
	v := []byte("bytes")
	tests := []struct {
		name  string
		want  []byte
		limit int
	}{
		{
			name:  "Unlimited",
			want:  []byte("bytes"),
			limit: -1,
		},
		{
			name:  "Zero",
			want:  []byte(""),
			limit: 0,
		},
		{
			name:  "Truncate",
			want:  []byte("by"),
			limit: 2,
		},
		{
			name:  "NoTruncation",
			want:  []byte("bytes"),
			limit: 10,
		},
	}
	orig := maxSpan.AttrValueLen
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			t.Cleanup(func() { maxSpan.AttrValueLen = orig })
			maxSpan.AttrValueLen = test.limit

			val := convAttrValue(attribute.ByteSliceValue(v))

			assert.Equal(t, telemetry.ValueKindBytes, val.Kind())
			assert.Equal(t, test.want, val.AsBytes())
		})
	}
}

func TestConvAttrValueSlice(t *testing.T) {
	t.Parallel()

	val := convAttrValue(attribute.SliceValue(
		attribute.BoolValue(true),
		attribute.IntValue(2),
		attribute.SliceValue(
			attribute.StringValue("nested"),
			attribute.ByteSliceValue([]byte("bytes")),
		),
	))

	assert.Equal(t, telemetry.ValueKindSlice, val.Kind())

	slice := val.AsSlice()
	if assert.Len(t, slice, 3) {
		assert.Equal(t, telemetry.ValueKindBool, slice[0].Kind())
		assert.True(t, slice[0].AsBool())

		assert.Equal(t, telemetry.ValueKindInt64, slice[1].Kind())
		assert.EqualValues(t, 2, slice[1].AsInt64())

		assert.Equal(t, telemetry.ValueKindSlice, slice[2].Kind())
		nested := slice[2].AsSlice()
		if assert.Len(t, nested, 2) {
			assert.Equal(t, telemetry.ValueKindString, nested[0].Kind())
			assert.Equal(t, "nested", nested[0].AsString())

			assert.Equal(t, telemetry.ValueKindBytes, nested[1].Kind())
			assert.Equal(t, []byte("bytes"), nested[1].AsBytes())
		}
	}
}

func TestTracerStartPropagatesOrigCtx(t *testing.T) {
	t.Parallel()

	type ctxKey struct{}
	var key ctxKey
	val := "value"

	ctx := context.WithValue(t.Context(), key, val)
	ctx, _ = newAutoTracerProvider().Tracer(tName).Start(ctx, "span.name")

	assert.Equal(t, val, ctx.Value(key))
}

func TestTracerStartReturnsNonNilSpan(t *testing.T) {
	t.Parallel()

	tr := newAutoTracerProvider().Tracer(tName)
	_, s := tr.Start(t.Context(), "span.name")
	assert.NotNil(t, s)
}

func TestTracerStartAddsSpanToCtx(t *testing.T) {
	t.Parallel()

	tr := newAutoTracerProvider().Tracer(tName)
	ctx, s := tr.Start(t.Context(), "span.name")

	assert.Same(t, s, SpanFromContext(ctx))
}

func TestTracerConcurrentSafe(t *testing.T) {
	t.Parallel()

	const goroutines = 10

	ctx := t.Context()
	run := func(tracer Tracer) <-chan struct{} {
		done := make(chan struct{})

		go func(tr Tracer) {
			defer close(done)

			var wg sync.WaitGroup
			for i := range goroutines {
				wg.Add(1)
				go func(name string) {
					defer wg.Done()
					_, _ = tr.Start(ctx, name)
				}("span" + strconv.Itoa(i))
			}

			wg.Wait()
		}(tracer)

		return done
	}

	assert.NotPanics(t, func() {
		tp := newAutoTracerProvider()
		done0, done1 := run(tp.Tracer("t0")), run(tp.Tracer("t1"))

		<-done0
		<-done1
	})
}

func TestSpanCreation(t *testing.T) {
	const (
		spanName   = "span name"
		tracerName = "go.opentelemetry.io/otel/sdk/test"
		tracerVer  = "v0.1.0"
	)

	ts := time.Now()

	tracer := newAutoTracerProvider().Tracer(
		tracerName,
		WithInstrumentationVersion(tracerVer),
		WithSchemaURL(semconv.SchemaURL),
	)

	assertTracer := func(traces *telemetry.Traces) func(*testing.T) {
		return func(t *testing.T) {
			t.Helper()

			rs := traces.ResourceSpans
			require.Len(t, rs, 1)
			sss := rs[0].ScopeSpans
			require.Len(t, sss, 1)
			ss := sss[0]
			assert.Equal(t, tracerName, ss.Scope.Name, "tracer name")
			assert.Equal(t, tracerVer, ss.Scope.Version, "tracer version")
			assert.Equal(t, semconv.SchemaURL, ss.SchemaURL, "tracer schema URL")
		}
	}

	testcases := []struct {
		TestName string
		SpanName string
		Options  []SpanStartOption
		Setup    func(*testing.T)
		Eval     func(*testing.T, context.Context, *autoSpan)
	}{
		{
			TestName: "SampledByDefault",
			Eval: func(t *testing.T, _ context.Context, s *autoSpan) {
				t.Run("Tracer", assertTracer(s.traces))

				assert.True(t, s.sampled.Load(), "not sampled by default.")
			},
		},
		{
			TestName: "ParentSpanContext",
			Setup: func(t *testing.T) {
				orig := start
				t.Cleanup(func() { start = orig })
				start = func(_ context.Context, _ *autoSpan, psc *SpanContext, _ *bool, _ *SpanContext) {
					*psc = spanContext0
				}
			},
			Eval: func(t *testing.T, _ context.Context, s *autoSpan) {
				t.Run("Tracer", assertTracer(s.traces))

				want := spanContext0.SpanID().String()
				got := s.span.ParentSpanID.String()
				assert.Equal(t, want, got)
			},
		},
		{
			TestName: "SpanContext",
			Setup: func(t *testing.T) {
				orig := start
				t.Cleanup(func() { start = orig })
				start = func(_ context.Context, _ *autoSpan, _ *SpanContext, _ *bool, sc *SpanContext) {
					*sc = spanContext0
				}
			},
			Eval: func(t *testing.T, _ context.Context, s *autoSpan) {
				t.Run("Tracer", assertTracer(s.traces))

				str := func(i interface{ String() string }) string {
					return i.String()
				}
				assert.Equal(t, str(spanContext0.TraceID()), s.span.TraceID.String(), "trace ID")
				assert.Equal(t, str(spanContext0.SpanID()), s.span.SpanID.String(), "autoSpan ID")
				assert.Equal(t, uint32(spanContext0.TraceFlags()), s.span.Flags, "flags")
				assert.Equal(t, str(spanContext0.TraceState()), s.span.TraceState, "tracestate")
			},
		},
		{
			TestName: "NotSampled",
			Setup: func(t *testing.T) {
				orig := start
				t.Cleanup(func() { start = orig })
				start = func(_ context.Context, _ *autoSpan, _ *SpanContext, s *bool, _ *SpanContext) {
					*s = false
				}
			},
			Eval: func(t *testing.T, _ context.Context, s *autoSpan) {
				assert.False(t, s.sampled.Load(), "sampled")
			},
		},
		{
			TestName: "WithName",
			SpanName: spanName,
			Eval: func(t *testing.T, _ context.Context, s *autoSpan) {
				t.Run("Tracer", assertTracer(s.traces))
				assert.Equal(t, spanName, s.span.Name)
			},
		},
		{
			TestName: "WithSpanKind",
			Options: []SpanStartOption{
				WithSpanKind(SpanKindClient),
			},
			Eval: func(t *testing.T, _ context.Context, s *autoSpan) {
				t.Run("Tracer", assertTracer(s.traces))
				assert.Equal(t, telemetry.SpanKindClient, s.span.Kind)
			},
		},
		{
			TestName: "WithTimestamp",
			Options: []SpanStartOption{
				WithTimestamp(ts),
			},
			Eval: func(t *testing.T, _ context.Context, s *autoSpan) {
				t.Run("Tracer", assertTracer(s.traces))
				assert.Equal(t, ts, s.span.StartTime)
			},
		},
		{
			TestName: "WithAttributes",
			Options: []SpanStartOption{
				WithAttributes(attrs...),
			},
			Eval: func(t *testing.T, _ context.Context, s *autoSpan) {
				t.Run("Tracer", assertTracer(s.traces))
				assert.Equal(t, tAttrs, s.span.Attrs)
			},
		},
		{
			TestName: "WithLinks",
			Options: []SpanStartOption{
				WithLinks(link0, link1),
			},
			Eval: func(t *testing.T, _ context.Context, s *autoSpan) {
				t.Run("Tracer", assertTracer(s.traces))
				want := []*telemetry.SpanLink{tLink0, tLink1}
				assert.Equal(t, want, s.span.Links)
			},
		},
	}

	ctx := t.Context()
	for _, tc := range testcases {
		t.Run(tc.TestName, func(t *testing.T) {
			if tc.Setup != nil {
				tc.Setup(t)
			}

			c, sIface := tracer.Start(ctx, tc.SpanName, tc.Options...)
			require.IsType(t, &autoSpan{}, sIface)
			s := sIface.(*autoSpan)

			tc.Eval(t, c, s)
		})
	}
}

func TestSpanEnd(t *testing.T) {
	orig := ended
	t.Cleanup(func() { ended = orig })

	var buf []byte
	ended = func(b []byte) { buf = b }

	timeNow := time.Unix(0, time.Now().UnixNano()) // No location.

	tests := []struct {
		Name    string
		Options []SpanEndOption
		Eval    func(*testing.T, time.Time)
	}{
		{
			Name: "Now",
			Eval: func(t *testing.T, ts time.Time) {
				assert.False(t, ts.IsZero(), "zero end time")
			},
		},
		{
			Name: "WithTimestamp",
			Options: []SpanEndOption{
				WithTimestamp(timeNow),
			},
			Eval: func(t *testing.T, ts time.Time) {
				assert.Equal(t, timeNow, ts, "end time not set")
			},
		},
	}

	for _, test := range tests {
		t.Run(test.Name, func(t *testing.T) {
			s := spanBuilder{}.Build()
			s.End(test.Options...)

			assert.False(t, s.sampled.Load(), "ended autoSpan should not be sampled")
			require.NotNil(t, buf, "no span data emitted")

			var traces telemetry.Traces
			err := json.Unmarshal(buf, &traces)
			require.NoError(t, err)

			rs := traces.ResourceSpans
			require.Len(t, rs, 1)
			ss := rs[0].ScopeSpans
			require.Len(t, ss, 1)
			spans := ss[0].Spans
			require.Len(t, spans, 1)

			test.Eval(t, spans[0].EndTime)
		})
	}
}

func TestSpanNilUnsampledGuards(t *testing.T) {
	run := func(fn func(s *autoSpan)) func(*testing.T) {
		return func(t *testing.T) {
			t.Helper()

			f := func(s *autoSpan) func() { return func() { fn(s) } }
			assert.NotPanics(t, f(nil), "nil span")
			assert.NotPanics(t, f(new(autoSpan)), "unsampled span")
		}
	}

	t.Run("End", run(func(s *autoSpan) { s.End() }))
	t.Run("AddEvent", run(func(s *autoSpan) { s.AddEvent("event name") }))
	t.Run("AddLink", run(func(s *autoSpan) { s.AddLink(Link{}) }))
	t.Run("IsRecording", run(func(s *autoSpan) { _ = s.IsRecording() }))
	t.Run("RecordError", run(func(s *autoSpan) { s.RecordError(nil) }))
	t.Run("SpanContext", run(func(s *autoSpan) { _ = s.SpanContext() }))
	t.Run("SetStatus", run(func(s *autoSpan) { s.SetStatus(codes.Error, "test") }))
	t.Run("SetName", run(func(s *autoSpan) { s.SetName("span name") }))
	t.Run("SetAttributes", run(func(s *autoSpan) { s.SetAttributes(attrs...) }))
	t.Run("TracerProvider", run(func(s *autoSpan) { _ = s.TracerProvider() }))
}

func TestSpanAddLink(t *testing.T) {
	s := spanBuilder{
		Options: []SpanStartOption{WithLinks(link0)},
	}.Build()
	s.AddLink(link1)

	want := []*telemetry.SpanLink{tLink0, tLink1}
	assert.Equal(t, want, s.span.Links)
}

func TestSpanAddLinkLimit(t *testing.T) {
	tests := []struct {
		limit   int
		want    []*telemetry.SpanLink
		dropped uint32
	}{
		{0, nil, 2},
		{1, []*telemetry.SpanLink{tLink1}, 1},
		{2, []*telemetry.SpanLink{tLink0, tLink1}, 0},
		{-1, []*telemetry.SpanLink{tLink0, tLink1}, 0},
	}

	for _, test := range tests {
		t.Run("Limit/"+strconv.Itoa(test.limit), func(t *testing.T) {
			orig := maxSpan.Links
			maxSpan.Links = test.limit
			t.Cleanup(func() { maxSpan.Links = orig })

			builder := spanBuilder{}
			s := builder.Build()
			s.AddLink(link0)
			s.AddLink(link1)
			assert.Equal(t, test.want, s.span.Links, "AddLink")
			assert.Equal(t, test.dropped, s.span.DroppedLinks, "AddLink DroppedLinks")

			builder.Options = []SpanStartOption{
				WithLinks(link0, link1),
			}
			s = builder.Build()
			assert.Equal(t, test.want, s.span.Links, "NewSpan")
			assert.Equal(t, test.dropped, s.span.DroppedLinks, "NewSpan DroppedLinks")
		})
	}
}

func TestSpanLinkAttrLimit(t *testing.T) {
	tests := []struct {
		limit   int
		want    []telemetry.Attr
		dropped uint32
	}{
		{0, nil, uint32(len(tAttrs))},
		{2, tAttrs[:2], uint32(len(tAttrs) - 2)},
		{len(tAttrs), tAttrs, 0},
		{-1, tAttrs, 0},
	}

	link := Link{Attributes: attrs}
	for _, test := range tests {
		t.Run("Limit/"+strconv.Itoa(test.limit), func(t *testing.T) {
			orig := maxSpan.LinkAttrs
			maxSpan.LinkAttrs = test.limit
			t.Cleanup(func() { maxSpan.LinkAttrs = orig })

			builder := spanBuilder{}

			s := builder.Build()
			s.AddLink(link)

			require.Len(t, s.span.Links, 1)
			got := s.span.Links[0]
			assert.Equal(t, test.want, got.Attrs, "AddLink attrs")
			assert.Equal(t, test.dropped, got.DroppedAttrs, "dropped AddLink attrs")

			builder.Options = []SpanStartOption{WithLinks(link)}
			s = builder.Build()

			require.Len(t, s.span.Links, 1)
			got = s.span.Links[0]
			assert.Equal(t, test.want, got.Attrs, "NewSpan link attrs")
			assert.Equal(t, test.dropped, got.DroppedAttrs, "dropped NewSpan link attrs")
		})
	}
}

func TestSpanIsRecording(t *testing.T) {
	builder := spanBuilder{}
	s := builder.Build()
	assert.True(t, s.IsRecording(), "sampled span should be recorded")

	builder.NotSampled = true
	s = builder.Build()
	assert.False(t, s.IsRecording(), "unsampled span should not be recorded")
}

func TestSpanRecordError(t *testing.T) {
	s := spanBuilder{}.Build()

	var want []*telemetry.SpanEvent
	s.RecordError(nil)
	require.Equal(t, want, s.span.Events, "nil error recorded")

	ts := time.Now()
	err := errors.New("test")
	s.RecordError(
		err,
		WithTimestamp(ts),
		WithAttributes(attribute.Bool("testing", true)),
	)
	want = append(want, &telemetry.SpanEvent{
		Name: semconv.ExceptionEventName,
		Time: ts,
		Attrs: []telemetry.Attr{
			telemetry.Bool("testing", true),
			telemetry.String(string(semconv.ExceptionTypeKey), "*errors.errorString"),
			telemetry.String(string(semconv.ExceptionMessageKey), err.Error()),
		},
	})
	assert.Equal(t, want, s.span.Events, "nil error recorded")

	s.RecordError(err, WithStackTrace(true))
	require.Len(t, s.span.Events, 2, "missing event")

	var hasST bool
	for _, attr := range s.span.Events[1].Attrs {
		if attr.Key == string(semconv.ExceptionStacktraceKey) {
			hasST = true
			break
		}
	}
	assert.True(t, hasST, "missing stacktrace attribute")
}

func TestAddEventLimit(t *testing.T) {
	const a, b, c = "a", "b", "c"

	ts := time.Now()

	evtA := &telemetry.SpanEvent{Name: "a", Time: ts}
	evtB := &telemetry.SpanEvent{Name: "b", Time: ts}
	evtC := &telemetry.SpanEvent{Name: "c", Time: ts}

	tests := []struct {
		limit   int
		want    []*telemetry.SpanEvent
		dropped uint32
	}{
		{0, nil, 3},
		{1, []*telemetry.SpanEvent{evtC}, 2},
		{2, []*telemetry.SpanEvent{evtB, evtC}, 1},
		{3, []*telemetry.SpanEvent{evtA, evtB, evtC}, 0},
		{-1, []*telemetry.SpanEvent{evtA, evtB, evtC}, 0},
	}

	for _, test := range tests {
		t.Run("Limit/"+strconv.Itoa(test.limit), func(t *testing.T) {
			orig := maxSpan.Events
			maxSpan.Events = test.limit
			t.Cleanup(func() { maxSpan.Events = orig })

			builder := spanBuilder{}

			s := builder.Build()
			s.addEvent(a, ts, nil)
			s.addEvent(b, ts, nil)
			s.addEvent(c, ts, nil)

			assert.Equal(t, test.want, s.span.Events, "add event")
			assert.Equal(t, test.dropped, s.span.DroppedEvents, "dropped events")
		})
	}
}

func TestAddEventAttrLimit(t *testing.T) {
	tests := []struct {
		limit   int
		want    []telemetry.Attr
		dropped uint32
	}{
		{0, nil, uint32(len(tAttrs))},
		{2, tAttrs[:2], uint32(len(tAttrs) - 2)},
		{len(tAttrs), tAttrs, 0},
		{-1, tAttrs, 0},
	}

	for _, test := range tests {
		t.Run("Limit/"+strconv.Itoa(test.limit), func(t *testing.T) {
			orig := maxSpan.EventAttrs
			maxSpan.EventAttrs = test.limit
			t.Cleanup(func() { maxSpan.EventAttrs = orig })

			builder := spanBuilder{}

			s := builder.Build()
			s.addEvent("name", time.Now(), attrs)

			require.Len(t, s.span.Events, 1)
			got := s.span.Events[0]
			assert.Equal(t, test.want, got.Attrs, "event attrs")
			assert.Equal(t, test.dropped, got.DroppedAttrs, "dropped event attrs")
		})
	}
}

func TestSpanSpanContext(t *testing.T) {
	s := spanBuilder{SpanContext: spanContext0}.Build()
	assert.Equal(t, spanContext0, s.SpanContext())
}

func TestSpanSetStatus(t *testing.T) {
	s := spanBuilder{}.Build()

	assert.Nil(t, s.span.Status, "empty status should not be set")

	const msg = "test"
	want := &telemetry.Status{Message: msg}

	for c, tCode := range map[codes.Code]telemetry.StatusCode{
		codes.Error: telemetry.StatusCodeError,
		codes.Ok:    telemetry.StatusCodeOK,
		codes.Unset: telemetry.StatusCodeUnset,
	} {
		want.Code = tCode
		s.SetStatus(c, msg)
		assert.Equalf(t, want, s.span.Status, "code: %s, msg: %s", c, msg)
	}
}

func TestSpanSetName(t *testing.T) {
	const name = "span name"
	builder := spanBuilder{}

	s := builder.Build()
	s.SetName(name)
	assert.Equal(t, name, s.span.Name, "span name not set")

	builder.Name = "alt"
	s = builder.Build()
	s.SetName(name)
	assert.Equal(t, name, s.span.Name, "SetName did not overwrite")
}

func TestSpanSetAttributes(t *testing.T) {
	builder := spanBuilder{}

	s := builder.Build()
	s.SetAttributes(attrs...)
	assert.Equal(t, tAttrs, s.span.Attrs, "span attributes not set")

	builder.Options = []SpanStartOption{
		WithAttributes(attrs[0].Key.Bool(!attrs[0].Value.AsBool())),
	}

	s = builder.Build()
	s.SetAttributes(attrs...)
	assert.Equal(t, tAttrs, s.span.Attrs, "SpanAttributes did not override")
}

func TestSpanAttributeLimits(t *testing.T) {
	tests := []struct {
		limit   int
		want    []telemetry.Attr
		dropped uint32
	}{
		{0, nil, uint32(len(tAttrs))},
		{2, tAttrs[:2], uint32(len(tAttrs) - 2)},
		{len(tAttrs), tAttrs, 0},
		{-1, tAttrs, 0},
	}

	for _, test := range tests {
		t.Run("Limit/"+strconv.Itoa(test.limit), func(t *testing.T) {
			orig := maxSpan.Attrs
			maxSpan.Attrs = test.limit
			t.Cleanup(func() { maxSpan.Attrs = orig })

			builder := spanBuilder{}

			s := builder.Build()
			s.SetAttributes(attrs...)
			assert.Equal(t, test.want, s.span.Attrs, "set span attributes")
			assert.Equal(t, test.dropped, s.span.DroppedAttrs, "dropped attrs")

			s.SetAttributes(attrs...)
			assert.Equal(t, test.want, s.span.Attrs, "set span attributes twice")
			assert.Equal(t, 2*test.dropped, s.span.DroppedAttrs, "2x dropped attrs")

			builder.Options = []SpanStartOption{WithAttributes(attrs...)}

			s = builder.Build()
			assert.Equal(t, test.want, s.span.Attrs, "new span attributes")
			assert.Equal(t, test.dropped, s.span.DroppedAttrs, "dropped attrs")
		})
	}
}

func TestSpanAttributeValueLimits(t *testing.T) {
	value := "hello world"

	aStr := attribute.String("string", value)
	aStrSlice := attribute.StringSlice("slice", []string{value, value})

	eq := func(a, b []telemetry.Attr) bool {
		if len(a) != len(b) {
			return false
		}
		for i := range a {
			if !a[i].Equal(b[i]) {
				return false
			}
		}
		return true
	}

	tests := []struct {
		limit int
		want  string
	}{
		{0, ""},
		{2, value[:2]},
		{11, value},
		{-1, value},
	}
	for _, test := range tests {
		t.Run("Limit/"+strconv.Itoa(test.limit), func(t *testing.T) {
			orig := maxSpan.AttrValueLen
			maxSpan.AttrValueLen = test.limit
			t.Cleanup(func() { maxSpan.AttrValueLen = orig })

			builder := spanBuilder{}

			want := []telemetry.Attr{
				telemetry.String("string", test.want),
				telemetry.Slice(
					"slice",
					telemetry.StringValue(test.want),
					telemetry.StringValue(test.want),
				),
			}

			s := builder.Build()
			s.SetAttributes(aStr, aStrSlice)
			assert.Truef(t, eq(want, s.span.Attrs), "set span attributes: got %#v, want %#v", s.span.Attrs, want)

			s.AddEvent("test", WithAttributes(aStr, aStrSlice))
			assert.Truef(
				t,
				eq(want, s.span.Events[0].Attrs),
				"span event attributes: got %#v, want %#v",
				s.span.Events[0].Attrs,
				want,
			)

			s.AddLink(Link{
				Attributes: []attribute.KeyValue{aStr, aStrSlice},
			})
			assert.Truef(
				t,
				eq(want, s.span.Links[0].Attrs),
				"span link attributes: got %#v, want %#v",
				s.span.Links[0].Attrs,
				want,
			)

			builder.Options = []SpanStartOption{
				WithAttributes(aStr, aStrSlice),
				WithLinks(Link{
					Attributes: []attribute.KeyValue{aStr, aStrSlice},
				}),
			}
			s = builder.Build()
			assert.Truef(t, eq(want, s.span.Attrs), "new span attributes: got %#v, want %#v", s.span.Attrs, want)
			assert.Truef(
				t,
				eq(want, s.span.Links[0].Attrs),
				"new span link attributes: got %#v, want %#v",
				s.span.Attrs,
				want,
			)
		})
	}
}

func TestSpanTracerProvider(t *testing.T) {
	var s autoSpan

	got := s.TracerProvider()
	assert.IsType(t, &autoTracerProvider{}, got)
}

type spanBuilder struct {
	Name        string
	NotSampled  bool
	SpanContext SpanContext
	Options     []SpanStartOption
}

func (b spanBuilder) Build() *autoSpan {
	tracer := new(autoTracer)
	s := &autoSpan{spanContext: b.SpanContext}
	s.sampled.Store(!b.NotSampled)
	s.traces, s.span = tracer.traces(
		b.Name,
		NewSpanStartConfig(b.Options...),
		s.spanContext,
		SpanContext{},
	)

	return s
}

func TestTruncate(t *testing.T) {
	type group struct {
		limit    int
		input    string
		expected string
	}

	tests := []struct {
		name   string
		groups []group
	}{
		// Edge case: limit is negative, no truncation should occur
		{
			name: "NoTruncation",
			groups: []group{
				{-1, "No truncation!", "No truncation!"},
			},
		},

		// Edge case: string is already shorter than the limit, no truncation
		// should occur
		{
			name: "ShortText",
			groups: []group{
				{10, "Short text", "Short text"},
				{15, "Short text", "Short text"},
				{100, "Short text", "Short text"},
			},
		},

		// Edge case: truncation happens with ASCII characters only
		{
			name: "ASCIIOnly",
			groups: []group{
				{1, "Hello World!", "H"},
				{5, "Hello World!", "Hello"},
				{12, "Hello World!", "Hello World!"},
			},
		},

		// Truncation including multi-byte characters (UTF-8)
		{
			name: "ValidUTF-8",
			groups: []group{
				{7, "Hello, 世界", "Hello, "},
				{8, "Hello, 世界", "Hello, 世"},
				{2, "こんにちは", "こん"},
				{3, "こんにちは", "こんに"},
				{5, "こんにちは", "こんにちは"},
				{12, "こんにちは", "こんにちは"},
			},
		},

		// Truncation with invalid UTF-8 characters
		{
			name: "InvalidUTF-8",
			groups: []group{
				{11, "Invalid\x80text", "Invalidtext"},
				// Do not modify invalid text if equal to limit.
				{11, "Valid text\x80", "Valid text\x80"},
				// Do not modify invalid text if under limit.
				{15, "Valid text\x80", "Valid text\x80"},
				{5, "Hello\x80World", "Hello"},
				{11, "Hello\x80World\x80!", "HelloWorld!"},
				{15, "Hello\x80World\x80Test", "HelloWorldTest"},
				{15, "Hello\x80\x80\x80World\x80Test", "HelloWorldTest"},
				{15, "\x80\x80\x80Hello\x80\x80\x80World\x80Test\x80\x80", "HelloWorldTest"},
			},
		},

		// Truncation with mixed validn and invalid UTF-8 characters
		{
			name: "MixedUTF-8",
			groups: []group{
				{6, "€"[0:2] + "hello€€", "hello€"},
				{6, "€" + "€"[0:2] + "hello", "€hello"},
				{11, "Valid text\x80📜", "Valid text📜"},
				{11, "Valid text📜\x80", "Valid text📜"},
				{14, "😊 Hello\x80World🌍🚀", "😊 HelloWorld🌍🚀"},
				{14, "😊\x80 Hello\x80World🌍🚀", "😊 HelloWorld🌍🚀"},
				{14, "😊\x80 Hello\x80World🌍\x80🚀", "😊 HelloWorld🌍🚀"},
				{14, "😊\x80 Hello\x80World🌍\x80🚀\x80", "😊 HelloWorld🌍🚀"},
				{14, "\x80😊\x80 Hello\x80World🌍\x80🚀\x80", "😊 HelloWorld🌍🚀"},
			},
		},

		// Edge case: empty string, should return empty string
		{
			name: "Empty",
			groups: []group{
				{5, "", ""},
			},
		},

		// Edge case: limit is 0, should return an empty string
		{
			name: "Zero",
			groups: []group{
				{0, "Some text", ""},
				{0, "", ""},
			},
		},
	}

	for _, tt := range tests {
		for _, g := range tt.groups {
			t.Run(tt.name, func(t *testing.T) {
				t.Parallel()

				got := truncate(g.limit, g.input)
				assert.Equalf(
					t, g.expected, got,
					"input: %q([]rune%v))\ngot: %q([]rune%v)\nwant %q([]rune%v)",
					g.input, []rune(g.input),
					got, []rune(got),
					g.expected, []rune(g.expected),
				)
			})
		}
	}
}

func BenchmarkTruncate(b *testing.B) {
	run := func(limit int, input string) func(b *testing.B) {
		return func(b *testing.B) {
			b.ReportAllocs()
			b.RunParallel(func(pb *testing.PB) {
				var out string
				for pb.Next() {
					out = truncate(limit, input)
				}
				_ = out
			})
		}
	}
	b.Run("Unlimited", run(-1, "hello 😊 world 🌍🚀"))
	b.Run("Zero", run(0, "Some text"))
	b.Run("Short", run(10, "Short Text"))
	b.Run("ASCII", run(5, "Hello, World!"))
	b.Run("ValidUTF-8", run(10, "hello 😊 world 🌍🚀"))
	b.Run("InvalidUTF-8", run(6, "€"[0:2]+"hello€€"))
	b.Run("MixedUTF-8", run(14, "\x80😊\x80 Hello\x80World🌍\x80🚀\x80"))
}

func TestSpanConcurrentSafe(t *testing.T) {
	t.Parallel()

	const (
		nTracers   = 2
		nSpans     = 2
		nGoroutine = 10
	)

	runSpan := func(s Span) <-chan struct{} {
		done := make(chan struct{})
		go func(span Span) {
			defer close(done)

			var wg sync.WaitGroup
			for i := range nGoroutine {
				wg.Add(1)
				go func(n int) {
					defer wg.Done()

					_ = span.IsRecording()
					_ = span.SpanContext()
					_ = span.TracerProvider()

					span.AddEvent("event")
					span.AddLink(Link{})
					span.RecordError(errors.New("err"))
					span.SetStatus(codes.Error, "error")
					span.SetName("span" + strconv.Itoa(n))
					span.SetAttributes(attribute.Bool("key", true))

					span.End()
				}(i)
			}

			wg.Wait()
		}(s)
		return done
	}

	runTracer := func(tr Tracer) <-chan struct{} {
		done := make(chan struct{})
		go func(tracer Tracer) {
			defer close(done)

			ctx := t.Context()

			var wg sync.WaitGroup
			for i := range nSpans {
				wg.Add(1)
				go func(n int) {
					defer wg.Done()
					_, s := tracer.Start(ctx, "span"+strconv.Itoa(n))
					<-runSpan(s)
				}(i)
			}

			wg.Wait()
		}(tr)
		return done
	}

	run := func(tp TracerProvider) <-chan struct{} {
		done := make(chan struct{})
		go func(provider TracerProvider) {
			defer close(done)

			var wg sync.WaitGroup
			for i := range nTracers {
				wg.Add(1)
				go func(n int) {
					defer wg.Done()
					<-runTracer(provider.Tracer("tracer" + strconv.Itoa(n)))
				}(i)
			}

			wg.Wait()
		}(tp)
		return done
	}

	assert.NotPanics(t, func() {
		done0, done1 := run(newAutoTracerProvider()), run(newAutoTracerProvider())

		<-done0
		<-done1
	})
}

func TestSpanLimit(t *testing.T) {
	tests := []struct {
		name string
		get  func(spanLimits) int
		zero int
		keys []string
	}{
		{
			name: "AttributeValueLengthLimit",
			get:  func(sl spanLimits) int { return sl.AttrValueLen },
			zero: -1,
			keys: []string{
				"OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT",
				"OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT",
			},
		},
		{
			name: "AttributeCountLimit",
			get:  func(sl spanLimits) int { return sl.Attrs },
			zero: 128,
			keys: []string{
				"OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT",
				"OTEL_ATTRIBUTE_COUNT_LIMIT",
			},
		},
		{
			name: "EventCountLimit",
			get:  func(sl spanLimits) int { return sl.Events },
			zero: 128,
			keys: []string{"OTEL_SPAN_EVENT_COUNT_LIMIT"},
		},
		{
			name: "EventAttributeCountLimit",
			get:  func(sl spanLimits) int { return sl.EventAttrs },
			zero: 128,
			keys: []string{"OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT"},
		},
		{
			name: "LinkCountLimit",
			get:  func(sl spanLimits) int { return sl.Links },
			zero: 128,
			keys: []string{"OTEL_SPAN_LINK_COUNT_LIMIT"},
		},
		{
			name: "LinkAttributeCountLimit",
			get:  func(sl spanLimits) int { return sl.LinkAttrs },
			zero: 128,
			keys: []string{"OTEL_LINK_ATTRIBUTE_COUNT_LIMIT"},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			t.Run("Default", func(t *testing.T) {
				assert.Equal(t, test.zero, test.get(newSpanLimits()))
			})

			t.Run("ValidValue", func(t *testing.T) {
				for _, key := range test.keys {
					t.Run(key, func(t *testing.T) {
						t.Setenv(key, "43")
						assert.Equal(t, 43, test.get(newSpanLimits()))
					})
				}
			})
		})
	}
}
