package game

import (
	"context"
	"testing"
	"time"

	"go.opentelemetry.io/otel/attribute"
	sdkmetric "go.opentelemetry.io/otel/sdk/metric"
	"go.opentelemetry.io/otel/sdk/metric/metricdata"

	"scrabble/backend/internal/engine"
)

// TestGameMetrics records each game instrument through a manual reader and asserts
// the counters carry the right "variant" attribute and the histograms observe.
func TestGameMetrics(t *testing.T) {
	ctx := context.Background()
	reader := sdkmetric.NewManualReader()
	meter := sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader)).Meter("test")
	m := newGameMetrics(meter)

	m.recordStarted(ctx, engine.VariantEnglish, false)
	m.recordStarted(ctx, engine.VariantEnglish, false)
	m.recordStarted(ctx, engine.VariantRussianScrabble, false)
	m.recordStarted(ctx, engine.VariantEnglish, true) // an honest-AI game
	m.recordAbandoned(ctx, engine.VariantErudit, false)
	m.recordAbandoned(ctx, engine.VariantEnglish, true) // an AI game abandoned (the 7-day rule)
	m.recordReplay(ctx, engine.VariantEnglish, time.Now().Add(-time.Millisecond))
	m.recordValidate(ctx, engine.VariantRussianScrabble, time.Now().Add(-time.Millisecond))
	m.recordMoveDuration(ctx, engine.VariantEnglish, 3, 5*time.Second)
	m.recordMoveDuration(ctx, engine.VariantEnglish, 3, 0) // non-positive: dropped

	var rm metricdata.ResourceMetrics
	if err := reader.Collect(ctx, &rm); err != nil {
		t.Fatalf("collect: %v", err)
	}

	started := counterByAttr(t, rm, "games_started_total", "variant")
	if started["scrabble_en"] != 3 || started["scrabble_ru"] != 1 {
		t.Errorf("games_started_total = %v, want scrabble_en:3 scrabble_ru:1", started)
	}
	// The vs_ai attribute splits AI from human games for the per-kind Grafana panels.
	if byKind := counterByAttr(t, rm, "games_started_total", "vs_ai"); byKind["false"] != 3 || byKind["true"] != 1 {
		t.Errorf("games_started_total by vs_ai = %v, want false:3 true:1", byKind)
	}
	if abandoned := counterByAttr(t, rm, "games_abandoned_total", "vs_ai"); abandoned["false"] != 1 || abandoned["true"] != 1 {
		t.Errorf("games_abandoned_total by vs_ai = %v, want false:1 true:1", abandoned)
	}
	if c := histogramCount(t, rm, "game_replay_duration"); c != 1 {
		t.Errorf("game_replay_duration observations = %d, want 1", c)
	}
	if c := histogramCount(t, rm, "game_move_validate_duration"); c != 1 {
		t.Errorf("game_move_validate_duration observations = %d, want 1", c)
	}
	if c := histogramCount(t, rm, "game_move_duration"); c != 1 {
		t.Errorf("game_move_duration observations = %d, want 1 (zero-duration dropped)", c)
	}
}

// TestPhaseOf checks the move-ordinal to phase bucketing.
func TestPhaseOf(t *testing.T) {
	cases := map[int]string{1: "opening", 8: "opening", 9: "middle", 20: "middle", 21: "endgame", 50: "endgame"}
	for mc, want := range cases {
		if got := phaseOf(mc); got != want {
			t.Errorf("phaseOf(%d) = %q, want %q", mc, got, want)
		}
	}
}

// counterByAttr sums the int64 counter named name, grouped by the value of the
// attribute key attr.
func counterByAttr(t *testing.T, rm metricdata.ResourceMetrics, name, attr string) map[string]int64 {
	t.Helper()
	out := map[string]int64{}
	for _, sm := range rm.ScopeMetrics {
		for _, md := range sm.Metrics {
			if md.Name != name {
				continue
			}
			sum, ok := md.Data.(metricdata.Sum[int64])
			if !ok {
				t.Fatalf("%s is not an int64 sum", name)
			}
			for _, dp := range sum.DataPoints {
				v, _ := dp.Attributes.Value(attribute.Key(attr))
				out[v.Emit()] += dp.Value // Emit renders any attribute type (string or bool) to a key
			}
		}
	}
	return out
}

// histogramCount returns the total observation count of the float64 histogram
// named name.
func histogramCount(t *testing.T, rm metricdata.ResourceMetrics, name string) uint64 {
	t.Helper()
	for _, sm := range rm.ScopeMetrics {
		for _, md := range sm.Metrics {
			if md.Name != name {
				continue
			}
			h, ok := md.Data.(metricdata.Histogram[float64])
			if !ok {
				t.Fatalf("%s is not a float64 histogram", name)
			}
			var n uint64
			for _, dp := range h.DataPoints {
				n += dp.Count
			}
			return n
		}
	}
	t.Fatalf("%s not found", name)
	return 0
}
