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

package metric_test

import (
	"testing"

	"github.com/stretchr/testify/assert"

	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/metric"
)

func TestConfig(t *testing.T) {
	version := "v1.1.1"
	schemaURL := "https://opentelemetry.io/schemas/1.37.0"
	attr := attribute.NewSet(
		attribute.String("user", "alice"),
		attribute.Bool("admin", true),
	)

	c := metric.NewMeterConfig(
		metric.WithInstrumentationVersion(version),
		metric.WithSchemaURL(schemaURL),
		metric.WithInstrumentationAttributes(attr.ToSlice()...),
	)

	assert.Equal(t, version, c.InstrumentationVersion(), "instrumentation version")
	assert.Equal(t, schemaURL, c.SchemaURL(), "schema URL")
	assert.Equal(t, attr, c.InstrumentationAttributes(), "instrumentation attributes")
}

func TestWithInstrumentationAttributesNotLazy(t *testing.T) {
	attrs := []attribute.KeyValue{
		attribute.String("service", "test"),
		attribute.Int("three", 3),
	}
	want := attribute.NewSet(attrs...)

	// WithInstrumentationAttributes is expected to immediately
	// create an immutable set from the attributes, so later changes
	// to attrs should not affect the config.
	opt := metric.WithInstrumentationAttributes(attrs...)
	attrs[0] = attribute.String("service", "changed")

	c := metric.NewMeterConfig(opt)
	assert.Equal(t, want, c.InstrumentationAttributes(), "instrumentation attributes")
}

func TestWithInstrumentationAttributeSet(t *testing.T) {
	attrs := attribute.NewSet(
		attribute.String("service", "test"),
		attribute.Int("three", 3),
	)

	c := metric.NewMeterConfig(
		metric.WithInstrumentationAttributeSet(attrs),
	)

	assert.Equal(t, attrs, c.InstrumentationAttributes(), "instrumentation attributes")
}

func TestWithInstrumentationAttributesMerge(t *testing.T) {
	aliceAttr := attribute.String("user", "Alice")
	bobAttr := attribute.String("user", "Bob")
	adminAttr := attribute.Bool("admin", true)

	alice := attribute.NewSet(aliceAttr)
	bob := attribute.NewSet(bobAttr)
	aliceAdmin := attribute.NewSet(aliceAttr, adminAttr)
	bobAdmin := attribute.NewSet(bobAttr, adminAttr)

	t.Run("SameKey", func(t *testing.T) {
		c := metric.NewMeterConfig(
			metric.WithInstrumentationAttributes(aliceAttr),
			metric.WithInstrumentationAttributes(bobAttr),
		)
		assert.Equal(t, bob, c.InstrumentationAttributes(),
			"Later values for the same key should overwrite earlier ones.")
	})

	t.Run("DifferentKeys", func(t *testing.T) {
		// Different keys should be merged
		c := metric.NewMeterConfig(
			metric.WithInstrumentationAttributes(aliceAttr),
			metric.WithInstrumentationAttributes(adminAttr),
		)
		assert.Equal(t, aliceAdmin, c.InstrumentationAttributes(),
			"Different keys should be merged.")
	})

	t.Run("Mixed", func(t *testing.T) {
		c := metric.NewMeterConfig(
			metric.WithInstrumentationAttributes(aliceAttr, adminAttr),
			metric.WithInstrumentationAttributes(bobAttr),
		)
		assert.Equal(t, bobAdmin, c.InstrumentationAttributes(),
			"Combination of same and different keys should be merged.")
	})

	t.Run("MergedEmpty", func(t *testing.T) {
		c := metric.NewMeterConfig(
			metric.WithInstrumentationAttributes(aliceAttr),
			metric.WithInstrumentationAttributes(),
		)
		assert.Equal(t, alice, c.InstrumentationAttributes(),
			"Empty attributes should not affect existing ones.")
	})

	t.Run("SameKeyWithSet", func(t *testing.T) {
		c := metric.NewMeterConfig(
			metric.WithInstrumentationAttributeSet(alice),
			metric.WithInstrumentationAttributeSet(bob),
		)
		assert.Equal(t, bob, c.InstrumentationAttributes(),
			"Later values for the same key should overwrite earlier ones.")
	})

	t.Run("DifferentKeysWithSet", func(t *testing.T) {
		c := metric.NewMeterConfig(
			metric.WithInstrumentationAttributeSet(alice),
			metric.WithInstrumentationAttributeSet(attribute.NewSet(adminAttr)),
		)
		assert.Equal(t, aliceAdmin, c.InstrumentationAttributes(),
			"Different keys should be merged.")
	})

	t.Run("MixedWithSet", func(t *testing.T) {
		c := metric.NewMeterConfig(
			metric.WithInstrumentationAttributeSet(aliceAdmin),
			metric.WithInstrumentationAttributeSet(bob),
		)
		assert.Equal(t, bobAdmin, c.InstrumentationAttributes(),
			"Combination of same and different keys should be merged.")
	})

	t.Run("MergedEmptyWithSet", func(t *testing.T) {
		c := metric.NewMeterConfig(
			metric.WithInstrumentationAttributeSet(alice),
			metric.WithInstrumentationAttributeSet(attribute.NewSet()),
		)
		assert.Equal(t, alice, c.InstrumentationAttributes(),
			"Empty attribute set should not affect existing ones.")
	})

	t.Run("MixedAttributesAndSet", func(t *testing.T) {
		c := metric.NewMeterConfig(
			metric.WithInstrumentationAttributes(aliceAttr),
			metric.WithInstrumentationAttributeSet(attribute.NewSet(bobAttr, adminAttr)),
		)
		assert.Equal(t, bobAdmin, c.InstrumentationAttributes(),
			"Attributes and attribute sets should be merged together.")
	})
}

func BenchmarkNewMeterConfig(b *testing.B) {
	for _, bb := range []struct {
		name    string
		options []metric.MeterOption
	}{
		{
			name: "with no options",
		},
		{
			name: "with an instrumentation version",
			options: []metric.MeterOption{
				metric.WithInstrumentationVersion("testing version"),
			},
		},
		{
			name: "with a schema url",
			options: []metric.MeterOption{
				metric.WithSchemaURL("testing URL"),
			},
		},
		{
			name: "with instrumentation attribute",
			options: []metric.MeterOption{
				metric.WithInstrumentationAttributes(attribute.String("key", "value")),
			},
		},
		{
			name: "with instrumentation attribute set",
			options: []metric.MeterOption{
				metric.WithInstrumentationAttributeSet(attribute.NewSet(attribute.String("key", "value"))),
			},
		},
	} {
		b.Run(bb.name, func(b *testing.B) {
			b.ReportAllocs()
			b.ResetTimer()

			for b.Loop() {
				metric.NewMeterConfig(bb.options...)
			}
		})
	}
}

type testExperimentalOption struct {
	metric.MeterOption
	metric.Int64CounterOption
	metric.Int64UpDownCounterOption
	metric.Int64HistogramOption
	metric.Int64GaugeOption
	metric.Float64CounterOption
	metric.Float64UpDownCounterOption
	metric.Float64HistogramOption
	metric.Float64GaugeOption
	metric.Int64ObservableCounterOption
	metric.Int64ObservableUpDownCounterOption
	metric.Int64ObservableGaugeOption
	metric.Float64ObservableCounterOption
	metric.Float64ObservableUpDownCounterOption
	metric.Float64ObservableGaugeOption
	metric.AddOption
	metric.RecordOption
	metric.ObserveOption
}

func (testExperimentalOption) Experimental() {}

func TestExperimentalOptionSafe(t *testing.T) {
	var opt testExperimentalOption

	assert.NotPanics(t, func() { _ = metric.NewMeterConfig(opt) })
	assert.NotPanics(t, func() { _ = metric.NewInt64CounterConfig(opt) })
	assert.NotPanics(t, func() { _ = metric.NewInt64UpDownCounterConfig(opt) })
	assert.NotPanics(t, func() { _ = metric.NewInt64HistogramConfig(opt) })
	assert.NotPanics(t, func() { _ = metric.NewInt64GaugeConfig(opt) })
	assert.NotPanics(t, func() { _ = metric.NewFloat64CounterConfig(opt) })
	assert.NotPanics(t, func() { _ = metric.NewFloat64UpDownCounterConfig(opt) })
	assert.NotPanics(t, func() { _ = metric.NewFloat64HistogramConfig(opt) })
	assert.NotPanics(t, func() { _ = metric.NewFloat64GaugeConfig(opt) })
	assert.NotPanics(t, func() { _ = metric.NewInt64ObservableCounterConfig(opt) })
	assert.NotPanics(t, func() { _ = metric.NewInt64ObservableUpDownCounterConfig(opt) })
	assert.NotPanics(t, func() { _ = metric.NewInt64ObservableGaugeConfig(opt) })
	assert.NotPanics(t, func() { _ = metric.NewFloat64ObservableCounterConfig(opt) })
	assert.NotPanics(t, func() { _ = metric.NewFloat64ObservableUpDownCounterConfig(opt) })
	assert.NotPanics(t, func() { _ = metric.NewFloat64ObservableGaugeConfig(opt) })
	assert.NotPanics(t, func() { _ = metric.NewAddConfig([]metric.AddOption{opt}) })
	assert.NotPanics(t, func() { _ = metric.NewRecordConfig([]metric.RecordOption{opt}) })
	assert.NotPanics(t, func() { _ = metric.NewObserveConfig([]metric.ObserveOption{opt}) })
}
