package observability

import (
	"sync"
	"sync/atomic"
	"testing"
)

func TestMaxSeen_FirstObservationExports(t *testing.T) {
	m := NewMaxSeen()
	var got string
	var val float64
	var calls int
	m.RecordAndExport("src-A", 1, func(k string, v float64) {
		got = k
		val = v
		calls++
	})
	if calls != 1 || got != "src-A" || val != 1 {
		t.Fatalf("first: want 1 export src-A=1, got calls=%d key=%q val=%v", calls, got, val)
	}
	if m.Get("src-A") != 1 {
		t.Fatalf("Get: want 1, got %d", m.Get("src-A"))
	}
}

func TestMaxSeen_StrictlyMonotonic(t *testing.T) {
	m := NewMaxSeen()
	var calls int
	exp := func(string, float64) { calls++ }
	m.RecordAndExport("src-A", 5, exp)
	m.RecordAndExport("src-A", 3, exp) // smaller: should not export
	m.RecordAndExport("src-A", 5, exp) // equal: should not export
	m.RecordAndExport("src-A", 7, exp) // larger: should export
	m.RecordAndExport("src-A", 2, exp) // smaller: should not export
	m.RecordAndExport("src-A", 10, exp) // larger: should export

	if calls != 3 {
		t.Fatalf("want 3 exports (1, 7, 10), got %d", calls)
	}
	if m.Get("src-A") != 10 {
		t.Fatalf("Get: want 10, got %d", m.Get("src-A"))
	}
}

func TestMaxSeen_PerKeyIsolation(t *testing.T) {
	m := NewMaxSeen()
	exp := func(string, float64) {}
	m.RecordAndExport("src-A", 100, exp)
	m.RecordAndExport("src-B", 5, exp)
	if m.Get("src-A") != 100 {
		t.Fatalf("src-A: want 100, got %d", m.Get("src-A"))
	}
	if m.Get("src-B") != 5 {
		t.Fatalf("src-B: want 5, got %d", m.Get("src-B"))
	}
	if m.Get("src-C") != 0 {
		t.Fatalf("src-C (unseen): want 0, got %d", m.Get("src-C"))
	}
}

func TestMaxSeen_ConcurrentSameKey(t *testing.T) {
	m := NewMaxSeen()
	var exports atomic.Int64
	exp := func(string, float64) { exports.Add(1) }
	var wg sync.WaitGroup
	for g := 0; g < 8; g++ {
		wg.Add(1)
		go func(g int) {
			defer wg.Done()
			for v := 1; v <= 1000; v++ {
				// Each goroutine uses a different value range
				// so we know the global max is 8*1000 - 7 = 7993
				// (8 goroutines, each writes its own values
				// shifted by g*1000). Just write all of them.
				_ = g
				m.RecordAndExport("src", uint32(v), exp)
			}
		}(g)
	}
	wg.Wait()
	// We don't assert on the number of exports (it can vary
	// based on CAS contention); we assert that the final
	// observed max is the actual global max written.
	if m.Get("src") != 1000 {
		t.Fatalf("final max: want 1000, got %d", m.Get("src"))
	}
	if exports.Load() < 1 {
		t.Fatalf("expected ≥1 export, got %d", exports.Load())
	}
}

func TestMaxSeen_NilExportIsNoOp(t *testing.T) {
	m := NewMaxSeen()
	// Should not panic.
	m.RecordAndExport("src-A", 1, nil)
	m.RecordAndExport("src-A", 5, nil)
	if m.Get("src-A") != 5 {
		t.Fatalf("nil export should still update state; got %d", m.Get("src-A"))
	}
}
