"""
Unit tests for FreeExile Damage & Defense Calculation Engine (PoE2 Spirit).
Verifies:
1. Character offensive & defensive layers (Armour non-linear DR, Evasion entropy, Resistances, Ward/ES).
2. Monster scaling across levels and ranks (Common, Magic, Rare, Boss).
3. Boss Stagger/Poise system (threshold knock-down, damage amplification window).
4. Boss multi-phase enrage triggers (Phase 1, Phase 2 Frenzy, Phase 3 Savage Enrage).
5. Ailment application (Shock damage taken amplification, Ignite/Bleed DoT).
"""

import unittest
import sys
import os

sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../server")))

from combat.combat_types import (
    DamageChannel,
    CombatantType,
    AilmentType,
    BossPhase,
    DamageVector,
    DamagePacket,
    DefenseSheet,
    BossStaggerState,
)
from combat.damage_calculator import DamageCalculator
from combat.monster_scaling import MonsterScalingEngine


class TestDamageAndDefenseCalculator(unittest.TestCase):
    def setUp(self):
        self.calc = DamageCalculator()
        self.scaling = MonsterScalingEngine()

    def test_evasion_entropy_deterministic_distribution(self):
        """Tests that the entropy pool guarantees expected hit frequency without streak RNG."""
        accuracy = 1000.0
        evasion = 1000.0
        entropy = 50

        # At equal level, 1000 accuracy vs 1000 evasion should yield an evade chance ~ 30-40%
        # Hit chance will be roughly 60-70%. Over 10 consecutive attacks, hits must be deterministic.
        hits = 0
        current_entropy = entropy
        for _ in range(10):
            is_hit, current_entropy = self.calc.roll_accuracy_vs_evasion(accuracy, evasion, current_entropy)
            if is_hit:
                hits += 1

        self.assertGreaterEqual(hits, 5)
        self.assertLessEqual(hits, 9)

    def test_armour_nonlinear_mitigation(self):
        """
        Tests Path of Exile non-linear Armour formula:
        DR = Armour / (Armour + 5 * Damage), capped at 90%.
        Small hits are heavily reduced; massive hits punch through armour!
        """
        armour = 5000.0

        # 1. Small hit (100 physical damage): DR = 5000 / (5000 + 500) = 90.9% -> capped at 90%
        small_hit = 100.0
        mitigated_small = self.calc.calculate_armour_mitigation(armour, small_hit)
        self.assertAlmostEqual(mitigated_small, 0.90, places=2)

        # 2. Medium hit (1000 physical damage): DR = 5000 / (5000 + 5000) = 50%
        med_hit = 1000.0
        mitigated_med = self.calc.calculate_armour_mitigation(armour, med_hit)
        self.assertAlmostEqual(mitigated_med, 0.50, places=2)

        # 3. Massive Boss Slam (5000 physical damage): DR = 5000 / (5000 + 25000) = 16.67%
        slam_hit = 5000.0
        mitigated_slam = self.calc.calculate_armour_mitigation(armour, slam_hit)
        self.assertAlmostEqual(mitigated_slam, 0.1667, places=3)

    def test_resistance_mitigation_and_penetration(self):
        """Tests that effective resistance is capped at 75% and penetration directly bypasses it."""
        # 1. Normal 75% resist against 1000 fire damage
        mitigated_dmg = self.calc.calculate_resistance_damage(
            raw_damage=1000.0,
            resistance=0.85, # 85% uncapped
            max_cap=0.75,    # 75% cap
            penetration=0.0
        )
        self.assertAlmostEqual(mitigated_dmg, 250.0, places=1) # Takes 25% of 1000

        # 2. Penetration (25% fire penetration reduces 75% effective res to 50%)
        pen_dmg = self.calc.calculate_resistance_damage(
            raw_damage=1000.0,
            resistance=0.85,
            max_cap=0.75,
            penetration=0.25
        )
        self.assertAlmostEqual(pen_dmg, 500.0, places=1) # Takes 50% of 1000

        # 3. Negative resistance (e.g. -20% vulnerability)
        vuln_dmg = self.calc.calculate_resistance_damage(
            raw_damage=1000.0,
            resistance=-0.20,
            max_cap=0.75,
            penetration=0.0
        )
        self.assertAlmostEqual(vuln_dmg, 1200.0, places=1) # Takes 120% of 1000

    def test_ward_energy_shield_absorption(self):
        """Tests that Ward absorbs incoming damage before Life."""
        packet = DamagePacket(
            attacker_id=10,
            combatant_type=CombatantType.PLAYER,
            attacker_level=50,
            damage_vector=DamageVector(physical=300.0),
            accuracy=5000.0
        )

        # Defender has 200 Ward and 1000 Life, 0 Armour for simplicity
        defense = DefenseSheet(
            defender_id=20,
            combatant_type=CombatantType.MONSTER_COMMON,
            defender_level=50,
            max_life=1000.0,
            current_life=1000.0,
            max_ward=200.0,
            current_ward=200.0,
            armour=0.0,
            evasion_rating=0.0
        )

        result, _ = self.calc.execute_hit(packet, defense, entropy_pool=0)
        self.assertTrue(result.is_hit)
        self.assertEqual(result.absorbed_by_ward, 200.0)
        self.assertEqual(result.damage_to_life, 100.0)
        self.assertEqual(defense.current_ward, 0.0)
        self.assertEqual(defense.current_life, 900.0)

    def test_phantom_evasion_iframe(self):
        """Tests Huyễn Ảnh Bộ i-frame grants 100% damage immunity."""
        packet = DamagePacket(
            attacker_id=1,
            combatant_type=CombatantType.MONSTER_COMMON,
            attacker_level=20,
            damage_vector=DamageVector(physical=500.0),
            accuracy=9999.0
        )
        defense = DefenseSheet(
            defender_id=2,
            combatant_type=CombatantType.PLAYER,
            defender_level=20,
            max_life=1000.0,
            current_life=1000.0,
            is_in_iframe=True
        )

        result, _ = self.calc.execute_hit(packet, defense, entropy_pool=0)
        self.assertFalse(result.is_hit)
        self.assertTrue(result.is_evaded)
        self.assertEqual(result.final_total_damage, 0.0)
        self.assertEqual(defense.current_life, 1000.0)

    def test_monster_scaling_and_rank_multipliers(self):
        """Tests level scaling and rank multipliers (Common, Magic, Rare, Boss)."""
        lvl1_common = self.scaling.create_monster_defense(level=1, rank=CombatantType.MONSTER_COMMON)
        lvl50_common = self.scaling.create_monster_defense(level=50, rank=CombatantType.MONSTER_COMMON)
        lvl50_boss = self.scaling.create_monster_defense(level=50, rank=CombatantType.BOSS)

        self.assertGreater(lvl50_common.max_life, lvl1_common.max_life * 5)
        # Boss at level 50 should have 15x - 30x the HP of Common monster
        self.assertGreaterEqual(lvl50_boss.max_life, lvl50_common.max_life * 15)
        # Boss has base elemental resistances
        self.assertGreaterEqual(lvl50_boss.resistances[DamageChannel.FIRE], 0.35)

    def test_boss_stagger_poise_mechanic(self):
        """
        Tests PoE2 Boss Stagger:
        Heavy impact hits fill Stagger meter.
        When reaching 100%, Boss enters STAGGERED state and takes +30% amplified damage.
        """
        boss_defense = self.scaling.create_monster_defense(level=60, rank=CombatantType.BOSS)
        boss_stagger = self.scaling.create_boss_stagger_state(boss_defense.max_life, level=60)

        # Player attacks with a heavy hammer strike (stagger_impact = 60% of poise)
        heavy_packet = DamagePacket(
            attacker_id=99,
            combatant_type=CombatantType.PLAYER,
            attacker_level=60,
            damage_vector=DamageVector(physical=1000.0),
            accuracy=50000.0,
            stagger_impact=boss_stagger.max_stagger_poise * 0.60
        )

        # Hit 1: 60% stagger
        res1, ent1 = self.calc.execute_hit(heavy_packet, boss_defense, entropy_pool=50, boss_stagger=boss_stagger)
        self.assertTrue(res1.is_hit)
        self.assertFalse(res1.boss_staggered_triggered)
        self.assertFalse(boss_stagger.is_staggered)

        # Hit 2: another 60% stagger -> exceeds 100% -> Stagger Triggered!
        res2, ent2 = self.calc.execute_hit(heavy_packet, boss_defense, entropy_pool=ent1, boss_stagger=boss_stagger)
        self.assertTrue(res2.is_hit)
        self.assertTrue(res2.boss_staggered_triggered)
        self.assertTrue(boss_stagger.is_staggered)
        self.assertGreater(boss_stagger.stagger_duration_remaining_sec, 0.0)

        # While staggered, boss takes amplified damage (damage_taken_multiplier is elevated)
        burst_packet = DamagePacket(
            attacker_id=99,
            combatant_type=CombatantType.PLAYER,
            attacker_level=60,
            damage_vector=DamageVector(fire=1000.0),
            accuracy=50000.0
        )
        res_burst, _ = self.calc.execute_hit(burst_packet, boss_defense, entropy_pool=ent2, boss_stagger=boss_stagger)
        self.assertTrue(res_burst.is_hit)
        # Damage should be multiplied by staggered vulnerability (+30%)
        expected_base_mitigated = 1000.0 * (1.0 - boss_defense.resistances[DamageChannel.FIRE])
        expected_staggered = expected_base_mitigated * 1.30
        self.assertAlmostEqual(res_burst.final_total_damage, expected_staggered, delta=2.0)

    def test_boss_enrage_phases(self):
        """Tests Boss escalating into Phase 2 (Frenzy) and Phase 3 (Savage Enrage) based on HP %."""
        # 100% HP -> Phase 1
        p1 = self.scaling.evaluate_boss_phase(current_hp=10000.0, max_hp=10000.0)
        self.assertEqual(p1, BossPhase.PHASE_1_NORMAL)

        # 50% HP -> Phase 2 (HP <= 66%)
        p2 = self.scaling.evaluate_boss_phase(current_hp=5000.0, max_hp=10000.0)
        self.assertEqual(p2, BossPhase.PHASE_2_ENRAGED)

        # 20% HP -> Phase 3 (HP <= 33%)
        p3 = self.scaling.evaluate_boss_phase(current_hp=2000.0, max_hp=10000.0)
        self.assertEqual(p3, BossPhase.PHASE_3_SAVAGE_FRENZY)

        # Test phase modifiers on damage packet
        base_packet = self.scaling.create_boss_attack_packet(boss_id=777, level=70, phase=BossPhase.PHASE_3_SAVAGE_FRENZY)
        # Phase 3 provides +50% extra damage and +25% penetration
        self.assertGreaterEqual(base_packet.elemental_penetration.get(DamageChannel.FIRE, 0.0), 0.20)

    def test_combat_engine_process_attack_and_phase_transition(self):
        """Tests that CombatEngine handles full attack transactions and updates Boss Phase dynamically."""
        from combat.combat_engine import CombatEngine, CombatEntity
        from world.martial_matrix import FiveElements

        engine = CombatEngine()

        # Attacker: Player (Kim element)
        player_defense = DefenseSheet(
            defender_id=1,
            combatant_type=CombatantType.PLAYER,
            defender_level=50,
            max_life=2000.0,
            current_life=2000.0,
        )
        player = CombatEntity(
            entity_id=1,
            name="Kiếm Khách Vô Danh",
            combatant_type=CombatantType.PLAYER,
            level=50,
            element=FiveElements.KIM,
            defense=player_defense,
        )

        # Defender: Boss (Mộc element -> Kim khắc Mộc bonus)
        boss_defense = self.scaling.create_monster_defense(level=50, rank=CombatantType.BOSS)
        boss_stagger = self.scaling.create_boss_stagger_state(boss_defense.max_life, level=50)
        boss = CombatEntity(
            entity_id=2,
            name="Mộc Yêu Ma Hoàng",
            combatant_type=CombatantType.BOSS,
            level=50,
            element=FiveElements.MOC,
            defense=boss_defense,
            boss_stagger=boss_stagger,
            boss_phase=BossPhase.PHASE_1_NORMAL,
        )

        engine.register_entity(player)
        engine.register_entity(boss)

        # Deal massive burst damage to bring Boss below 66% HP
        dmg_packet = DamagePacket(
            attacker_id=1,
            combatant_type=CombatantType.PLAYER,
            attacker_level=50,
            damage_vector=DamageVector(physical=boss_defense.max_life * 0.45),
            accuracy=50000.0,
        )

        res = engine.process_attack(1, 2, dmg_packet, current_timestamp_ms=1000)
        self.assertIsNotNone(res)
        self.assertTrue(res.is_hit)
        # Boss phase should transition to Phase 2 Enraged
        self.assertEqual(boss.boss_phase, BossPhase.PHASE_2_ENRAGED)
        self.assertEqual(res.boss_phase_transition, BossPhase.PHASE_2_ENRAGED)

    def test_combat_engine_ward_recharge_and_stagger_tick(self):
        """Tests that Ward recharges after 4s idle and staggered boss recovers after 4s."""
        from combat.combat_engine import CombatEngine, CombatEntity
        from world.martial_matrix import FiveElements

        engine = CombatEngine()
        defense = DefenseSheet(
            defender_id=10,
            combatant_type=CombatantType.PLAYER,
            defender_level=30,
            max_life=1000.0,
            current_life=1000.0,
            max_ward=500.0,
            current_ward=100.0, # Damaged ward
        )
        stagger = BossStaggerState(
            max_stagger_poise=1000.0,
            current_stagger=0.0,
            is_staggered=True,
            stagger_duration_remaining_sec=3.0,
        )
        entity = CombatEntity(
            entity_id=10,
            name="Huyết Ma Tướng",
            combatant_type=CombatantType.BOSS,
            level=30,
            element=FiveElements.THO,
            defense=defense,
            boss_stagger=stagger,
            last_hit_received_timestamp_ms=0,
        )
        engine.register_entity(entity)

        # Tick 1: 2 seconds later (timestamp = 2000ms -> only 2s since hit at 0ms, ward should NOT recharge yet)
        engine.update_tick(delta_sec=2.0, current_timestamp_ms=2000)
        self.assertEqual(defense.current_ward, 100.0)
        self.assertEqual(stagger.stagger_duration_remaining_sec, 1.0)
        self.assertTrue(stagger.is_staggered)

        # Tick 2: 3 more seconds (timestamp = 5000ms -> 5s elapsed, ward recharges 20% of max per sec * 1s)
        engine.update_tick(delta_sec=3.0, current_timestamp_ms=5000)
        # Stagger duration was 1.0s, delta is 3.0s -> stagger expired!
        self.assertFalse(stagger.is_staggered)
        self.assertEqual(stagger.stagger_duration_remaining_sec, 0.0)
        # Ward has recharged
        self.assertGreater(defense.current_ward, 100.0)


if __name__ == "__main__":
    unittest.main()
