package model

import (
	"math"
	"testing"
	"time"

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

func TestDerivedCombatStatsFromBaseAttributes(t *testing.T) {
	now := time.Now()
	hero := &Hero{
		Attack:       10,
		Defense:      5,
		Speed:        1.0,
		Strength:     10,
		Constitution: 8,
		Agility:      6,
		Gear: map[EquipmentSlot]*GearItem{
			SlotMainHand: {
				PrimaryStat:   5,
				SpeedModifier: 1.3,
			},
			SlotChest: {
				PrimaryStat:   4,
				SpeedModifier: 0.7,
				AgilityBonus:  -3,
			},
		},
	}

	gotAttack := hero.EffectiveAttackAt(now)
	// atk = 10 + 10*2 + 3/4 + 8/8 = 31 + weapon.PrimaryStat(5) = 36
	if gotAttack != 36 {
		t.Fatalf("expected attack 36, got %d", gotAttack)
	}

	gotDefense := hero.EffectiveDefenseAt(now)
	// def = 5 + 8/4 + 3/8 = 7 + chest.PrimaryStat(4) = 11
	if gotDefense != 11 {
		t.Fatalf("expected defense 11, got %d", gotDefense)
	}

	gotSpeed := hero.EffectiveSpeedAt(now)
	wantSpeed := (1.0 + 3*tuning.DefaultValues().AgilityCoef) * 1.3 * 0.7
	if math.Abs(gotSpeed-wantSpeed) > 0.001 {
		t.Fatalf("expected speed %.3f, got %.3f", wantSpeed, gotSpeed)
	}
}

func TestBuffsProvideTemporaryStatEffects(t *testing.T) {
	now := time.Now()
	hero := &Hero{
		Attack:       10,
		Defense:      5,
		Speed:        1.0,
		Strength:     10,
		Constitution: 8,
		Agility:      6,
		Buffs: []ActiveBuff{
			{
				Buff:      mustBuffDef(BuffRage),
				AppliedAt: now.Add(-time.Second),
				ExpiresAt: now.Add(5 * time.Second),
			},
			{
				Buff:      mustBuffDef(BuffWarCry),
				AppliedAt: now.Add(-time.Second),
				ExpiresAt: now.Add(5 * time.Second),
			},
			{
				Buff:      mustBuffDef(BuffShield),
				AppliedAt: now.Add(-time.Second),
				ExpiresAt: now.Add(5 * time.Second),
			},
		},
	}

	if hero.EffectiveAttackAt(now) <= 30 {
		t.Fatalf("expected buffed attack to increase above baseline")
	}
	if hero.EffectiveDefenseAt(now) <= 5 {
		t.Fatalf("expected shield constitution bonus to increase defense")
	}
	if hero.EffectiveSpeedAt(now) <= 1.0 {
		t.Fatalf("expected war cry to increase attack speed")
	}
}

func TestEffectiveSpeedIsCapped(t *testing.T) {
	now := time.Now()
	hero := &Hero{
		Speed:   2.5,
		Agility: 200,
		Gear: map[EquipmentSlot]*GearItem{
			SlotMainHand: {SpeedModifier: 1.5},
		},
		Buffs: []ActiveBuff{
			{
				Buff:      mustBuffDef(BuffWarCry),
				AppliedAt: now.Add(-time.Second),
				ExpiresAt: now.Add(10 * time.Second),
			},
		},
	}

	got := hero.EffectiveSpeedAt(now)
	maxAttackSpeed := tuning.DefaultValues().MaxAttackSpeed
	if got != maxAttackSpeed {
		t.Fatalf("expected speed cap %.1f, got %.3f", maxAttackSpeed, got)
	}
}

func TestRushDoesNotAffectAttackSpeed(t *testing.T) {
	now := time.Now()
	hero := &Hero{Speed: 1.0, Agility: 5}

	baseSpeed := hero.EffectiveSpeedAt(now)

	hero.Buffs = []ActiveBuff{{
		Buff:      mustBuffDef(BuffRush),
		AppliedAt: now.Add(-time.Second),
		ExpiresAt: now.Add(5 * time.Second),
	}}
	buffedSpeed := hero.EffectiveSpeedAt(now)

	if math.Abs(buffedSpeed-baseSpeed) > 0.001 {
		t.Fatalf("Rush should not affect attack speed: base=%.3f, buffed=%.3f", baseSpeed, buffedSpeed)
	}
}

func TestRushAffectsMovementSpeed(t *testing.T) {
	now := time.Now()
	hero := &Hero{Speed: 1.0, Agility: 5}

	baseMoveSpeed := hero.MovementSpeedMultiplier(now)
	if baseMoveSpeed != 1.0 {
		t.Fatalf("expected base movement multiplier 1.0, got %.3f", baseMoveSpeed)
	}

	hero.Buffs = []ActiveBuff{{
		Buff:      mustBuffDef(BuffRush),
		AppliedAt: now.Add(-time.Second),
		ExpiresAt: now.Add(5 * time.Second),
	}}

	got := hero.MovementSpeedMultiplier(now)
	want := 1.0 + mustBuffDef(BuffRush).Magnitude
	if math.Abs(got-want) > 0.001 {
		t.Fatalf("expected Rush movement multiplier %.3f, got %.3f", want, got)
	}
}

func TestSlowDoesNotAffectAttackSpeed(t *testing.T) {
	now := time.Now()
	hero := &Hero{Speed: 1.0, Agility: 5}

	baseSpeed := hero.EffectiveSpeedAt(now)

	hero.Debuffs = []ActiveDebuff{{
		Debuff:    mustDebuffDef(DebuffSlow),
		AppliedAt: now.Add(-time.Second),
		ExpiresAt: now.Add(3 * time.Second),
	}}
	debuffedSpeed := hero.EffectiveSpeedAt(now)

	if math.Abs(debuffedSpeed-baseSpeed) > 0.001 {
		t.Fatalf("Slow should not affect attack speed: base=%.3f, debuffed=%.3f", baseSpeed, debuffedSpeed)
	}
}

func TestSlowAffectsMovementSpeed(t *testing.T) {
	now := time.Now()
	hero := &Hero{Speed: 1.0, Agility: 5}

	hero.Debuffs = []ActiveDebuff{{
		Debuff:    mustDebuffDef(DebuffSlow),
		AppliedAt: now.Add(-time.Second),
		ExpiresAt: now.Add(3 * time.Second),
	}}

	got := hero.MovementSpeedMultiplier(now)
	want := 0.6 // 1.0 * (1 - 0.4)
	if math.Abs(got-want) > 0.001 {
		t.Fatalf("expected Slow to give movement multiplier %.1f, got %.3f", want, got)
	}
}

func TestIceSlowReducesAttackSpeed(t *testing.T) {
	now := time.Now()
	hero := &Hero{Speed: 1.0, Agility: 5}

	baseSpeed := hero.EffectiveSpeedAt(now)

	hero.Debuffs = []ActiveDebuff{{
		Debuff:    mustDebuffDef(DebuffIceSlow),
		AppliedAt: now.Add(-time.Second),
		ExpiresAt: now.Add(3 * time.Second),
	}}
	debuffedSpeed := hero.EffectiveSpeedAt(now)

	want := baseSpeed * 0.8
	if math.Abs(debuffedSpeed-want) > 0.01 {
		t.Fatalf("IceSlow should reduce attack speed by 20%%: expected %.3f, got %.3f", want, debuffedSpeed)
	}
}

func TestLevelUpParity(t *testing.T) {
	hero1 := &Hero{
		Level: 1, XP: 500,
		MaxHP: 100, HP: 100,
		Attack: 10, Defense: 5, Speed: 1.0,
		Strength: 5, Constitution: 5, Agility: 5, Luck: 5,
	}
	hero2 := *hero1

	// Level up hero1 via LevelUp()
	levels1 := 0
	for hero1.LevelUp() {
		levels1++
	}

	// Level up hero2 via LevelUp() too (should be identical since both use same method)
	levels2 := 0
	for hero2.LevelUp() {
		levels2++
	}

	if levels1 != levels2 {
		t.Fatalf("expected same levels gained: %d vs %d", levels1, levels2)
	}
	if hero1.Level != hero2.Level || hero1.MaxHP != hero2.MaxHP ||
		hero1.Attack != hero2.Attack || hero1.Defense != hero2.Defense {
		t.Fatalf("heroes diverged after identical LevelUp calls")
	}
}

// v3 progression: XP bases ×10 vs v2; secondary-stat cadences ×10 (3→30, 4→40, …).
func TestProgressionV3CanonicalSnapshots(t *testing.T) {
	now := time.Now()
	snap := func(targetLevel int) *Hero {
		h := &Hero{
			Level: 1, XP: 0,
			MaxHP: 100, HP: 100,
			Attack: 10, Defense: 5, Speed: 1.0,
			Strength: 1, Constitution: 1, Agility: 1, Luck: 1,
		}
		for h.Level < targetLevel {
			h.XP += XPToNextLevel(h.Level)
			if !h.LevelUp() {
				t.Fatalf("expected level-up before reaching target level %d", targetLevel)
			}
		}
		h.RefreshDerivedCombatStats(now)
		return h
	}

	t.Run("L30", func(t *testing.T) {
		h := snap(30)
		if h.MaxHP != 103 || h.Attack != 11 || h.Defense != 6 || h.Strength != 1 {
			t.Fatalf("L30 snapshot: maxHp=%d atk=%d def=%d str=%d", h.MaxHP, h.Attack, h.Defense, h.Strength)
		}
		if h.EffectiveAttackAt(now) != 13 || h.EffectiveDefenseAt(now) != 6 {
			t.Fatalf("L30 derived: atkPow=%d defPow=%d", h.EffectiveAttackAt(now), h.EffectiveDefenseAt(now))
		}
	})
	t.Run("L45", func(t *testing.T) {
		h := snap(45)
		if h.MaxHP != 104 || h.Attack != 11 || h.Defense != 6 || h.Strength != 2 {
			t.Fatalf("L45 snapshot: maxHp=%d atk=%d def=%d str=%d", h.MaxHP, h.Attack, h.Defense, h.Strength)
		}
		if h.EffectiveAttackAt(now) != 15 || h.EffectiveDefenseAt(now) != 6 {
			t.Fatalf("L45 derived: atkPow=%d defPow=%d", h.EffectiveAttackAt(now), h.EffectiveDefenseAt(now))
		}
	})
}

func TestXPToNextLevelFormula(t *testing.T) {
	if got := XPToNextLevel(1); got != 180 {
		t.Fatalf("XPToNextLevel(1) = %d, want 180", got)
	}
	if got := XPToNextLevel(2); got != 230 {
		t.Fatalf("XPToNextLevel(2) = %d, want 230", got)
	}
	if got := XPToNextLevel(10); got != 1450 {
		t.Fatalf("XPToNextLevel(10) = %d, want 1450", got)
	}
	if got := XPToNextLevel(30); got != 23000 {
		t.Fatalf("XPToNextLevel(30) = %d, want 23000", got)
	}
}

func TestLevelUpDoesNotRestoreHP(t *testing.T) {
	hero := &Hero{
		Level:        1,
		XP:           200,
		HP:           40,
		MaxHP:        100,
		Attack:       10,
		Defense:      5,
		Speed:        1.0,
		Strength:     5,
		Constitution: 5,
		Agility:      5,
		Luck:         5,
	}

	if !hero.LevelUp() {
		t.Fatal("expected hero to level up")
	}
	if hero.HP == hero.MaxHP {
		t.Fatalf("level-up should NOT restore HP (spec §3.3): hp=%d max=%d", hero.HP, hero.MaxHP)
	}
	if hero.HP != 40 {
		t.Fatalf("HP should be unchanged after level-up: got %d, want 40", hero.HP)
	}
}

func mustBuffDef(bt BuffType) Buff {
	b, ok := BuffDefinition(bt)
	if !ok {
		panic("missing buff def: " + string(bt))
	}
	return b
}

func mustDebuffDef(dt DebuffType) Debuff {
	d, ok := DebuffDefinition(dt)
	if !ok {
		panic("missing debuff def: " + string(dt))
	}
	return d
}
