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

package attribute

import (
	"cmp"
	"fmt"
	"math"
	"reflect"
	"slices"
	"strings"
	"testing"

	"go.opentelemetry.io/otel/attribute/internal/xxhash"
)

type keyVal struct {
	name string
	kv   func(string) KeyValue
}

// keyVals is all the KeyValue generators that are used for testing. This is
// not []KeyValue so different keys can be used with the test Values.
var keyVals = []keyVal{
	{name: "BoolTrue", kv: func(k string) KeyValue { return Bool(k, true) }},
	{name: "BoolFalse", kv: func(k string) KeyValue { return Bool(k, false) }},
	{name: "BoolSliceLen0", kv: func(k string) KeyValue { return BoolSlice(k, []bool{}) }},
	{name: "BoolSliceLen1", kv: func(k string) KeyValue { return BoolSlice(k, []bool{true}) }},
	{name: "BoolSliceLen2", kv: func(k string) KeyValue { return BoolSlice(k, []bool{false, true}) }},
	{name: "BoolSliceLen3", kv: func(k string) KeyValue { return BoolSlice(k, []bool{true, true, false}) }},
	{
		name: "BoolSliceLen5",
		kv:   func(k string) KeyValue { return BoolSlice(k, []bool{true, false, true, true, false}) },
	},
	{name: "IntNegative", kv: func(k string) KeyValue { return Int(k, -1278) }},
	{name: "IntZero", kv: func(k string) KeyValue { return Int(k, 0) }}, // Should be different than false above.
	{name: "IntSliceLen5", kv: func(k string) KeyValue { return IntSlice(k, []int{3, 23, 21, -8, 0}) }},
	{name: "IntSliceLen1", kv: func(k string) KeyValue { return IntSlice(k, []int{1}) }},
	{
		name: "Int64One",
		kv:   func(k string) KeyValue { return Int64(k, 1) },
	}, // Should be different from true and []int{1}.
	{name: "Int64", kv: func(k string) KeyValue { return Int64(k, 29369) }},
	{name: "Int64SliceLen4", kv: func(k string) KeyValue { return Int64Slice(k, []int64{3826, -38, -29, -1}) }},
	{name: "Int64SliceWithZero", kv: func(k string) KeyValue { return Int64Slice(k, []int64{8, -328, 29, 0}) }},
	{name: "Int64SliceLen0", kv: func(k string) KeyValue { return Int64Slice(k, []int64{}) }},
	{name: "Int64SliceLen2", kv: func(k string) KeyValue { return Int64Slice(k, []int64{7, -7}) }},
	{name: "Int64SliceLen3", kv: func(k string) KeyValue { return Int64Slice(k, []int64{11, -22, 33}) }},
	{name: "Float64", kv: func(k string) KeyValue { return Float64(k, -0.3812381) }},
	{name: "Float64Large", kv: func(k string) KeyValue { return Float64(k, 1e32) }},
	{
		name: "Float64SliceLen4",
		kv:   func(k string) KeyValue { return Float64Slice(k, []float64{0.1, -3.8, -29., 0.3321}) },
	},
	{
		name: "Float64SliceLarge",
		kv:   func(k string) KeyValue { return Float64Slice(k, []float64{-13e8, -32.8, 4., 1e28}) },
	},
	{name: "Float64SliceLen0", kv: func(k string) KeyValue { return Float64Slice(k, []float64{}) }},
	{name: "Float64SliceLen1", kv: func(k string) KeyValue { return Float64Slice(k, []float64{3.14}) }},
	{name: "Float64SliceLen2", kv: func(k string) KeyValue { return Float64Slice(k, []float64{0.15, -2.5}) }},
	{name: "Float64SliceLen3", kv: func(k string) KeyValue { return Float64Slice(k, []float64{1.5, -2.5, 3.5}) }},
	{name: "StringFoo", kv: func(k string) KeyValue { return String(k, "foo") }},
	{name: "StringBar", kv: func(k string) KeyValue { return String(k, "bar") }},
	{name: "StringSliceLen0", kv: func(k string) KeyValue { return StringSlice(k, []string{}) }},
	{name: "StringSliceLen2", kv: func(k string) KeyValue { return StringSlice(k, []string{"alpha", "beta"}) }},
	{name: "StringSliceLen3", kv: func(k string) KeyValue { return StringSlice(k, []string{"foo", "bar", "baz"}) }},
	{name: "StringSliceLooksLikeIntSlice", kv: func(k string) KeyValue { return StringSlice(k, []string{"[]i1"}) }},
	{
		name: "StringSliceLen5",
		kv:   func(k string) KeyValue { return StringSlice(k, []string{"a", "bb", "ccc", "dddd", "eeeee"}) },
	},
	{name: "ByteSliceFoo", kv: func(k string) KeyValue { return ByteSlice(k, []byte("foo")) }},
	{name: "ByteSliceLooksLikeIntSlice", kv: func(k string) KeyValue { return ByteSlice(k, []byte("[]i1")) }},
	{name: "SliceLen0", kv: func(k string) KeyValue { return Slice(k) }},
	{name: "SliceLen1", kv: func(k string) KeyValue { return Slice(k, BoolValue(true)) }},
	{name: "SliceLen2", kv: func(k string) KeyValue { return Slice(k, BoolValue(true), IntValue(42)) }},
	{name: "SliceLen3", kv: func(k string) KeyValue {
		return Slice(
			k,
			StringValue("triad"),
			IntValue(3),
			BoolValue(false),
		)
	}},
	{name: "SliceLen4", kv: func(k string) KeyValue {
		return Slice(
			k,
			StringValue("quad"),
			IntValue(4),
			BoolValue(false),
			Float64Value(4.25),
		)
	}},
	{name: "SliceLen5", kv: func(k string) KeyValue {
		return Slice(
			k,
			StringValue("penta"),
			IntValue(5),
			BoolValue(true),
			Float64Value(5.5),
			ByteSliceValue([]byte("five")),
		)
	}},
	{name: "SliceNested", kv: func(k string) KeyValue {
		return Slice(
			k,
			StringValue("nested"),
			SliceValue(Float64Value(math.Inf(1)), ByteSliceValue([]byte("bin"))),
			BoolValue(true),
			IntValue(6),
			StringValue("tail"),
			StringSliceValue([]string{"fallback"}),
		)
	}},
	{name: "MapLen0", kv: func(k string) KeyValue { return Map(k) }},
	{name: "MapLen2", kv: func(k string) KeyValue {
		return Map(
			k,
			String("b", "two"),
			Int("a", 1),
		)
	}},
	{name: "MapNested", kv: func(k string) KeyValue {
		return Map(
			k,
			String("nested", "map"),
			Map("child", Float64("f", math.Inf(1)), ByteSlice("bin", []byte("bin"))),
			Slice("slice", BoolValue(true), StringValue("tail")),
		)
	}},
	{name: "EmptyValue", kv: func(k string) KeyValue { return KeyValue{Key: Key(k)} }},
}

func TestHashKVs(t *testing.T) {
	type testcase struct {
		num  int
		hash uint64
		kvs  []KeyValue
	}

	keys := []string{"k0", "k1"}

	// Track hashes as we generate them so collision detection stays linear.
	i := 0
	seen := make(map[uint64]testcase)
	assertUniqueHash := func(kvs []KeyValue) {
		hash := hashKVs(kvs)
		tc := testcase{num: i, hash: hash, kvs: kvs}
		i++

		if prev, ok := seen[hash]; ok {
			t.Errorf("hashes equal: (%d: %d)%s == (%d: %d)%s",
				prev.num, prev.hash, slice(prev.kvs), tc.num, tc.hash, slice(tc.kvs))
			return
		}

		seen[hash] = tc
	}

	// Test empty slice.
	assertUniqueHash(nil)

	// Test all combinations of 1, 2, and 3 attributes with different keys and values.
	for _, key := range keys {
		for i := range keyVals {
			kvs := []KeyValue{keyVals[i].kv(key)}
			assertUniqueHash(kvs)

			for j := range keyVals {
				kvs := []KeyValue{
					keyVals[i].kv(key),
					keyVals[j].kv(key),
				}
				assertUniqueHash(kvs)

				for k := range keyVals {
					kvs := []KeyValue{
						keyVals[i].kv(key),
						keyVals[j].kv(key),
						keyVals[k].kv(key),
					}
					assertUniqueHash(kvs)
				}
			}
		}
	}
}

func TestHashValueMapOrdering(t *testing.T) {
	tests := []struct {
		name string
		kvs  []KeyValue
	}{
		{
			name: "Len4",
			kvs: []KeyValue{
				String("d", "4"),
				String("a", "1"),
				String("c", "3"),
				String("b", "2"),
			},
		},
		{
			name: "Len5",
			kvs: []KeyValue{
				String("e", "5"),
				String("a", "1"),
				String("d", "4"),
				String("b", "2"),
				String("c", "3"),
			},
		},
		{
			name: "Reflect",
			kvs: []KeyValue{
				String("f", "6"),
				String("a", "1"),
				String("e", "5"),
				String("b", "2"),
				String("d", "4"),
				String("c", "3"),
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			reversed := slices.Clone(tt.kvs)
			slices.Reverse(reversed)

			h1 := xxhash.New()
			h1 = hashValue(h1, MapValue(tt.kvs...))
			got := h1.Sum64()
			h2 := xxhash.New()
			h2 = hashValue(h2, MapValue(reversed...))
			want := h2.Sum64()
			if got != want {
				t.Fatalf("hashValue(MapValue(%v)) = %d, want %d", tt.kvs, got, want)
			}
		})
	}
}

// TestHasherMatchesSetEquivalent pins the invariant that a Hasher written in
// ascending key order produces the same Distinct as the equivalent Set. Hasher
// and Set hashing share their initialization, per-attribute mixing, and final
// framing, so this holds structurally today. The test guards against a future
// change that splits those paths apart.
func TestHasherMatchesSetEquivalent(t *testing.T) {
	t.Run("Empty", func(t *testing.T) {
		h := NewHasher()
		set := NewSet()
		if got, want := h.Distinct(), set.Equivalent(); got != want {
			t.Errorf("Hasher.Distinct() = %v, NewSet().Equivalent() = %v", got, want)
		}
	})

	for _, gen := range keyVals {
		t.Run(gen.name, func(t *testing.T) {
			kv := gen.kv("k")
			h := NewHasher()
			h.Write(kv)
			set := NewSet(kv)
			if got, want := h.Distinct(), set.Equivalent(); got != want {
				t.Errorf("Hasher.Distinct() = %v, NewSet(%v).Equivalent() = %v", got, kv, want)
			}
		})
	}

	t.Run("All", func(t *testing.T) {
		// Distinct keys in ascending order, which is the precondition Write
		// documents and the order NewSet sorts into.
		attrs := make([]KeyValue, len(keyVals))
		for i, gen := range keyVals {
			attrs[i] = gen.kv(fmt.Sprintf("k%03d", i))
		}
		h := NewHasher()
		for _, kv := range attrs {
			h.Write(kv)
		}
		set := NewSet(attrs...)
		if got, want := h.Distinct(), set.Equivalent(); got != want {
			t.Errorf("Hasher.Distinct() = %v, NewSet(...).Equivalent() = %v", got, want)
		}
	})
}

func TestHasherReset(t *testing.T) {
	h := NewHasher()
	h.Write(String("a", "1"))
	h.Write(String("b", "2"))
	distinctBefore := h.Distinct()

	h.Reset()
	if got, want := h.Distinct(), emptySet.Equivalent(); got != want {
		t.Errorf("h.Distinct() after Reset = %v, want %v", got, want)
	}

	h.Write(String("a", "1"))
	h.Write(String("b", "2"))
	if got, want := h.Distinct(), distinctBefore; got != want {
		t.Errorf("h.Distinct() after re-write = %v, want %v", got, want)
	}
}

func slice(kvs []KeyValue) string {
	if len(kvs) == 0 {
		return "[]"
	}

	var b strings.Builder
	_, _ = b.WriteRune('[')
	_, _ = b.WriteString(string(kvs[0].Key))
	_, _ = b.WriteRune(':')
	_, _ = b.WriteString(kvs[0].Value.String())
	for _, kv := range kvs[1:] {
		_, _ = b.WriteRune(',')
		_, _ = b.WriteString(string(kv.Key))
		_, _ = b.WriteRune(':')
		_, _ = b.WriteString(kv.Value.String())
	}
	_, _ = b.WriteRune(']')
	return b.String()
}

func BenchmarkHashKVs(b *testing.B) {
	attrs := make([]KeyValue, len(keyVals))
	for i := range keyVals {
		attrs[i] = keyVals[i].kv("k")
	}

	benches := []struct {
		name string
		kvs  []KeyValue
	}{
		{
			name: "All",
			kvs:  attrs,
		},
	}
	for _, gen := range keyVals {
		benches = append(benches, struct {
			name string
			kvs  []KeyValue
		}{
			name: gen.name,
			kvs:  []KeyValue{gen.kv("k")},
		})
	}

	for _, bench := range benches {
		b.Run(bench.name, func(b *testing.B) {
			b.ReportAllocs()
			for b.Loop() {
				hashKVs(bench.kvs)
			}
		})
	}
}

func BenchmarkHashValueSlice(b *testing.B) {
	benches := []struct {
		name string
		v    Value
	}{
		{
			name: "Len2",
			v: SliceValue(
				BoolValue(true),
				StringValue("two"),
			),
		},
		{
			name: "Len5",
			v: SliceValue(
				BoolValue(true),
				IntValue(2),
				StringValue("three"),
				Float64Value(4.5),
				ByteSliceValue([]byte("five")),
			),
		},
		{
			name: "Len8Nested",
			v: SliceValue(
				BoolValue(true),
				IntValue(2),
				StringValue("three"),
				Float64Value(4.5),
				ByteSliceValue([]byte("five")),
				SliceValue(StringValue("nested"), Int64Value(6)),
				BoolSliceValue([]bool{true, false, true}),
				StringSliceValue([]string{"seven", "eight"}),
			),
		},
	}

	for _, bench := range benches {
		b.Run(bench.name, func(b *testing.B) {
			b.ReportAllocs()
			for b.Loop() {
				h := xxhash.New()
				h = hashValue(h, bench.v)
				_ = h.Sum64()
			}
		})
	}
}

func BenchmarkHashValueMap(b *testing.B) {
	benches := []struct {
		name string
		v    Value
	}{
		{
			name: "Len2",
			v: MapValue(
				Bool("two", true),
				String("one", "one"),
			),
		},
		{
			name: "Len5",
			v: MapValue(
				Bool("one", true),
				Int("two", 2),
				String("three", "three"),
				Float64("four", 4.5),
				ByteSlice("five", []byte("five")),
			),
		},
		{
			name: "Len8Nested",
			v: MapValue(
				Bool("one", true),
				Int("two", 2),
				String("three", "three"),
				Float64("four", 4.5),
				ByteSlice("five", []byte("five")),
				Map("six", String("nested", "map"), Int64("value", 6)),
				Slice("seven", StringValue("nested"), BoolValue(true)),
				StringSlice("eight", []string{"seven", "eight"}),
			),
		},
	}

	for _, bench := range benches {
		b.Run(bench.name, func(b *testing.B) {
			b.ReportAllocs()
			for b.Loop() {
				h := xxhash.New()
				h = hashValue(h, bench.v)
				_ = h.Sum64()
			}
		})
	}
}

func FuzzHashKVs(f *testing.F) {
	// Add seed inputs to ensure coverage of edge cases.
	f.Add("", "", "", "", "", "", 0, int64(0), 0.0, false, uint8(0))
	f.Add("key", "value", "🌍", "test", "bool", "float", -1, int64(-1), -1.0, true, uint8(1))
	f.Add("duplicate", "duplicate", "duplicate", "duplicate", "duplicate", "NaN",
		0, int64(0), math.Inf(1), false, uint8(2))

	f.Fuzz(func(t *testing.T, k1, k2, k3, k4, k5, s string, i int, i64 int64, fVal float64, b bool, sliceType uint8) {
		// Test variable number of attributes (0-11).
		numAttrs := len(k1) % 12 // Use key length to determine number of attributes.
		if numAttrs == 0 && k1 == "" {
			// Test empty set.
			h := hashKVs(nil)
			if h == 0 {
				t.Error("hash of empty slice should not be zero")
			}
			return
		}

		var kvs []KeyValue

		// Add basic types.
		if numAttrs > 0 {
			kvs = append(kvs, String(k1, s))
		}
		if numAttrs > 1 {
			kvs = append(kvs, Int(k2, i))
		}
		if numAttrs > 2 {
			kvs = append(kvs, Int64(k3, i64))
		}
		if numAttrs > 3 {
			kvs = append(kvs, Float64(k4, fVal))
		}
		if numAttrs > 4 {
			kvs = append(kvs, Bool(k5, b))
		}

		// Add slice and composite types based on sliceType parameter.
		if numAttrs > 5 {
			switch sliceType % 7 {
			case 0:
				// Test BoolSlice with variable length.
				bools := make([]bool, len(s)%5) // 0-4 elements
				for i := range bools {
					bools[i] = (i+len(k1))%2 == 0
				}
				kvs = append(kvs, BoolSlice("boolslice", bools))
			case 1:
				// Test IntSlice with variable length.
				ints := make([]int, len(s)%6) // 0-5 elements
				for i := range ints {
					ints[i] = i + len(k2)
				}
				kvs = append(kvs, IntSlice("intslice", ints))
			case 2:
				// Test Int64Slice with variable length.
				int64s := make([]int64, len(s)%4) // 0-3 elements
				for i := range int64s {
					int64s[i] = int64(i) + i64
				}
				kvs = append(kvs, Int64Slice("int64slice", int64s))
			case 3:
				// Test Float64Slice with variable length and special values.
				float64s := make([]float64, len(s)%5) // 0-4 elements
				for i := range float64s {
					switch i % 4 {
					case 0:
						float64s[i] = fVal
					case 1:
						float64s[i] = math.Inf(1) // +Inf
					case 2:
						float64s[i] = math.Inf(-1) // -Inf
					case 3:
						float64s[i] = math.NaN() // NaN
					}
				}
				kvs = append(kvs, Float64Slice("float64slice", float64s))
			case 4:
				// Test ByteSlice with variable length.
				bytes := make([]byte, len(s)%5)
				for i := range bytes {
					bytes[i] = byte(i + len(k1))
				}
				kvs = append(kvs, ByteSlice("bytes", bytes))
			case 5:
				values := make([]Value, len(s)%4) // 0-3 elements
				for i := range values {
					switch i % 4 {
					case 0:
						values[i] = BoolValue((i+len(k1))%2 == 0)
					case 1:
						values[i] = IntValue(i + len(k2))
					case 2:
						values[i] = StringValue(fmt.Sprintf("item_%d", i))
					case 3:
						values[i] = SliceValue(Float64Value(fVal), ByteSliceValue([]byte("bin")))
					}
				}
				kvs = append(kvs, Slice("slice", values...))
			case 6:
				values := make([]KeyValue, len(s)%4) // 0-3 elements
				for i := range values {
					key := fmt.Sprintf("item_%d", i)
					switch i % 4 {
					case 0:
						values[i] = Bool(key, (i+len(k1))%2 == 0)
					case 1:
						values[i] = Int(key, i+len(k2))
					case 2:
						values[i] = String(key, fmt.Sprintf("item_%d", i))
					case 3:
						values[i] = Map(key, Float64("float", fVal), ByteSlice("bin", []byte("bin")))
					}
				}
				kvs = append(kvs, Map("map", values...))
			}
		}

		// Add StringSlice.
		if numAttrs > 6 {
			strings := make([]string, len(k1)%4) // 0-3 elements
			for i := range strings {
				strings[i] = fmt.Sprintf("%s_%d", s, i)
			}
			kvs = append(kvs, StringSlice("stringslice", strings))
		}

		// Test duplicate keys (should be handled by Set construction).
		if numAttrs > 7 && k1 != "" {
			kvs = append(kvs, String(k1, "duplicate_key_value"))
		}

		// Add more attributes with Unicode keys.
		if numAttrs > 8 {
			kvs = append(kvs, String("🔑", "unicode_key"))
		}
		if numAttrs > 9 {
			kvs = append(kvs, String("empty", ""))
		}

		// Add empty value.
		if numAttrs > 10 {
			kvs = append(kvs, KeyValue{Key: Key("empty_value")})
		}

		// Sort to ensure consistent ordering (as Set would do).
		slices.SortFunc(kvs, func(a, b KeyValue) int {
			return cmp.Compare(string(a.Key), string(b.Key))
		})

		// Remove duplicates (as Set will do).
		if len(kvs) > 1 {
			j := 0
			for i := 1; i < len(kvs); i++ {
				if kvs[j].Key != kvs[i].Key {
					j++
					kvs[j] = kvs[i]
				} else {
					// Keep the later value for duplicate keys.
					kvs[j] = kvs[i]
				}
			}
			kvs = kvs[:j+1]
		}

		// Hash the key-value pairs.
		h1 := hashKVs(kvs)
		h2 := hashKVs(kvs) // Should be deterministic

		if h1 != h2 {
			t.Errorf("hash is not deterministic: %d != %d for kvs=%v", h1, h2, kvs)
		}

		if h1 == 0 && len(kvs) > 0 {
			t.Errorf("hash should not be zero for non-empty input: kvs=%v", kvs)
		}

		// Test that different inputs produce different hashes (most of the time).
		// This is a probabilistic test - collisions are possible but rare.
		if len(kvs) > 0 {
			// Modify one value slightly.
			modifiedKvs := make([]KeyValue, len(kvs))
			copy(modifiedKvs, kvs)
			if len(modifiedKvs) > 0 {
				switch modifiedKvs[0].Value.Type() {
				case STRING:
					modifiedKvs[0] = String(string(modifiedKvs[0].Key), modifiedKvs[0].Value.AsString()+"_modified")
				case INT64:
					modifiedKvs[0] = Int64(string(modifiedKvs[0].Key), modifiedKvs[0].Value.AsInt64()+1)
				case BOOL:
					modifiedKvs[0] = Bool(string(modifiedKvs[0].Key), !modifiedKvs[0].Value.AsBool())
				case FLOAT64:
					val := modifiedKvs[0].Value.AsFloat64()
					if !math.IsNaN(val) && !math.IsInf(val, 0) {
						modifiedKvs[0] = Float64(string(modifiedKvs[0].Key), val+1.0)
					}
				case SLICE:
					origSlice := modifiedKvs[0].Value.AsSlice()
					if len(origSlice) > 0 {
						newSlice := slices.Clone(origSlice)
						switch newSlice[0].Type() {
						case INT64:
							newSlice[0] = Int64Value(newSlice[0].AsInt64() + 1)
						case BOOL:
							newSlice[0] = BoolValue(!newSlice[0].AsBool())
						case STRING:
							newSlice[0] = StringValue(newSlice[0].AsString() + "_mod")
						default:
							newSlice[0] = StringValue("modified")
						}
						modifiedKvs[0] = Slice(string(modifiedKvs[0].Key), newSlice...)
					}
				case MAP:
					origMap := modifiedKvs[0].Value.AsMap()
					if len(origMap) > 0 {
						newMap := slices.Clone(origMap)
						newMap[0] = String(string(newMap[0].Key), "modified")
						modifiedKvs[0] = Map(string(modifiedKvs[0].Key), newMap...)
					}
				case EMPTY:
					modifiedKvs[0] = String(string(modifiedKvs[0].Key), "not_empty")
				}

				h3 := hashKVs(modifiedKvs)
				// Note: We don't assert h1 != h3 because hash collisions are theoretically possible
				// but we can log suspicious cases for manual review.
				if h1 == h3 && !reflect.DeepEqual(kvs, modifiedKvs) {
					t.Logf("Potential hash collision detected: original=%v, modified=%v, hash=%d", kvs, modifiedKvs, h1)
				}
			}
		}
	})
}

func BenchmarkHasher(b *testing.B) {
	kvs := []KeyValue{
		String("A", "alpha"),
		Int64("B", 42),
		Bool("C", true),
	}
	b.ReportAllocs()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		h := NewHasher()
		for _, kv := range kvs {
			h.Write(kv)
		}
		_ = h.Distinct()
	}
}
