package metrics

import (
	"fmt"
	"testing"
	"time"

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

func TestKeyID(t *testing.T) {
	id1 := KeyID("test-key-1")
	id2 := KeyID("test-key-1")
	if id1 != id2 {
		t.Error("KeyID should be deterministic")
	}

	id3 := KeyID("test-key-2")
	if id1 == id3 {
		t.Error("different keys should produce different IDs")
	}

	if len(id1) != 64 {
		t.Errorf("KeyID length = %d, want 64 (SHA256 hex)", len(id1))
	}
}

func TestMaskAPIKey(t *testing.T) {
	tests := []struct {
		input string
		want  string
	}{
		{"short", "short****"},
		{"1234567890", "1234567890****"},
		{"12345678901", "1234567890****"},
		{"long key with more than 10 chars", "long key w****"},
	}

	for _, tt := range tests {
		t.Run(tt.input, func(t *testing.T) {
			got := MaskAPIKey(tt.input)
			if got != tt.want {
				t.Errorf("MaskAPIKey(%q) = %q, want %q", tt.input, got, tt.want)
			}
		})
	}
}

func TestTimeWindow_Duration(t *testing.T) {
	tests := []struct {
		tw   TimeWindow
		want time.Duration
	}{
		{TimeWindow6Hours, 6 * time.Hour},
		{TimeWindow12Hours, 12 * time.Hour},
		{TimeWindow1Day, 24 * time.Hour},
		{TimeWindow3Days, 72 * time.Hour},
		{TimeWindow1Week, 168 * time.Hour},
	}

	for _, tt := range tests {
		got := tt.tw.Duration()
		if got != tt.want {
			t.Errorf("TimeWindow%d.Duration() = %q, want %q", int(tt.tw), got, tt.want)
		}
	}
}

func TestTimeWindow_String(t *testing.T) {
	tests := []struct {
		tw   TimeWindow
		want string
	}{
		{TimeWindow6Hours, "6h"},
		{TimeWindow12Hours, "12h"},
		{TimeWindow1Day, "1d"},
		{TimeWindow3Days, "3d"},
		{TimeWindow1Week, "1w"},
		{TimeWindow(99), "unknown"},
	}

	for _, tt := range tests {
		if got := tt.tw.String(); got != tt.want {
			t.Errorf("TimeWindow%d.String() = %q, want %q", int(tt.tw), got, tt.want)
		}
	}
}

func TestErrorType_String(t *testing.T) {
	if got := ErrorTypeAuth.String(); got != "auth" {
		t.Errorf("ErrorTypeAuth.String() = %q, want %q", got, "auth")
	}
}

func TestAllErrorTypes(t *testing.T) {
	types := AllErrorTypes()
	if len(types) != 8 {
		t.Errorf("AllErrorTypes() length = %d, want 8", len(types))
	}
	for _, et := range types {
		if et.Type == "" {
			t.Error("error type should not be empty")
		}
		if et.Label == "" {
			t.Error("label should not be empty")
		}
	}
}

func TestAggregationInterval(t *testing.T) {
	tests := []struct {
		tw   TimeWindow
		want string
	}{
		{TimeWindow6Hours, "5m0s"},
		{TimeWindow12Hours, "10m0s"},
		{TimeWindow1Day, "15m0s"},
		{TimeWindow3Days, "30m0s"},
		{TimeWindow1Week, "1h0m0s"},
	}

	for _, tt := range tests {
		got := aggregationInterval(tt.tw).String()
		if got != tt.want {
			t.Errorf("aggregationInterval(TimeWindow%d) = %q, want %q", int(tt.tw), got, tt.want)
		}
	}
}

func TestBarAggregationInterval(t *testing.T) {
	tests := []struct {
		tw   TimeWindow
		want string
	}{
		{TimeWindow6Hours, "30m0s"},
		{TimeWindow12Hours, "1h0m0s"},
		{TimeWindow1Day, "2h0m0s"},
		{TimeWindow3Days, "6h0m0s"},
		{TimeWindow1Week, "12h0m0s"},
	}

	for _, tt := range tests {
		got := BarAggregationInterval(tt.tw).String()
		if got != tt.want {
			t.Errorf("BarAggregationInterval(TimeWindow%d) = %q, want %q", int(tt.tw), got, tt.want)
		}
	}
}

func TestMachineDashboardAggregationInterval(t *testing.T) {
	tests := []struct {
		tw   TimeWindow
		want time.Duration
	}{
		{TimeWindow6Hours, 10 * time.Minute},
		{TimeWindow12Hours, 20 * time.Minute},
		{TimeWindow1Day, 30 * time.Minute},
		{TimeWindow3Days, time.Hour},
		{TimeWindow1Week, 2 * time.Hour},
	}
	for _, tt := range tests {
		require.Equal(t, tt.want, MachineDashboardAggregationInterval(tt.tw))
	}
}

func TestMachineDashboardOverviewAggregationInterval(t *testing.T) {
	tests := []struct {
		tw   TimeWindow
		want time.Duration
	}{
		{TimeWindow6Hours, 20 * time.Minute},
		{TimeWindow12Hours, 40 * time.Minute},
		{TimeWindow1Day, time.Hour},
		{TimeWindow3Days, 2 * time.Hour},
		{TimeWindow1Week, 4 * time.Hour},
	}
	for _, tt := range tests {
		require.Equal(t, tt.want, MachineDashboardOverviewAggregationInterval(tt.tw))
		// Overview charts use twice the token-source interval, so the top-of-page
		// charts bucket at half the granularity of the token-source detail.
		require.Equal(t, 2*MachineDashboardAggregationInterval(tt.tw), MachineDashboardOverviewAggregationInterval(tt.tw))
	}
}

func TestMemoryCollectorTimeSeriesCachePrunesExpiredEntries(t *testing.T) {
	now := time.Now()
	mc := &MemoryCollector{
		requestBuckets: make(map[string]map[string]map[int64]*requestBucket),
		tsCache: map[string]*cacheEntry[[]TimeSeriesDataPoint]{
			"expired": {cachedAt: now.Add(-2 * time.Minute)},
			"fresh":   {cachedAt: now},
		},
		cacheTTL: time.Minute,
	}

	mc.queryTimeSeriesCached("new", TimeWindow6Hours, "", "", "")

	require.NotContains(t, mc.tsCache, "expired")
	require.Contains(t, mc.tsCache, "fresh")
	require.Contains(t, mc.tsCache, "new")
}

func TestMemoryCollectorTimeSeriesCacheCapsCardinality(t *testing.T) {
	now := time.Now()
	mc := &MemoryCollector{
		requestBuckets: make(map[string]map[string]map[int64]*requestBucket),
		tsCache:        make(map[string]*cacheEntry[[]TimeSeriesDataPoint], maxTimeSeriesCacheEntries),
		cacheTTL:       time.Hour,
	}
	for i := 0; i < maxTimeSeriesCacheEntries; i++ {
		mc.tsCache[fmt.Sprintf("key-%03d", i)] = &cacheEntry[[]TimeSeriesDataPoint]{
			cachedAt: now.Add(time.Duration(i) * time.Second),
		}
	}

	mc.queryTimeSeriesCached("new", TimeWindow6Hours, "", "", "")

	require.Len(t, mc.tsCache, maxTimeSeriesCacheEntries)
	require.NotContains(t, mc.tsCache, "key-000")
	require.Contains(t, mc.tsCache, "new")
}

func TestMemoryCollectorRecordRequestInvalidatesCachedTimeWindows(t *testing.T) {
	mc := &MemoryCollector{
		requestBuckets: make(map[string]map[string]map[int64]*requestBucket),
		keyMaskMap:     make(map[string]string),
		tsCache:        make(map[string]*cacheEntry[[]TimeSeriesDataPoint]),
		cacheTTL:       time.Hour,
	}

	// Prime the broader window cache before new data arrives. If RecordRequest
	// does not invalidate that cache, the week window can remain stale while the
	// day window recomputes from fresh buckets, making 1w appear smaller than 1d.
	_ = mc.QueryTimeSeries(TimeWindow1Week)

	mc.RecordRequest("sk-test-key", "openai/gpt-4o", 100, 25, 50, 250*time.Millisecond)

	day := sumTimeSeriesCounts(mc.QueryTimeSeries(TimeWindow1Day))
	week := sumTimeSeriesCounts(mc.QueryTimeSeries(TimeWindow1Week))

	require.Equal(t, uint64(1), day)
	require.Equal(t, day, week)
}

func TestMemoryCollectorRecordRequestInvalidatesSummaryCache(t *testing.T) {
	mc := &MemoryCollector{
		requestBuckets: make(map[string]map[string]map[int64]*requestBucket),
		keyMaskMap:     make(map[string]string),
		tsCache:        make(map[string]*cacheEntry[[]TimeSeriesDataPoint]),
		cacheTTL:       time.Hour,
	}

	require.Empty(t, mc.StatsSummary())
	mc.RecordRequest("sk-test-key", "openai/gpt-4o", 100, 25, 50, 250*time.Millisecond)

	var total uint64
	for _, keyStats := range mc.StatsSummary() {
		for _, modelStats := range keyStats.ModelStats {
			total += modelStats.Stats()[TimeWindow1Week].Count
		}
	}

	require.Equal(t, uint64(1), total)
}

func sumTimeSeriesCounts(points []TimeSeriesDataPoint) uint64 {
	var total uint64
	for _, point := range points {
		total += point.Count
	}
	return total
}
