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

package otlpmetricgrpc

import (
	"context"
	"testing"
	"time"

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

	"google.golang.org/grpc/codes"

	"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/observ"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/counter"
	"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/otest"
	"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"
	semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
	"go.opentelemetry.io/otel/semconv/v1.41.0/otelconv"
)

func TestSelfObservability(t *testing.T) {
	coll, err := otest.NewGRPCCollector("", nil)
	require.NoError(t, err)
	defer coll.Shutdown()

	tests := []struct {
		name         string
		envValue     string
		endpoint     string
		expectError  bool
		setupMetrics func(*metricdata.ResourceMetrics)
		wantMetrics  func(actualComponentName, addr string, port int, err error) []metricdata.Metrics
	}{
		{
			name:        "success",
			envValue:    "true",
			endpoint:    "dns:///" + coll.Addr().String(),
			expectError: false,
			wantMetrics: func(actualComponentName, addr string, port int, _ error) []metricdata.Metrics {
				return []metricdata.Metrics{
					{
						Name:        otelconv.SDKExporterMetricDataPointExported{}.Name(),
						Description: otelconv.SDKExporterMetricDataPointExported{}.Description(),
						Unit:        otelconv.SDKExporterMetricDataPointExported{}.Unit(),
						Data: metricdata.Sum[int64]{
							Temporality: metricdata.CumulativeTemporality,
							IsMonotonic: true,
							DataPoints: []metricdata.DataPoint[int64]{
								{
									Attributes: attribute.NewSet(
										semconv.OTelComponentName(actualComponentName),
										semconv.OTelComponentTypeKey.String("otlp_grpc_metric_exporter"),
										semconv.ServerAddressKey.String(addr),
										semconv.ServerPortKey.Int(port),
									),
									Value: 4,
								},
							},
						},
					},
					{
						Name:        otelconv.SDKExporterMetricDataPointInflight{}.Name(),
						Description: otelconv.SDKExporterMetricDataPointInflight{}.Description(),
						Unit:        otelconv.SDKExporterMetricDataPointInflight{}.Unit(),
						Data: metricdata.Sum[int64]{
							Temporality: metricdata.CumulativeTemporality,
							IsMonotonic: false,
							DataPoints: []metricdata.DataPoint[int64]{
								{
									Attributes: attribute.NewSet(
										semconv.OTelComponentName(actualComponentName),
										semconv.OTelComponentTypeKey.String("otlp_grpc_metric_exporter"),
										semconv.ServerAddressKey.String(addr),
										semconv.ServerPortKey.Int(port),
									),
									Value: 0,
								},
							},
						},
					},
					{
						Name:        otelconv.SDKExporterOperationDuration{}.Name(),
						Description: otelconv.SDKExporterOperationDuration{}.Description(),
						Unit:        otelconv.SDKExporterOperationDuration{}.Unit(),
						Data: metricdata.Histogram[float64]{
							Temporality: metricdata.CumulativeTemporality,
							DataPoints: []metricdata.HistogramDataPoint[float64]{
								{
									Attributes: attribute.NewSet(
										semconv.OTelComponentName(actualComponentName),
										semconv.OTelComponentTypeKey.String("otlp_grpc_metric_exporter"),
										semconv.RPCResponseStatusCode(codes.OK.String()),
										semconv.ServerAddressKey.String(addr),
										semconv.ServerPortKey.Int(port),
									),
									Count: 1,
								},
							},
						},
					},
				}
			},
		},
		{
			name:        "error",
			envValue:    "true",
			endpoint:    "dns:///invalid:999999",
			expectError: true,
			wantMetrics: func(actualComponentName, addr string, port int, err error) []metricdata.Metrics {
				baseAttrs := []attribute.KeyValue{
					semconv.OTelComponentName(actualComponentName),
					semconv.OTelComponentTypeKey.String("otlp_grpc_metric_exporter"),
				}
				if addr != "" {
					baseAttrs = append(baseAttrs, semconv.ServerAddressKey.String(addr))
				}
				if port >= 0 {
					baseAttrs = append(baseAttrs, semconv.ServerPortKey.Int(port))
				}

				errorAttrs := append([]attribute.KeyValue{semconv.ErrorType(err)}, baseAttrs...)

				return []metricdata.Metrics{
					{
						Name:        otelconv.SDKExporterMetricDataPointExported{}.Name(),
						Description: otelconv.SDKExporterMetricDataPointExported{}.Description(),
						Unit:        otelconv.SDKExporterMetricDataPointExported{}.Unit(),
						Data: metricdata.Sum[int64]{
							Temporality: metricdata.CumulativeTemporality,
							IsMonotonic: true,
							DataPoints: []metricdata.DataPoint[int64]{
								{
									Attributes: attribute.NewSet(errorAttrs...),
									Value:      4,
								},
								{
									Attributes: attribute.NewSet(baseAttrs...),
									Value:      0,
								},
							},
						},
					},
					{
						Name:        otelconv.SDKExporterMetricDataPointInflight{}.Name(),
						Description: otelconv.SDKExporterMetricDataPointInflight{}.Description(),
						Unit:        otelconv.SDKExporterMetricDataPointInflight{}.Unit(),
						Data: metricdata.Sum[int64]{
							Temporality: metricdata.CumulativeTemporality,
							IsMonotonic: false,
							DataPoints: []metricdata.DataPoint[int64]{
								{
									Attributes: attribute.NewSet(baseAttrs...),
								},
							},
						},
					},
					{
						Name:        otelconv.SDKExporterOperationDuration{}.Name(),
						Description: otelconv.SDKExporterOperationDuration{}.Description(),
						Unit:        otelconv.SDKExporterOperationDuration{}.Unit(),
						Data: metricdata.Histogram[float64]{
							Temporality: metricdata.CumulativeTemporality,
							DataPoints: []metricdata.HistogramDataPoint[float64]{
								{
									Attributes: attribute.NewSet(
										append(
											[]attribute.KeyValue{semconv.RPCResponseStatusCode("Unavailable")},
											errorAttrs...,
										)...,
									),
									Count: 1,
								},
							},
						},
					},
				}
			},
		},
		{
			name:        "partial success (transform error)",
			envValue:    "true",
			endpoint:    "dns:///" + coll.Addr().String(),
			expectError: true,
			setupMetrics: func(rm *metricdata.ResourceMetrics) {
				// Add a metric with invalid temporality
				rm.ScopeMetrics[0].Metrics = append(rm.ScopeMetrics[0].Metrics, metricdata.Metrics{
					Name: "invalid_temporality_metric",
					Data: metricdata.Sum[int64]{
						Temporality: metricdata.Temporality(99),
						IsMonotonic: true,
						DataPoints: []metricdata.DataPoint[int64]{
							{Value: 1},
						},
					},
				})
			},
			wantMetrics: func(actualComponentName, addr string, port int, _ error) []metricdata.Metrics {
				return []metricdata.Metrics{
					{
						Name:        otelconv.SDKExporterMetricDataPointExported{}.Name(),
						Description: otelconv.SDKExporterMetricDataPointExported{}.Description(),
						Unit:        otelconv.SDKExporterMetricDataPointExported{}.Unit(),
						Data: metricdata.Sum[int64]{
							Temporality: metricdata.CumulativeTemporality,
							IsMonotonic: true,
							DataPoints: []metricdata.DataPoint[int64]{
								{
									Attributes: attribute.NewSet(
										semconv.OTelComponentName(actualComponentName),
										semconv.OTelComponentTypeKey.String("otlp_grpc_metric_exporter"),
										semconv.ServerAddressKey.String(addr),
										semconv.ServerPortKey.Int(port),
									),
									Value: 4,
								},
							},
						},
					},
					{
						Name:        otelconv.SDKExporterMetricDataPointInflight{}.Name(),
						Description: otelconv.SDKExporterMetricDataPointInflight{}.Description(),
						Unit:        otelconv.SDKExporterMetricDataPointInflight{}.Unit(),
						Data: metricdata.Sum[int64]{
							Temporality: metricdata.CumulativeTemporality,
							IsMonotonic: false,
							DataPoints: []metricdata.DataPoint[int64]{
								{
									Attributes: attribute.NewSet(
										semconv.OTelComponentName(actualComponentName),
										semconv.OTelComponentTypeKey.String("otlp_grpc_metric_exporter"),
										semconv.ServerAddressKey.String(addr),
										semconv.ServerPortKey.Int(port),
									),
									Value: 0,
								},
							},
						},
					},
					{
						Name:        otelconv.SDKExporterOperationDuration{}.Name(),
						Description: otelconv.SDKExporterOperationDuration{}.Description(),
						Unit:        otelconv.SDKExporterOperationDuration{}.Unit(),
						Data: metricdata.Histogram[float64]{
							Temporality: metricdata.CumulativeTemporality,
							DataPoints: []metricdata.HistogramDataPoint[float64]{
								{
									Attributes: attribute.NewSet(
										semconv.OTelComponentName(actualComponentName),
										semconv.OTelComponentTypeKey.String("otlp_grpc_metric_exporter"),
										semconv.RPCResponseStatusCode(codes.OK.String()),
										semconv.ServerAddressKey.String(addr),
										semconv.ServerPortKey.Int(port),
									),
									Count: 1,
								},
							},
						},
					},
				}
			},
		},
		{
			name:        "disabled",
			envValue:    "false",
			endpoint:    coll.Addr().String(),
			expectError: false,
			wantMetrics: func(_, _ string, _ int, _ error) []metricdata.Metrics {
				return nil
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Reset counter for predictable ID in test
			origID := counter.SetExporterID(0)
			t.Cleanup(func() { counter.SetExporterID(origID) })
			t.Setenv("OTEL_GO_X_OBSERVABILITY", tt.envValue)

			orig := otel.GetMeterProvider()
			t.Cleanup(func() { otel.SetMeterProvider(orig) })

			dropReaderMetrics := metric.NewView(
				metric.Instrument{
					Scope: instrumentation.Scope{Name: "go.opentelemetry.io/otel/sdk/metric/internal/observ"},
				},
				metric.Stream{Aggregation: metric.AggregationDrop{}},
			)

			reader := metric.NewManualReader()
			provider := metric.NewMeterProvider(
				metric.WithReader(reader),
				metric.WithView(dropReaderMetrics),
			)
			otel.SetMeterProvider(provider)

			exp, err := New(t.Context(),
				WithEndpoint(tt.endpoint),
				WithInsecure())
			require.NoError(t, err)
			ctx := context.Background() //nolint:usetesting // required to avoid getting a canceled context at cleanup.
			t.Cleanup(func() {
				require.NoError(t, exp.Shutdown(ctx))
			})

			rm := createTestResourceMetrics()
			if tt.setupMetrics != nil {
				tt.setupMetrics(rm)
			}
			exportErr := exp.Export(t.Context(), rm)
			if tt.expectError {
				assert.Error(t, exportErr)
			} else {
				require.NoError(t, exportErr)
			}

			var got metricdata.ResourceMetrics
			err = reader.Collect(t.Context(), &got)
			require.NoError(t, err)

			if len(tt.wantMetrics("", "", 0, nil)) == 0 {
				// Verify no metrics for our scope
				selfObsMetricCount := 0
				for _, sm := range got.ScopeMetrics {
					if sm.Scope.Name == "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" {
						selfObsMetricCount += len(sm.Metrics)
					}
				}
				assert.Equal(t, 0, selfObsMetricCount, "expected no self-observability metrics when disabled")
			} else {
				require.Len(t, got.ScopeMetrics, 1)
				actualComponentName := extractComponentName(got.ScopeMetrics[0])
				addr, port, err := observ.ParseCanonicalTarget(tt.endpoint)
				if err != nil {
					addr = ""
					port = -1
				}

				want := metricdata.ScopeMetrics{
					Scope: instrumentation.Scope{
						Name:      "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc",
						Version:   sdk.Version(),
						SchemaURL: semconv.SchemaURL,
					},
					Metrics: tt.wantMetrics(actualComponentName, addr, port, exportErr),
				}

				assertScopeMetricsEqual(t, want, got.ScopeMetrics[0])
			}
		})
	}
}

func assertScopeMetricsEqual(t *testing.T, want, got metricdata.ScopeMetrics) {
	t.Helper()

	assert.Equal(t, want.Scope, got.Scope)
	require.Len(t, got.Metrics, len(want.Metrics))

	for i := range want.Metrics {
		if isHistogramMetric(want.Metrics[i]) {
			metricdatatest.AssertEqual(
				t, want.Metrics[i], got.Metrics[i],
				metricdatatest.IgnoreTimestamp(),
				metricdatatest.IgnoreValue(),
			)
			continue
		}

		metricdatatest.AssertEqual(
			t, want.Metrics[i], got.Metrics[i],
			metricdatatest.IgnoreTimestamp(),
		)
	}
}

func isHistogramMetric(m metricdata.Metrics) bool {
	switch m.Data.(type) {
	case metricdata.Histogram[float64]:
		return true
	default:
		return false
	}
}

// extractComponentName extracts the component name from metrics data to handle dynamic counter.
func extractComponentName(scopeMetrics metricdata.ScopeMetrics) string {
	for _, m := range scopeMetrics.Metrics {
		switch data := m.Data.(type) {
		case metricdata.Sum[int64]:
			if len(data.DataPoints) > 0 {
				attrs := data.DataPoints[0].Attributes.ToSlice()
				for _, attr := range attrs {
					if attr.Key == semconv.OTelComponentNameKey {
						return attr.Value.AsString()
					}
				}
			}
		case metricdata.Histogram[float64]:
			if len(data.DataPoints) > 0 {
				attrs := data.DataPoints[0].Attributes.ToSlice()
				for _, attr := range attrs {
					if attr.Key == semconv.OTelComponentNameKey {
						return attr.Value.AsString()
					}
				}
			}
		}
	}
	return ""
}

// createTestResourceMetrics creates sample metric data for testing.
func createTestResourceMetrics() *metricdata.ResourceMetrics {
	now := time.Now()
	return &metricdata.ResourceMetrics{
		Resource: resource.Default(),
		ScopeMetrics: []metricdata.ScopeMetrics{
			{
				Scope: instrumentation.Scope{Name: "test", Version: "v1"},
				Metrics: []metricdata.Metrics{
					{
						Name: "test_gauge_int",
						Data: metricdata.Gauge[int64]{
							DataPoints: []metricdata.DataPoint[int64]{
								{Value: 1, Time: now},
								{Value: 2, Time: now},
							},
						},
					},
					{
						Name: "test_sum_float",
						Data: metricdata.Sum[float64]{
							Temporality: metricdata.CumulativeTemporality,
							IsMonotonic: true,
							DataPoints: []metricdata.DataPoint[float64]{
								{Value: 3.5, Time: now},
							},
						},
					},
					{
						Name: "test_histogram",
						Data: metricdata.Histogram[float64]{
							Temporality: metricdata.CumulativeTemporality,
							DataPoints: []metricdata.HistogramDataPoint[float64]{
								{Count: 10, Sum: 5.0, Time: now},
							},
						},
					},
				},
			},
		},
	}
}
