package combat

import (
	"testing"
	"time"

	"a301_game_server/internal/entity"
	"a301_game_server/pkg/mathutil"
	pb "a301_game_server/proto/gen/pb"
)

// mockCombatant implements Combatant for testing.
type mockCombatant struct {
	id       uint64
	pos      mathutil.Vec3
	hp, maxHP int32
	mp, maxMP int32
	str, dex, intStat int32
}

func (m *mockCombatant) EntityID() uint64           { return m.id }
func (m *mockCombatant) EntityType() entity.Type    { return entity.TypePlayer }
func (m *mockCombatant) Position() mathutil.Vec3    { return m.pos }
func (m *mockCombatant) SetPosition(p mathutil.Vec3) { m.pos = p }
func (m *mockCombatant) Rotation() float32          { return 0 }
func (m *mockCombatant) SetRotation(float32)        {}
func (m *mockCombatant) ToProto() *pb.EntityState   { return &pb.EntityState{EntityId: m.id} }
func (m *mockCombatant) HP() int32                  { return m.hp }
func (m *mockCombatant) SetHP(hp int32) {
	if hp < 0 { hp = 0 }
	if hp > m.maxHP { hp = m.maxHP }
	m.hp = hp
}
func (m *mockCombatant) MaxHP() int32               { return m.maxHP }
func (m *mockCombatant) MP() int32                  { return m.mp }
func (m *mockCombatant) SetMP(mp int32) {
	if mp < 0 { mp = 0 }
	m.mp = mp
}
func (m *mockCombatant) IsAlive() bool              { return m.hp > 0 }
func (m *mockCombatant) Stats() CombatStats {
	return CombatStats{Str: m.str, Dex: m.dex, Int: m.intStat, Level: 1}
}

func newMock(id uint64, x, z float32) *mockCombatant {
	return &mockCombatant{
		id: id, pos: mathutil.NewVec3(x, 0, z),
		hp: 100, maxHP: 100, mp: 100, maxMP: 100,
		str: 10, dex: 10, intStat: 10,
	}
}

func TestBasicAttack(t *testing.T) {
	mgr := NewManager()
	mgr.SetBroadcast(func(entity.Entity, uint16, interface{}) {}, func(uint64, uint16, interface{}) {})

	attacker := newMock(1, 0, 0)
	target := newMock(2, 2, 0) // within range (3.0)

	entities := map[uint64]*mockCombatant{1: attacker, 2: target}

	ok, errMsg := mgr.UseSkill(attacker, 1, 2, mathutil.Vec3{},
		func(id uint64) entity.Entity {
			if e, ok := entities[id]; ok { return e }
			return nil
		},
		nil,
	)

	if !ok {
		t.Fatalf("expected success, got error: %s", errMsg)
	}

	if target.HP() >= 100 {
		t.Error("target should have taken damage")
	}
}

func TestOutOfRange(t *testing.T) {
	mgr := NewManager()
	mgr.SetBroadcast(func(entity.Entity, uint16, interface{}) {}, func(uint64, uint16, interface{}) {})

	attacker := newMock(1, 0, 0)
	target := newMock(2, 100, 0) // far away

	entities := map[uint64]*mockCombatant{1: attacker, 2: target}

	ok, _ := mgr.UseSkill(attacker, 1, 2, mathutil.Vec3{},
		func(id uint64) entity.Entity {
			if e, ok := entities[id]; ok { return e }
			return nil
		},
		nil,
	)

	if ok {
		t.Error("expected failure due to range")
	}
}

func TestCooldown(t *testing.T) {
	mgr := NewManager()
	mgr.SetBroadcast(func(entity.Entity, uint16, interface{}) {}, func(uint64, uint16, interface{}) {})

	attacker := newMock(1, 0, 0)
	target := newMock(2, 2, 0)

	entities := map[uint64]*mockCombatant{1: attacker, 2: target}
	getEnt := func(id uint64) entity.Entity {
		if e, ok := entities[id]; ok { return e }
		return nil
	}

	// First use should succeed.
	ok, _ := mgr.UseSkill(attacker, 1, 2, mathutil.Vec3{}, getEnt, nil)
	if !ok {
		t.Fatal("first use should succeed")
	}

	// Immediate second use should fail (cooldown).
	ok, errMsg := mgr.UseSkill(attacker, 1, 2, mathutil.Vec3{}, getEnt, nil)
	if ok {
		t.Error("expected cooldown failure")
	}
	if errMsg != "skill on cooldown" {
		t.Errorf("expected 'skill on cooldown', got '%s'", errMsg)
	}
}

func TestManaConsumption(t *testing.T) {
	mgr := NewManager()
	mgr.SetBroadcast(func(entity.Entity, uint16, interface{}) {}, func(uint64, uint16, interface{}) {})

	caster := newMock(1, 0, 0)
	target := newMock(2, 5, 0)
	caster.mp = 10 // not enough for Fireball (20 mana)

	entities := map[uint64]*mockCombatant{1: caster, 2: target}

	ok, errMsg := mgr.UseSkill(caster, 2, 2, mathutil.Vec3{},
		func(id uint64) entity.Entity {
			if e, ok := entities[id]; ok { return e }
			return nil
		},
		nil,
	)

	if ok {
		t.Error("expected mana failure")
	}
	if errMsg != "not enough mana" {
		t.Errorf("expected 'not enough mana', got '%s'", errMsg)
	}
}

func TestHealSelf(t *testing.T) {
	mgr := NewManager()
	mgr.SetBroadcast(func(entity.Entity, uint16, interface{}) {}, func(uint64, uint16, interface{}) {})

	caster := newMock(1, 0, 0)
	caster.hp = 30

	ok, _ := mgr.UseSkill(caster, 3, 0, mathutil.Vec3{},
		func(id uint64) entity.Entity { return nil },
		nil,
	)

	if !ok {
		t.Fatal("heal should succeed")
	}
	if caster.HP() <= 30 {
		t.Error("HP should have increased")
	}
}

func TestPoisonDoT(t *testing.T) {
	mgr := NewManager()
	mgr.SetBroadcast(func(entity.Entity, uint16, interface{}) {}, func(uint64, uint16, interface{}) {})

	attacker := newMock(1, 0, 0)
	target := newMock(2, 5, 0)

	entities := map[uint64]*mockCombatant{1: attacker, 2: target}

	// Apply poison (skill 5).
	ok, _ := mgr.UseSkill(attacker, 5, 2, mathutil.Vec3{},
		func(id uint64) entity.Entity {
			if e, ok := entities[id]; ok { return e }
			return nil
		},
		nil,
	)
	if !ok {
		t.Fatal("poison should succeed")
	}

	initialHP := target.HP()

	// Simulate ticks until first DoT tick (2s at 50ms/tick = 40 ticks).
	for i := 0; i < 40; i++ {
		mgr.UpdateBuffs(50*time.Millisecond, func(id uint64) Combatant {
			if e, ok := entities[id]; ok { return e }
			return nil
		})
	}

	if target.HP() >= initialHP {
		t.Error("poison DoT should have dealt damage")
	}
}

func TestDamageFormula(t *testing.T) {
	// Base 15, attacker STR 10 (1.1x), defender DEX 10 (0.95x)
	// Expected ~15.675 (without crit).
	result := CalcDamage(15, 10, 10)
	if result.FinalDamage < 1 {
		t.Error("damage should be at least 1")
	}
}

func TestRespawn(t *testing.T) {
	mgr := NewManager()

	ent := newMock(1, 50, 50)
	ent.hp = 0

	spawn := mathutil.NewVec3(0, 0, 0)
	mgr.Respawn(ent, spawn)

	if !ent.IsAlive() {
		t.Error("should be alive after respawn")
	}
	if ent.HP() != ent.MaxHP() {
		t.Error("should have full HP after respawn")
	}
	if ent.Position() != spawn {
		t.Error("should be at spawn position")
	}
}
