package game

import (
	"testing"
	"time"

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

func TestOfflineDigestCollecting(t *testing.T) {
	now := time.Date(2026, 4, 1, 12, 0, 0, 0, time.UTC)
	recent := now.Add(-20 * time.Second)
	if OfflineDigestCollecting(&recent, now) {
		t.Error("expected false before grace window")
	}
	old := now.Add(-2 * time.Minute)
	if !OfflineDigestCollecting(&old, now) {
		t.Error("expected true after grace window")
	}
	if OfflineDigestCollecting(nil, now) {
		t.Error("expected false when disconnect time is nil")
	}
}

func TestSimulateOneFight_HeroSurvives(t *testing.T) {
	hero := &model.Hero{
		Level: 1, XP: 0,
		MaxHP: 10000, HP: 10000,
		Attack: 100, Defense: 60, Speed: 1.0,
		Strength: 10, Constitution: 10, Agility: 10, Luck: 5,
		State: model.StateWalking,
	}

	now := time.Now()
	survived, enemy, xpGained, goldGained, _ := SimulateOneFight(hero, now, nil, nil, 100*time.Millisecond, VictoryRewardDeps{})

	if !survived {
		t.Fatalf("overpowered hero should survive, enemy was %s", enemy.Name)
	}
	if xpGained <= 0 {
		t.Fatal("expected positive XP gain")
	}
	if goldGained <= 0 {
		t.Fatal("expected positive gold gain")
	}
	if enemy.Name == "" {
		t.Fatal("expected enemy with a name")
	}
}

func TestSimulateOneFight_HeroDies(t *testing.T) {
	hero := &model.Hero{
		Level: 1, XP: 0,
		MaxHP: 1, HP: 1,
		Attack: 1, Defense: 0, Speed: 1.0,
		State: model.StateWalking,
	}

	now := time.Now()
	survived, _, _, _, _ := SimulateOneFight(hero, now, nil, nil, 100*time.Millisecond, VictoryRewardDeps{})

	if survived {
		t.Fatal("1 HP hero should die to any enemy")
	}
	if hero.HP != 0 {
		t.Fatalf("expected HP 0 after death, got %d", hero.HP)
	}
	if hero.State != model.StateDead {
		t.Fatalf("expected state dead, got %s", hero.State)
	}
}

func TestSimulateOneFight_LevelUp(t *testing.T) {
	// Seed XP just below L1->L2 threshold (100 XP with default tuning).
	hero := &model.Hero{
		Level: 1, XP: 99,
		MaxHP: 10000, HP: 10000,
		Attack: 100, Defense: 60, Speed: 1.0,
		Strength: 10, Constitution: 10, Agility: 10, Luck: 5,
		State: model.StateWalking,
	}

	now := time.Now()
	survived, _, xpGained, _, _ := SimulateOneFight(hero, now, nil, nil, 100*time.Millisecond, VictoryRewardDeps{})

	if !survived {
		t.Fatal("overpowered hero should survive")
	}
	if xpGained <= 0 {
		t.Fatal("expected XP gain")
	}
	if hero.Level < 2 {
		t.Fatalf("expected level 2+ after gaining %d XP from 99 base, got level %d", xpGained, hero.Level)
	}
}

func TestBuildEnemyInstanceForLevel_XPPerLevelRampsFrom10(t *testing.T) {
	tmpl := model.Enemy{
		BaseLevel:  1,
		XPReward:   1,
		XPPerLevel: 4,
		IsElite:    false,
	}
	early := BuildEnemyInstanceForLevel(tmpl, 6, nil)
	if early.XPReward != 1 {
		t.Fatalf("normal mob instance L6: want base XP only (no per-level ramp), got %d", early.XPReward)
	}
	mid := BuildEnemyInstanceForLevel(tmpl, 12, nil)
	if mid.XPReward <= 1 {
		t.Fatalf("normal mob instance L12: want xp_per_level applied, got %d", mid.XPReward)
	}
	elite := tmpl
	elite.IsElite = true
	el := BuildEnemyInstanceForLevel(elite, 5, nil)
	if el.XPReward <= 1 {
		t.Fatalf("elite instance L5: want xp_per_level even before 10, got %d", el.XPReward)
	}
}

func TestOfflineAutoPotionHook_DoesNotTriggerWhenHealthy(t *testing.T) {
	hero := &model.Hero{
		MaxHP:   100,
		HP:      100,
		Potions: 3,
	}
	if used := OfflineAutoPotionHook(hero, time.Now()); used {
		t.Fatal("expected no potion usage when hero is above threshold")
	}
	if hero.Potions != 3 {
		t.Fatalf("expected potions unchanged, got %d", hero.Potions)
	}
}

func TestNonGoldLootForDigest(t *testing.T) {
	drops := []model.LootDrop{
		{ItemType: "gold", Rarity: model.RarityCommon, GoldAmount: 10},
		{ItemType: "potion", Rarity: model.RarityCommon},
		{ItemType: "gold", Rarity: model.RarityCommon, GoldAmount: 5},
	}
	out := NonGoldLootForDigest(drops)
	if len(out) != 1 || out[0].ItemType != "potion" {
		t.Fatalf("want single potion line, got %#v", out)
	}
	if NonGoldLootForDigest(nil) != nil {
		t.Fatal("nil in -> nil out")
	}
	if NonGoldLootForDigest([]model.LootDrop{{ItemType: "gold", GoldAmount: 1}}) != nil {
		t.Fatal("gold-only -> nil")
	}
}

func TestBuildEnemyInstanceForLevel_EncounterStatMultiplier(t *testing.T) {
	cfg := tuning.DefaultValues()
	cfg.EnemyEncounterStatMultiplier = 2.0
	tuning.Set(cfg)
	t.Cleanup(func() { tuning.Set(tuning.DefaultValues()) })

	tmpl := model.Enemy{
		BaseLevel: 1,
		MaxHP:     50,
		HP:        50,
		Attack:    10,
		Defense:   4,
	}
	out := BuildEnemyInstanceForLevel(tmpl, 1, nil)
	if out.MaxHP != 100 || out.HP != 100 {
		t.Fatalf("MaxHP/HP: got %d/%d want 100/100", out.MaxHP, out.HP)
	}
	if out.Attack != 20 || out.Defense != 8 {
		t.Fatalf("Attack/Defense: got %d/%d want 20/8", out.Attack, out.Defense)
	}

	scaled := BuildEnemyInstanceForLevelScaledOnly(tmpl, 1)
	if scaled.MaxHP != 50 || scaled.Attack != 10 || scaled.Defense != 4 {
		t.Fatalf("scaled-only: got hp=%d atk=%d def=%d want 50/10/4", scaled.MaxHP, scaled.Attack, scaled.Defense)
	}
	base, afterG := EnemyEncounterStatStages(tmpl, 1)
	if base.MaxHP != 50 || afterG.MaxHP != 100 {
		t.Fatalf("stages MaxHP: base=%d afterGlobal=%d", base.MaxHP, afterG.MaxHP)
	}
	if afterG.Attack != 20 || afterG.Defense != 8 {
		t.Fatalf("stages atk/def: got %d/%d want 20/8", afterG.Attack, afterG.Defense)
	}
}

func TestApplyEnemyEncounterHeroScaling_Unequipped(t *testing.T) {
	cfg := tuning.DefaultValues()
	cfg.EnemyEncounterStatMultiplier = 1.0
	cfg.EnemyStatMultiplierVsUnequippedHero = 0.75
	tuning.Set(cfg)
	t.Cleanup(func() { tuning.Set(tuning.DefaultValues()) })

	hero := &model.Hero{Gear: make(map[model.EquipmentSlot]*model.GearItem)}
	enemy := model.Enemy{MaxHP: 100, HP: 100, Attack: 20, Defense: 8}
	ApplyEnemyEncounterHeroScaling(hero, &enemy)
	if enemy.MaxHP != 75 || enemy.HP != 75 || enemy.Attack != 15 || enemy.Defense != 6 {
		t.Fatalf("scaled enemy: got hp=%d atk=%d def=%d", enemy.MaxHP, enemy.Attack, enemy.Defense)
	}

	hero.Gear[model.SlotMainHand] = &model.GearItem{PrimaryStat: 1, StatType: "attack"}
	enemy2 := model.Enemy{MaxHP: 100, HP: 100, Attack: 20, Defense: 8}
	ApplyEnemyEncounterHeroScaling(hero, &enemy2)
	if enemy2.MaxHP != 100 {
		t.Fatalf("geared hero should not scale enemy: got MaxHP %d", enemy2.MaxHP)
	}
}

func TestPickEnemyForLevel(t *testing.T) {
	tests := []struct {
		level int
	}{
		{1}, {5}, {10}, {20}, {50},
	}

	for _, tt := range tests {
		enemy := PickEnemyForLevel(tt.level)
		if enemy.Name == "" {
			t.Errorf("PickEnemyForLevel(%d) returned enemy with empty name", tt.level)
		}
		if enemy.MaxHP <= 0 {
			t.Errorf("PickEnemyForLevel(%d) returned enemy with MaxHP=%d", tt.level, enemy.MaxHP)
		}
		if enemy.HP != enemy.MaxHP {
			t.Errorf("PickEnemyForLevel(%d) returned enemy with HP=%d != MaxHP=%d", tt.level, enemy.HP, enemy.MaxHP)
		}
	}
}

func TestScaleEnemyTemplate(t *testing.T) {
	tmpl, ok := model.EnemyBySlug("wolf")
	if !ok {
		tmpl = model.Enemy{
			Slug:             "wolf",
			Archetype:        "wolf",
			Name:             "Forest Wolf",
			MaxHP:            40,
			HP:               40,
			Attack:           8,
			Defense:          2,
			Speed:            1.2,
			BaseLevel:        1,
			LevelVariance:    0.3,
			MaxHeroLevelDiff: 5,
			HPPerLevel:       5,
			AttackPerLevel:   1.5,
			DefensePerLevel:  1.0,
		}
	}
	scaled := ScaleEnemyTemplate(tmpl, 5)

	if scaled.MaxHP <= tmpl.MaxHP {
		t.Errorf("scaled MaxHP %d should exceed base %d at level 5", scaled.MaxHP, tmpl.MaxHP)
	}
	if scaled.HP != scaled.MaxHP {
		t.Errorf("scaled HP %d should equal MaxHP %d", scaled.HP, scaled.MaxHP)
	}
}
