package game

import (
	"math"
	"testing"
	"time"

	"github.com/denisovdennis/autohero/internal/model"
	"github.com/denisovdennis/autohero/internal/tuning"
)

func TestBandIndexForHeroLevel(t *testing.T) {
	cases := []struct {
		L    int
		want int
	}{
		{1, 0}, {9, 0},
		{10, 1}, {19, 1},
		{20, 2}, {29, 2},
		{30, 3}, {39, 3},
		{40, 4}, {49, 4},
		{0, -1}, {50, -1},
	}
	for _, tc := range cases {
		if got := bandIndexForHeroLevel(tc.L); got != tc.want {
			t.Fatalf("bandIndexForHeroLevel(%d)=%d want %d", tc.L, got, tc.want)
		}
	}
}

func TestTemplateProgressionBand(t *testing.T) {
	if TemplateProgressionBand(model.Enemy{MinLevel: 1, MaxLevel: 5, BaseLevel: 1}) != 0 {
		t.Fatal("low-tier template should be band 0")
	}
	// Midpoint (25+35)/2 = 30 → tier band index 2 (see TemplateProgressionBand).
	if TemplateProgressionBand(model.Enemy{MinLevel: 25, MaxLevel: 35, BaseLevel: 25}) != 2 {
		t.Fatalf("mid 30 should map to template band 2, got %d", TemplateProgressionBand(model.Enemy{MinLevel: 25, MaxLevel: 35}))
	}
	if TemplateProgressionBand(model.Enemy{MinLevel: 41, MaxLevel: 45, BaseLevel: 41}) != 4 {
		t.Fatalf("late template should map to band 4, got %d", TemplateProgressionBand(model.Enemy{MinLevel: 41, MaxLevel: 45}))
	}
}

func TestAggregateBandDurationsPure(t *testing.T) {
	// One second per level-up; 9+10+10+10+10 = 49 levels
	sec := make([]float64, 49)
	for i := range sec {
		sec[i] = 1
	}
	var band [5]time.Duration
	for idx, L := range levelRange(1, 49) {
		bi := bandIndexForHeroLevel(L)
		if bi >= 0 {
			band[bi] += time.Duration(sec[idx] * float64(time.Second))
		}
	}
	if band[0] != 9*time.Second {
		t.Fatalf("band0: %v", band[0])
	}
	for i := 1; i <= 4; i++ {
		if band[i] != 10*time.Second {
			t.Fatalf("band %d: %v", i, band[i])
		}
	}
}

func TestGlobalScaleMonotonicTotalTime(t *testing.T) {
	ensureTestEnemyTemplates()
	cfg := tuning.DefaultValues()
	tuning.Set(cfg)
	t.Cleanup(func() { tuning.Set(tuning.DefaultValues()) })

	base := CloneEnemyTemplates(EnemyTemplatesFromSlice(model.EnemyTemplates))
	params := ProgressionSimParams{
		IterationsPerLevel:      6,
		Seed:                  42,
		RestAfterCombat:       10 * time.Second,
		Gear:                  ReferenceGearMedian,
		AccountLosses:         false,
		MinHeroLevel:          1,
		MaxHeroLevelInclusive: 15,
	}

	slow := ApplyXPRewardScaleSpec(base, XPRewardScaleSpec{Global: 0.2, Elite: 1, PerBand: [5]float64{1, 1, 1, 1, 1}})
	fast := ApplyXPRewardScaleSpec(base, XPRewardScaleSpec{Global: 5.0, Elite: 1, PerBand: [5]float64{1, 1, 1, 1, 1}})

	rSlow, err := SimulateProgressionBands(params, slow)
	if err != nil {
		t.Fatal(err)
	}
	rFast, err := SimulateProgressionBands(params, fast)
	if err != nil {
		t.Fatal(err)
	}
	if rSlow.Total <= rFast.Total {
		t.Fatalf("expected higher XP scale to reduce total time: slow=%s fast=%s", rSlow.Total, rFast.Total)
	}
}

func TestProratedBandTargets(t *testing.T) {
	full := DefaultProgressionBandTargets
	// Through L29: full bands 0,1,2 → 1+3+6 weeks
	pr := ProratedBandTargets(29, full)
	sum := SumBandTargets(pr)
	wantWeeks := 10 // 1+3+6
	if got := int(sum / (7 * 24 * time.Hour)); got != wantWeeks {
		t.Fatalf("ProratedBandTargets(29) sum weeks=%d want %d (%v)", got, wantWeeks, pr)
	}
	// Through L9: only band 0 full
	pr2 := ProratedBandTargets(9, full)
	if SumBandTargets(pr2) != full[0] {
		t.Fatalf("band0 only: %v", pr2)
	}
}

func TestEnforceMonotonicXPRewardByTier(t *testing.T) {
	m := map[string]model.Enemy{
		"a": {Slug: "a", MinLevel: 1, MaxLevel: 5, XPReward: 10},
		"b": {Slug: "b", MinLevel: 20, MaxLevel: 30, XPReward: 5},
	}
	out := EnforceMonotonicXPRewardByTier(m)
	if out["b"].XPReward <= out["a"].XPReward {
		t.Fatalf("higher tier should have strictly higher xp: a=%d b=%d", out["a"].XPReward, out["b"].XPReward)
	}
}

func TestOptimizePerTypeScalesRuns(t *testing.T) {
	ensureTestEnemyTemplates()
	cfg := tuning.DefaultValues()
	tuning.Set(cfg)
	t.Cleanup(func() { tuning.Set(tuning.DefaultValues()) })
	base := CloneEnemyTemplates(EnemyTemplatesFromSlice(model.EnemyTemplates))
	params := ProgressionSimParams{
		IterationsPerLevel:      3,
		Seed:                  1,
		RestAfterCombat:       5 * time.Second,
		Gear:                  ReferenceGearMedian,
		MinHeroLevel:          1,
		MaxHeroLevelInclusive: 6,
	}
	targets := ProratedBandTargets(6, DefaultProgressionBandTargets)
	m, _, res, sq := OptimizePerTypeScales(base, params, targets, 1, 6, false)
	if len(m) != len(base) {
		t.Fatalf("len(perType)=%d len(base)=%d", len(m), len(base))
	}
	if math.IsNaN(sq) {
		t.Fatal("sqErr nan")
	}
	if math.IsInf(res.TotalSec, 1) {
		t.Skip("combat unwinnable in testdata")
	}
}

func TestSquaredErrorSum(t *testing.T) {
	sim := [5]time.Duration{10 * time.Second, 20 * time.Second, 30 * time.Second, 40 * time.Second, 50 * time.Second}
	tg := [5]time.Duration{10 * time.Second, 10 * time.Second, 10 * time.Second, 10 * time.Second, 10 * time.Second}
	if SquaredErrorSum(sim, tg) <= 0 {
		t.Fatal("expected positive error")
	}
}
