// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package jaeger

import (
	"context"
	"encoding/binary"
	"fmt"
	"os"
	"sort"
	"testing"
	"time"

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

	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/codes"
	gen "go.opentelemetry.io/otel/exporters/jaeger/internal/gen-go/jaeger"
	ottest "go.opentelemetry.io/otel/exporters/jaeger/internal/internaltest"
	"go.opentelemetry.io/otel/sdk/instrumentation"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	"go.opentelemetry.io/otel/sdk/trace/tracetest"
	semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
	"go.opentelemetry.io/otel/trace"
)

func TestNewRawExporter(t *testing.T) {
	testCases := []struct {
		name     string
		endpoint EndpointOption
	}{
		{
			name:     "default exporter with collector endpoint",
			endpoint: WithCollectorEndpoint(),
		},
		{
			name:     "default exporter with agent endpoint",
			endpoint: WithAgentEndpoint(),
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			_, err := New(tc.endpoint)
			assert.NoError(t, err)
		})
	}
}

func TestNewRawExporterUseEnvVarIfOptionUnset(t *testing.T) {
	// Record and restore env
	envStore := ottest.NewEnvStore()
	envStore.Record(envEndpoint)
	defer func() {
		require.NoError(t, envStore.Restore())
	}()

	// If the user sets the environment variable OTEL_EXPORTER_JAEGER_ENDPOINT, endpoint will always get a value.
	require.NoError(t, os.Unsetenv(envEndpoint))
	_, err := New(
		WithCollectorEndpoint(),
	)

	assert.NoError(t, err)
}

type testCollectorEndpoint struct {
	batchesUploaded []*gen.Batch
}

func (c *testCollectorEndpoint) shutdown(context.Context) error {
	return nil
}

func (c *testCollectorEndpoint) upload(_ context.Context, batch *gen.Batch) error {
	c.batchesUploaded = append(c.batchesUploaded, batch)
	return nil
}

var _ batchUploader = (*testCollectorEndpoint)(nil)

func withTestCollectorEndpoint() EndpointOption {
	return endpointOptionFunc(func() (batchUploader, error) {
		return &testCollectorEndpoint{}, nil
	})
}

func withTestCollectorEndpointInjected(ce *testCollectorEndpoint) EndpointOption {
	return endpointOptionFunc(func() (batchUploader, error) {
		return ce, nil
	})
}

func TestExporterExportSpan(t *testing.T) {
	const (
		serviceName = "test-service"
		tagKey      = "key"
		tagVal      = "val"
	)

	testCollector := &testCollectorEndpoint{}
	exp, err := New(withTestCollectorEndpointInjected(testCollector))
	require.NoError(t, err)
	tp := sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exp),
		sdktrace.WithResource(resource.NewSchemaless(
			semconv.ServiceName(serviceName),
			attribute.String(tagKey, tagVal),
		)),
	)

	tracer := tp.Tracer("test-tracer")

	ctx := context.Background()
	for i := 0; i < 3; i++ {
		_, span := tracer.Start(ctx, fmt.Sprintf("test-span-%d", i))
		span.End()
		assert.True(t, span.SpanContext().IsValid())
	}

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

	batchesUploaded := testCollector.batchesUploaded
	require.Len(t, batchesUploaded, 1)
	uploadedBatch := batchesUploaded[0]
	assert.Equal(t, serviceName, uploadedBatch.GetProcess().GetServiceName())
	assert.Len(t, uploadedBatch.GetSpans(), 3)

	require.Len(t, uploadedBatch.GetProcess().GetTags(), 1)
	assert.Equal(t, tagKey, uploadedBatch.GetProcess().GetTags()[0].GetKey())
	assert.Equal(t, tagVal, uploadedBatch.GetProcess().GetTags()[0].GetVStr())
}

func TestSpanSnapshotToThrift(t *testing.T) {
	now := time.Now()
	traceID, _ := trace.TraceIDFromHex("0102030405060708090a0b0c0d0e0f10")
	spanID, _ := trace.SpanIDFromHex("0102030405060708")
	parentSpanID, _ := trace.SpanIDFromHex("0807060504030201")

	linkTraceID, _ := trace.TraceIDFromHex("0102030405060709090a0b0c0d0e0f11")
	linkSpanID, _ := trace.SpanIDFromHex("0102030405060709")

	eventNameValue := "event-test"
	eventDropped := int64(10)
	keyValue := "value"
	statusCodeValue := "ERROR"
	doubleValue := 123.456
	intValue := int64(123)
	boolTrue := true
	arrValue := "[0,1,2,3]"
	statusMessage := "this is a problem"
	spanKind := "client"
	rv1 := "rv11"
	rv2 := int64(5)
	instrLibName := "instrumentation-library"
	instrLibVersion := "semver:1.0.0"

	tests := []struct {
		name string
		data tracetest.SpanStub
		want *gen.Span
	}{
		{
			name: "no status description",
			data: tracetest.SpanStub{
				SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
					TraceID: traceID,
					SpanID:  spanID,
				}),
				Name:      "/foo",
				StartTime: now,
				EndTime:   now,
				Status:    sdktrace.Status{Code: codes.Error},
				SpanKind:  trace.SpanKindClient,
				InstrumentationLibrary: instrumentation.Library{
					Name:    instrLibName,
					Version: instrLibVersion,
				},
			},
			want: &gen.Span{
				TraceIdLow:    651345242494996240,
				TraceIdHigh:   72623859790382856,
				SpanId:        72623859790382856,
				OperationName: "/foo",
				StartTime:     now.UnixNano() / 1000,
				Duration:      0,
				Tags: []*gen.Tag{
					{Key: keyError, VType: gen.TagType_BOOL, VBool: &boolTrue},
					{Key: keyInstrumentationLibraryName, VType: gen.TagType_STRING, VStr: &instrLibName},
					{Key: keyInstrumentationLibraryVersion, VType: gen.TagType_STRING, VStr: &instrLibVersion},
					{Key: keyStatusCode, VType: gen.TagType_STRING, VStr: &statusCodeValue},
					// Should not have a status message because it was unset
					{Key: keySpanKind, VType: gen.TagType_STRING, VStr: &spanKind},
				},
			},
		},
		{
			name: "no parent",
			data: tracetest.SpanStub{
				SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
					TraceID: traceID,
					SpanID:  spanID,
				}),
				Name:      "/foo",
				StartTime: now,
				EndTime:   now,
				Links: []sdktrace.Link{
					{
						SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
							TraceID: linkTraceID,
							SpanID:  linkSpanID,
						}),
					},
				},
				Attributes: []attribute.KeyValue{
					attribute.String("key", keyValue),
					attribute.Float64("double", doubleValue),
					attribute.Int64("int", intValue),
				},
				Events: []sdktrace.Event{
					{
						Name:                  eventNameValue,
						Attributes:            []attribute.KeyValue{attribute.String("k1", keyValue)},
						DroppedAttributeCount: int(eventDropped),
						Time:                  now,
					},
				},
				Status: sdktrace.Status{
					Code:        codes.Error,
					Description: statusMessage,
				},
				SpanKind: trace.SpanKindClient,
				InstrumentationLibrary: instrumentation.Library{
					Name:    instrLibName,
					Version: instrLibVersion,
				},
			},
			want: &gen.Span{
				TraceIdLow:    651345242494996240,
				TraceIdHigh:   72623859790382856,
				SpanId:        72623859790382856,
				OperationName: "/foo",
				StartTime:     now.UnixNano() / 1000,
				Duration:      0,
				Tags: []*gen.Tag{
					{Key: "double", VType: gen.TagType_DOUBLE, VDouble: &doubleValue},
					{Key: "key", VType: gen.TagType_STRING, VStr: &keyValue},
					{Key: "int", VType: gen.TagType_LONG, VLong: &intValue},
					{Key: keyError, VType: gen.TagType_BOOL, VBool: &boolTrue},
					{Key: keyInstrumentationLibraryName, VType: gen.TagType_STRING, VStr: &instrLibName},
					{Key: keyInstrumentationLibraryVersion, VType: gen.TagType_STRING, VStr: &instrLibVersion},
					{Key: keyStatusCode, VType: gen.TagType_STRING, VStr: &statusCodeValue},
					{Key: keyStatusMessage, VType: gen.TagType_STRING, VStr: &statusMessage},
					{Key: keySpanKind, VType: gen.TagType_STRING, VStr: &spanKind},
				},
				References: []*gen.SpanRef{
					{
						RefType:     gen.SpanRefType_FOLLOWS_FROM,
						TraceIdHigh: int64(binary.BigEndian.Uint64(linkTraceID[0:8])),
						TraceIdLow:  int64(binary.BigEndian.Uint64(linkTraceID[8:16])),
						SpanId:      int64(binary.BigEndian.Uint64(linkSpanID[:])),
					},
				},
				Logs: []*gen.Log{
					{
						Timestamp: now.UnixNano() / 1000,
						Fields: []*gen.Tag{
							{
								Key:   keyEventName,
								VStr:  &eventNameValue,
								VType: gen.TagType_STRING,
							},
							{
								Key:   "k1",
								VStr:  &keyValue,
								VType: gen.TagType_STRING,
							},
							{
								Key:   keyDroppedAttributeCount,
								VLong: &eventDropped,
								VType: gen.TagType_LONG,
							},
						},
					},
				},
			},
		},
		{
			name: "with parent",
			data: tracetest.SpanStub{
				SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
					TraceID: traceID,
					SpanID:  spanID,
				}),
				Parent: trace.NewSpanContext(trace.SpanContextConfig{
					TraceID: traceID,
					SpanID:  parentSpanID,
				}),
				Links: []sdktrace.Link{
					{
						SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
							TraceID: linkTraceID,
							SpanID:  linkSpanID,
						}),
					},
				},
				Name:      "/foo",
				StartTime: now,
				EndTime:   now,
				Attributes: []attribute.KeyValue{
					attribute.IntSlice("arr", []int{0, 1, 2, 3}),
				},
				Status: sdktrace.Status{
					Code:        codes.Unset,
					Description: statusMessage,
				},
				SpanKind: trace.SpanKindInternal,
				InstrumentationLibrary: instrumentation.Library{
					Name:    instrLibName,
					Version: instrLibVersion,
				},
			},
			want: &gen.Span{
				TraceIdLow:    651345242494996240,
				TraceIdHigh:   72623859790382856,
				SpanId:        72623859790382856,
				ParentSpanId:  578437695752307201,
				OperationName: "/foo",
				StartTime:     now.UnixNano() / 1000,
				Duration:      0,
				Tags: []*gen.Tag{
					// status code, message and span kind should NOT be populated
					{Key: "arr", VType: gen.TagType_STRING, VStr: &arrValue},
					{Key: keyInstrumentationLibraryName, VType: gen.TagType_STRING, VStr: &instrLibName},
					{Key: keyInstrumentationLibraryVersion, VType: gen.TagType_STRING, VStr: &instrLibVersion},
				},
				References: []*gen.SpanRef{
					{
						RefType:     gen.SpanRefType_FOLLOWS_FROM,
						TraceIdHigh: int64(binary.BigEndian.Uint64(linkTraceID[0:8])),
						TraceIdLow:  int64(binary.BigEndian.Uint64(linkTraceID[8:16])),
						SpanId:      int64(binary.BigEndian.Uint64(linkSpanID[:])),
					},
				},
			},
		},
		{
			name: "resources do not affect the tags",
			data: tracetest.SpanStub{
				SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
					TraceID: traceID,
					SpanID:  spanID,
				}),
				Parent: trace.NewSpanContext(trace.SpanContextConfig{
					TraceID: traceID,
					SpanID:  parentSpanID,
				}),
				Name:      "/foo",
				StartTime: now,
				EndTime:   now,
				Resource: resource.NewSchemaless(
					attribute.String("rk1", rv1),
					attribute.Int64("rk2", rv2),
					semconv.ServiceName("service name"),
				),
				Status: sdktrace.Status{
					Code:        codes.Unset,
					Description: statusMessage,
				},
				SpanKind: trace.SpanKindInternal,
				InstrumentationLibrary: instrumentation.Library{
					Name:    instrLibName,
					Version: instrLibVersion,
				},
			},
			want: &gen.Span{
				TraceIdLow:    651345242494996240,
				TraceIdHigh:   72623859790382856,
				SpanId:        72623859790382856,
				ParentSpanId:  578437695752307201,
				OperationName: "/foo",
				StartTime:     now.UnixNano() / 1000,
				Duration:      0,
				Tags: []*gen.Tag{
					{Key: keyInstrumentationLibraryName, VType: gen.TagType_STRING, VStr: &instrLibName},
					{Key: keyInstrumentationLibraryVersion, VType: gen.TagType_STRING, VStr: &instrLibVersion},
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := spanToThrift(tt.data.Snapshot())
			sort.Slice(got.Tags, func(i, j int) bool {
				return got.Tags[i].Key < got.Tags[j].Key
			})
			sort.Slice(tt.want.Tags, func(i, j int) bool {
				return tt.want.Tags[i].Key < tt.want.Tags[j].Key
			})
			if diff := cmp.Diff(got, tt.want); diff != "" {
				t.Errorf("Diff%v", diff)
			}
		})
	}
}

func TestExporterShutdownHonorsCancel(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	e, err := New(withTestCollectorEndpoint())
	require.NoError(t, err)
	assert.EqualError(t, e.Shutdown(ctx), context.Canceled.Error())
}

func TestExporterShutdownHonorsTimeout(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond)
	<-ctx.Done()

	e, err := New(withTestCollectorEndpoint())
	require.NoError(t, err)
	assert.EqualError(t, e.Shutdown(ctx), context.DeadlineExceeded.Error())
	cancel()
}

func TestErrorOnExportShutdownExporter(t *testing.T) {
	e, err := New(withTestCollectorEndpoint())
	require.NoError(t, err)
	assert.NoError(t, e.Shutdown(context.Background()))
	assert.NoError(t, e.ExportSpans(context.Background(), nil))
}

func TestExporterExportSpansHonorsCancel(t *testing.T) {
	e, err := New(withTestCollectorEndpoint())
	require.NoError(t, err)
	now := time.Now()
	ss := tracetest.SpanStubs{
		{
			Name: "s1",
			Resource: resource.NewSchemaless(
				semconv.ServiceName("name"),
				attribute.Key("r1").String("v1"),
			),
			StartTime: now,
			EndTime:   now,
		},
		{
			Name: "s2",
			Resource: resource.NewSchemaless(
				semconv.ServiceName("name"),
				attribute.Key("r2").String("v2"),
			),
			StartTime: now,
			EndTime:   now,
		},
	}
	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	assert.EqualError(t, e.ExportSpans(ctx, ss.Snapshots()), context.Canceled.Error())
}

func TestExporterExportSpansHonorsTimeout(t *testing.T) {
	e, err := New(withTestCollectorEndpoint())
	require.NoError(t, err)
	now := time.Now()
	ss := tracetest.SpanStubs{
		{
			Name: "s1",
			Resource: resource.NewSchemaless(
				semconv.ServiceName("name"),
				attribute.Key("r1").String("v1"),
			),
			StartTime: now,
			EndTime:   now,
		},
		{
			Name: "s2",
			Resource: resource.NewSchemaless(
				semconv.ServiceName("name"),
				attribute.Key("r2").String("v2"),
			),
			StartTime: now,
			EndTime:   now,
		},
	}
	ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond)
	defer cancel()
	<-ctx.Done()

	assert.EqualError(t, e.ExportSpans(ctx, ss.Snapshots()), context.DeadlineExceeded.Error())
}

func TestJaegerBatchList(t *testing.T) {
	newString := func(value string) *string {
		return &value
	}
	spanKind := "unspecified"
	now := time.Now()

	testCases := []struct {
		name               string
		roSpans            []sdktrace.ReadOnlySpan
		defaultServiceName string
		expectedBatchList  []*gen.Batch
	}{
		{
			name:              "no span shots",
			roSpans:           nil,
			expectedBatchList: nil,
		},
		{
			name: "span's snapshot contains nil span",
			roSpans: []sdktrace.ReadOnlySpan{
				tracetest.SpanStub{
					Name: "s1",
					Resource: resource.NewSchemaless(
						semconv.ServiceName("name"),
						attribute.Key("r1").String("v1"),
					),
					StartTime: now,
					EndTime:   now,
				}.Snapshot(),
				nil,
			},
			expectedBatchList: []*gen.Batch{
				{
					Process: &gen.Process{
						ServiceName: "name",
						Tags: []*gen.Tag{
							{Key: "r1", VType: gen.TagType_STRING, VStr: newString("v1")},
						},
					},
					Spans: []*gen.Span{
						{
							OperationName: "s1",
							Tags: []*gen.Tag{
								{Key: keySpanKind, VType: gen.TagType_STRING, VStr: &spanKind},
							},
							StartTime: now.UnixNano() / 1000,
						},
					},
				},
			},
		},
		{
			name: "merge spans that have the same resources",
			roSpans: tracetest.SpanStubs{
				{
					Name: "s1",
					Resource: resource.NewSchemaless(
						semconv.ServiceName("name"),
						attribute.Key("r1").String("v1"),
					),
					StartTime: now,
					EndTime:   now,
				},
				{
					Name: "s2",
					Resource: resource.NewSchemaless(
						semconv.ServiceName("name"),
						attribute.Key("r1").String("v1"),
					),
					StartTime: now,
					EndTime:   now,
				},
				{
					Name: "s3",
					Resource: resource.NewSchemaless(
						semconv.ServiceName("name"),
						attribute.Key("r2").String("v2"),
					),
					StartTime: now,
					EndTime:   now,
				},
			}.Snapshots(),
			expectedBatchList: []*gen.Batch{
				{
					Process: &gen.Process{
						ServiceName: "name",
						Tags: []*gen.Tag{
							{Key: "r1", VType: gen.TagType_STRING, VStr: newString("v1")},
						},
					},
					Spans: []*gen.Span{
						{
							OperationName: "s1",
							Tags: []*gen.Tag{
								{Key: "span.kind", VType: gen.TagType_STRING, VStr: &spanKind},
							},
							StartTime: now.UnixNano() / 1000,
						},
						{
							OperationName: "s2",
							Tags: []*gen.Tag{
								{Key: "span.kind", VType: gen.TagType_STRING, VStr: &spanKind},
							},
							StartTime: now.UnixNano() / 1000,
						},
					},
				},
				{
					Process: &gen.Process{
						ServiceName: "name",
						Tags: []*gen.Tag{
							{Key: "r2", VType: gen.TagType_STRING, VStr: newString("v2")},
						},
					},
					Spans: []*gen.Span{
						{
							OperationName: "s3",
							Tags: []*gen.Tag{
								{Key: "span.kind", VType: gen.TagType_STRING, VStr: &spanKind},
							},
							StartTime: now.UnixNano() / 1000,
						},
					},
				},
			},
		},
		{
			name: "no service name in spans",
			roSpans: tracetest.SpanStubs{
				{
					Name: "s1",
					Resource: resource.NewSchemaless(
						attribute.Key("r1").String("v1"),
					),
					StartTime: now,
					EndTime:   now,
				},
			}.Snapshots(),
			defaultServiceName: "default service name",
			expectedBatchList: []*gen.Batch{
				{
					Process: &gen.Process{
						ServiceName: "default service name",
						Tags: []*gen.Tag{
							{Key: "r1", VType: gen.TagType_STRING, VStr: newString("v1")},
						},
					},
					Spans: []*gen.Span{
						{
							OperationName: "s1",
							Tags: []*gen.Tag{
								{Key: "span.kind", VType: gen.TagType_STRING, VStr: &spanKind},
							},
							StartTime: now.UnixNano() / 1000,
						},
					},
				},
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			batchList := jaegerBatchList(tc.roSpans, tc.defaultServiceName)

			assert.ElementsMatch(t, tc.expectedBatchList, batchList)
		})
	}
}

func TestProcess(t *testing.T) {
	v1 := "v1"

	testCases := []struct {
		name               string
		res                *resource.Resource
		defaultServiceName string
		expectedProcess    *gen.Process
	}{
		{
			name: "resources contain service name",
			res: resource.NewSchemaless(
				semconv.ServiceName("service name"),
				attribute.Key("r1").String("v1"),
			),
			defaultServiceName: "default service name",
			expectedProcess: &gen.Process{
				ServiceName: "service name",
				Tags: []*gen.Tag{
					{Key: "r1", VType: gen.TagType_STRING, VStr: &v1},
				},
			},
		},
		{
			name:               "resources don't have service name",
			res:                resource.NewSchemaless(attribute.Key("r1").String("v1")),
			defaultServiceName: "default service name",
			expectedProcess: &gen.Process{
				ServiceName: "default service name",
				Tags: []*gen.Tag{
					{Key: "r1", VType: gen.TagType_STRING, VStr: &v1},
				},
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			pro := process(tc.res, tc.defaultServiceName)

			assert.Equal(t, tc.expectedProcess, pro)
		})
	}
}
