"""
Sprint 2 Unit & Integration Test Suite for FreeExile Martial Combat System:
- Ma Võ Tàn Cốt & Dị Biến Tà Ấn (Skill Gems & Support Sigils trade-offs)
- Huyễn Ảnh Bộ 0.25s i-frame Animation Cancel (Server-Authoritative)
- Song Binh Tự Động Hoán Đổi (PoE2 Spirit 0ms Dual Weapon Auto-Swap)
- Kỳ Kinh Bát Mạch (Meridian Integration) & Ngũ Hành Tương Khắc
- Strict Code Hygiene & Deterministic Verification
"""

import os
import sys
import time
import unittest

PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
SERVER_DIR = os.path.join(PROJECT_ROOT, "server")
if SERVER_DIR not in sys.path:
    sys.path.insert(0, SERVER_DIR)
if PROJECT_ROOT not in sys.path:
    sys.path.insert(0, PROJECT_ROOT)

from world.combat_engine import CombatEngine, CombatActor, SkillCastResult
from world.martial_matrix import FiveElements, WeaponCategory
from world.skill_database_service import SkillDatabaseService
from world.meridian_service import MeridianService


class TestSprint2MartialCombatSystem(unittest.TestCase):
    def setUp(self):
        self.engine = CombatEngine()
        self.skill_db = SkillDatabaseService(db_path=":memory:")
        self.skill_db.seed_canonical_data(force=True)

        self.attacker = CombatActor(
            actor_id=1,
            name="Tiêu Dao Kiếm Khách",
            element=FiveElements.HOA,
            current_hp=1000.0,
            max_hp=1000.0,
            base_attack=50.0,
            current_energy=100.0,
            max_energy=100.0,
            weapon_set_1="SWORD",
            weapon_set_2="PROJECTILE_WEAPON",
            active_weapon_set=1,
            is_player=True,
            player_id="player_sprint2",
        )
        self.defender = CombatActor(
            actor_id=2,
            name="Kim Cang Thú Vương",
            element=FiveElements.KIM,
            current_hp=2000.0,
            max_hp=2000.0,
            base_attack=60.0,
            resistances={
                FiveElements.KIM: 0.15,
                FiveElements.MOC: 0.05,
                FiveElements.THUY: 0.30,
                FiveElements.HOA: 0.0,
                FiveElements.THO: 0.20,
            },
        )
        self.engine.register_actor(self.attacker)
        self.engine.register_actor(self.defender)

    def tearDown(self):
        self.skill_db.close()

    def test_sigil_linking_modifiers_and_projectiles(self):
        """Skill 1001 + Sigil 2001 (Multi-Projectile) multiplies projectiles and increases energy cost."""
        now_ms = int(time.time() * 1000)
        res = self.engine.execute_skill_cast(
            attacker_id=1,
            skill_id=1001,
            target_id=2,
            timestamp_ms=now_ms,
            linked_sigil_ids=[2001],
            skill_db=self.skill_db,
        )
        self.assertTrue(res.success)
        self.assertEqual(res.skill_id, 1001)
        # Base projectiles = 1, Sigil 2001 grants +3 -> total 4 projectiles
        self.assertEqual(res.projectiles, 4)
        # Base energy 45 * 1.45 = 65.25
        self.assertAlmostEqual(res.energy_consumed, 65.25, places=1)
        self.assertAlmostEqual(self.attacker.current_energy, 100.0 - 65.25, places=1)
        self.assertIsNotNone(res.damage_result)
        self.assertFalse(res.damage_result.is_evaded)
        self.assertGreater(res.damage_result.final_damage, 0.0)

    def test_incompatible_sigil_tag_conflict(self):
        """Incompatible Sigil (Phân Quang on Melee Skill 1007) is filtered by tag validation."""
        valid, msg = self.skill_db.validate_gem_link(1007, 2001)
        self.assertFalse(valid)
        self.assertIn("xung đột", msg)

        # Attacker equips blade for skill 1007
        self.attacker.weapon_set_1 = "BLADE"
        now_ms = int(time.time() * 1000)
        res = self.engine.execute_skill_cast(
            attacker_id=1,
            skill_id=1007,
            target_id=2,
            timestamp_ms=now_ms,
            linked_sigil_ids=[2001],
            skill_db=self.skill_db,
        )
        self.assertTrue(res.success)
        # Incompatible sigil was skipped: projectile count remains default (1)
        self.assertEqual(res.projectiles, 1)
        # Energy cost remains unamplified base (22)
        self.assertEqual(res.energy_consumed, 22.0)

    def test_phantom_evasion_animation_cancel(self):
        """Huyễn Ảnh Bộ cancels ongoing animation lock and evades incoming attacks during 250ms window."""
        now_ms = int(time.time() * 1000)
        # Cast skill 1001 with 650ms animation lock
        res = self.engine.execute_skill_cast(
            attacker_id=1,
            skill_id=1001,
            target_id=2,
            timestamp_ms=now_ms,
        )
        self.assertTrue(res.success)
        # 100ms later: actor is locked in animation
        mid_cast_ms = now_ms + 100
        self.assertTrue(self.engine.is_animation_locked(1, mid_cast_ms))

        # Attempting to cast another skill while locked fails
        second_cast = self.engine.execute_skill_cast(
            attacker_id=1,
            skill_id=1001,
            target_id=2,
            timestamp_ms=mid_cast_ms,
        )
        self.assertFalse(second_cast.success)
        self.assertEqual(second_cast.error_reason, "ANIMATION_LOCKED")

        # Activate Huyễn Ảnh Bộ (Phantom Evasion) at mid_cast_ms -> Cancels animation lock!
        evade_ok = self.engine.trigger_phantom_evasion(1, mid_cast_ms)
        self.assertTrue(evade_ok)
        self.assertFalse(self.engine.is_animation_locked(1, mid_cast_ms))

        # Incoming attack from defender at mid_cast_ms + 50ms (within 250ms i-frame) is 100% evaded
        incoming_dmg = self.engine.calculate_damage(
            attacker_id=2,
            defender_id=1,
            raw_damage=120.0,
            damage_element=FiveElements.KIM,
            current_timestamp_ms=mid_cast_ms + 50,
        )
        self.assertTrue(incoming_dmg.is_evaded)
        self.assertEqual(incoming_dmg.final_damage, 0.0)
        self.assertEqual(self.attacker.current_hp, 1000.0)

    def test_dual_weapon_auto_swap(self):
        """Casting a projectile skill while holding SWORD auto-swaps to PROJECTILE_WEAPON with 0ms delay."""
        self.assertEqual(self.attacker.active_weapon_set, 1)
        now_ms = int(time.time() * 1000)

        # Skill 1003 ("U Minh Huyết Độc Châm") requires PROJECTILE_WEAPON
        res = self.engine.execute_skill_cast(
            attacker_id=1,
            skill_id=1003,
            target_id=2,
            timestamp_ms=now_ms,
        )
        self.assertTrue(res.success)
        self.assertTrue(res.swapped_weapon)
        self.assertEqual(res.new_weapon_set, 2)
        self.assertEqual(self.attacker.active_weapon_set, 2)

    def test_weapon_incompatible_both_sets(self):
        """Fails with WEAPON_INCOMPATIBLE if neither weapon set meets skill requirements."""
        now_ms = int(time.time() * 1000)
        # Skill 1008 ("Liệt Diễm Bộc Khí Trảm") requires BLADE; attacker has SWORD and PROJECTILE_WEAPON
        res = self.engine.execute_skill_cast(
            attacker_id=1,
            skill_id=1008,
            target_id=2,
            timestamp_ms=now_ms,
        )
        self.assertFalse(res.success)
        self.assertEqual(res.error_reason, "WEAPON_INCOMPATIBLE")
        self.assertFalse(res.swapped_weapon)

    def test_meridian_service_stat_integration_and_five_elements(self):
        """Unlocking Meridian nodes amplifies base_attack and scales Five Elements combat damage."""
        now_ms = int(time.time() * 1000)
        # Baseline cast
        res_baseline = self.engine.execute_skill_cast(
            attacker_id=1, skill_id=1001, target_id=2, timestamp_ms=now_ms
        )
        self.assertTrue(res_baseline.success)
        dmg_baseline = res_baseline.damage_result.final_damage

        # Unlock meridian nodes using MeridianService (Origin m_c1 -> Vessel m_c3 which has +8% dps_mult)
        meridian_svc = MeridianService(db_path=":memory:")
        meridian_svc.add_points("player_sprint2", 10)
        r1 = meridian_svc.unlock_node("player_sprint2", "m_c1")
        self.assertTrue(r1.success)
        r2 = meridian_svc.unlock_node("player_sprint2", "m_c3")
        self.assertTrue(r2.success)

        # Apply updated meridian stats to attacker
        initial_atk = self.attacker.base_attack
        meridian_svc.apply_to_combat_actor("player_sprint2", self.attacker)
        self.assertGreater(self.attacker.base_attack, initial_atk)

        # Cast again at now_ms + 2000 (after 1800ms cooldown)
        res_buffed = self.engine.execute_skill_cast(
            attacker_id=1, skill_id=1001, target_id=2, timestamp_ms=now_ms + 2000
        )
        self.assertTrue(res_buffed.success)
        self.assertGreater(res_buffed.damage_result.final_damage, dmg_baseline)

    def test_cooldown_and_energy_depletion(self):
        """Cooldown active and insufficient energy correctly gate skill execution."""
        now_ms = int(time.time() * 1000)
        # Cast skill 1001 with 1800ms cooldown and 650ms animation lock
        first = self.engine.execute_skill_cast(1, 1001, 2, now_ms)
        self.assertTrue(first.success)

        # Cast during animation lock fails with ANIMATION_LOCKED
        during_lock = self.engine.execute_skill_cast(1, 1001, 2, now_ms + 200)
        self.assertFalse(during_lock.success)
        self.assertEqual(during_lock.error_reason, "ANIMATION_LOCKED")

        # Cast after animation lock (650ms) but before cooldown (1800ms) fails with COOLDOWN_ACTIVE
        repeat = self.engine.execute_skill_cast(1, 1001, 2, now_ms + 800)
        self.assertFalse(repeat.success)
        self.assertEqual(repeat.error_reason, "COOLDOWN_ACTIVE")

        # Deplete energy to zero
        self.attacker.current_energy = 5.0
        no_energy = self.engine.execute_skill_cast(1, 1001, 2, now_ms + 2000)
        self.assertFalse(no_energy.success)
        self.assertEqual(no_energy.error_reason, "INSUFFICIENT_ENERGY")


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