"""
Unit and Integration Test Suite for FreeExile Milestone 4 Dodge Roll & i-Frame Evasion:
- Client-side Huyễn Ảnh Bộ (0.25s i-frame window, cooldown gating, phantom trails)
- Client-side monster attack damage negation & floating combat text ('NÉ ĐÒN! (i-frame)')
- Server-side CombatEngine 250ms evasion window boundary verification
- Multi-platform synchronization (Server Python, Client WebApp JS, Client iOS TypeScript)
- Strict code hygiene & line count compliance (<= 500 lines hard cap)
"""

import os
import sys
import unittest
import time
from pathlib import Path

PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
SERVER_DIR = PROJECT_ROOT / "server"
if str(SERVER_DIR) not in sys.path:
    sys.path.insert(0, str(SERVER_DIR))
if str(PROJECT_ROOT) not in sys.path:
    sys.path.insert(0, str(PROJECT_ROOT))

from world.combat_engine import CombatEngine, CombatActor
from world.martial_matrix import FiveElements

WEBAPP_DIR = PROJECT_ROOT / "client" / "webapp"
COMBAT_SKILLS_JS = WEBAPP_DIR / "js" / "engine" / "combat_skills.js"
MONSTER_SYSTEM_JS = WEBAPP_DIR / "js" / "engine" / "monster_system.js"
CANVAS_RENDERER_JS = WEBAPP_DIR / "js" / "engine" / "canvas_renderer.js"
ENTITY_RENDERER_JS = WEBAPP_DIR / "js" / "engine" / "entity_renderer.js"
IOS_COMBAT_TS = PROJECT_ROOT / "client" / "src" / "combat" / "CombatController.ts"


class TestDodgeRollAndIFrameEvasion(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.combat_skills_code = COMBAT_SKILLS_JS.read_text(encoding="utf-8")
        cls.monster_system_code = MONSTER_SYSTEM_JS.read_text(encoding="utf-8")
        cls.canvas_renderer_code = CANVAS_RENDERER_JS.read_text(encoding="utf-8")
        cls.entity_renderer_code = ENTITY_RENDERER_JS.read_text(encoding="utf-8")

    # --- Area 1: Client WebApp Combat Skills (Huyễn Ảnh Bộ) ---
    def test_client_dodge_sets_iframe_window(self):
        """Verifies combat_skills.js sets isIFrame = true and iFrameTimer = 0.25."""
        self.assertIn("player.isIFrame = true;", self.combat_skills_code)
        self.assertIn("player.iFrameTimer = 0.25;", self.combat_skills_code)

    def test_client_dodge_charges_and_state(self):
        """Verifies dodge charges pool and dodge animation state are present."""
        self.assertIn("dodgeCharges", self.combat_skills_code)
        self.assertIn("player.animState = 'dodge';", self.combat_skills_code)

    def test_client_dodge_cooldown_gating(self):
        """Verifies doDodge contains guards against double-casting or cooldown violation."""
        self.assertIn("doDodge", self.combat_skills_code)
        self.assertIn("triggerCooldown", self.combat_skills_code)

    def test_client_ghost_trail_integration(self):
        """Verifies ghost trails are generated in canvas_renderer and drawn in entity_renderer."""
        self.assertIn("player.ghostTrails.push", self.canvas_renderer_code)
        self.assertIn("player.ghostTrails", self.entity_renderer_code)

    # --- Area 2: Client Monster Attack Evasion & Feedback ---
    def test_monster_system_iframe_check(self):
        """Verifies monster attack checks player i-frame before applying damage."""
        self.assertIn("executeMonsterAttackOnPlayer", self.monster_system_code)
        self.assertIn("player.isIFrame", self.monster_system_code)

    def test_monster_system_evasion_floating_text(self):
        """Verifies floating combat text 'NÉ ĐÒN! (i-frame)' in cyan (#38bdf8)."""
        self.assertIn("NÉ ĐÒN! (i-frame)", self.monster_system_code)
        self.assertIn("#38bdf8", self.monster_system_code)

    def test_monster_system_evasion_sfx(self):
        """Verifies dodge whoosh sound effect plays on successful evasion."""
        self.assertIn("playDodgeWhoosh", self.monster_system_code)

    # --- Area 3: Server CombatEngine Parity & Exact Boundaries ---
    def test_server_actor_iframe_duration_constant(self):
        """Verifies server CombatActor default evasion_iframe_duration_ms is 250."""
        actor = CombatActor(actor_id=1, name="TestHero", element=FiveElements.KIM)
        self.assertEqual(actor.evasion_iframe_duration_ms, 250)

    def test_server_iframe_boundary_zero_ms(self):
        """Attack landing at exactly t+0ms is completely evaded (0 damage)."""
        engine = CombatEngine()
        atk = CombatActor(1, "Mob", FiveElements.HOA, base_attack=100.0)
        defn = CombatActor(2, "Hero", FiveElements.KIM, current_hp=500.0, max_hp=500.0)
        engine.register_actor(atk)
        engine.register_actor(defn)

        now_ms = 1000000
        engine.trigger_phantom_evasion(defn.actor_id, now_ms)
        res = engine.calculate_damage(atk.actor_id, defn.actor_id, 100.0, FiveElements.HOA, now_ms)
        self.assertTrue(res.is_evaded)
        self.assertEqual(res.final_damage, 0.0)
        self.assertEqual(defn.current_hp, 500.0)

    def test_server_iframe_boundary_249ms(self):
        """Attack landing at t+249ms (inside window) is completely evaded."""
        engine = CombatEngine()
        atk = CombatActor(1, "Mob", FiveElements.HOA, base_attack=100.0)
        defn = CombatActor(2, "Hero", FiveElements.KIM, current_hp=500.0, max_hp=500.0)
        engine.register_actor(atk)
        engine.register_actor(defn)

        now_ms = 1000000
        engine.trigger_phantom_evasion(defn.actor_id, now_ms)
        res = engine.calculate_damage(atk.actor_id, defn.actor_id, 100.0, FiveElements.HOA, now_ms + 249)
        self.assertTrue(res.is_evaded)
        self.assertEqual(res.final_damage, 0.0)
        self.assertEqual(defn.current_hp, 500.0)

    def test_server_iframe_boundary_250ms(self):
        """Attack landing at exactly t+250ms (inclusive boundary) is evaded."""
        engine = CombatEngine()
        atk = CombatActor(1, "Mob", FiveElements.HOA, base_attack=100.0)
        defn = CombatActor(2, "Hero", FiveElements.KIM, current_hp=500.0, max_hp=500.0)
        engine.register_actor(atk)
        engine.register_actor(defn)

        now_ms = 1000000
        engine.trigger_phantom_evasion(defn.actor_id, now_ms)
        res = engine.calculate_damage(atk.actor_id, defn.actor_id, 100.0, FiveElements.HOA, now_ms + 250)
        self.assertTrue(res.is_evaded)
        self.assertEqual(res.final_damage, 0.0)
        self.assertEqual(defn.current_hp, 500.0)

    def test_server_iframe_expired_251ms(self):
        """Attack landing at t+251ms (outside window) applies normal damage."""
        engine = CombatEngine()
        atk = CombatActor(1, "Mob", FiveElements.HOA, base_attack=100.0)
        defn = CombatActor(2, "Hero", FiveElements.KIM, current_hp=500.0, max_hp=500.0)
        engine.register_actor(atk)
        engine.register_actor(defn)

        now_ms = 1000000
        engine.trigger_phantom_evasion(defn.actor_id, now_ms)
        res = engine.calculate_damage(atk.actor_id, defn.actor_id, 100.0, FiveElements.HOA, now_ms + 251)
        self.assertFalse(res.is_evaded)
        self.assertGreater(res.final_damage, 0.0)
        self.assertLess(defn.current_hp, 500.0)

    def test_server_iframe_negative_timestamp_rejected(self):
        """Attack landing before evasion timestamp (e.g. t - 50ms) is not evaded."""
        engine = CombatEngine()
        atk = CombatActor(1, "Mob", FiveElements.HOA, base_attack=100.0)
        defn = CombatActor(2, "Hero", FiveElements.KIM, current_hp=500.0, max_hp=500.0)
        engine.register_actor(atk)
        engine.register_actor(defn)

        now_ms = 1000000
        engine.trigger_phantom_evasion(defn.actor_id, now_ms)
        res = engine.calculate_damage(atk.actor_id, defn.actor_id, 100.0, FiveElements.HOA, now_ms - 50)
        self.assertFalse(res.is_evaded)
        self.assertGreater(res.final_damage, 0.0)

    # --- Area 4: Multi-Platform Synchronization ---
    def test_ios_metal_combat_controller_duration_parity(self):
        """Verifies iOS CombatController.ts uses identical 250ms duration."""
        if IOS_COMBAT_TS.exists():
            ts_code = IOS_COMBAT_TS.read_text(encoding="utf-8")
            self.assertIn("evasionIframeDurationMs: number = 250", ts_code)

    # --- Area 5: Strict Code Hygiene Line Caps ---
    def test_combat_skills_line_cap(self):
        """combat_skills.js must not exceed 500 lines hard cap."""
        lines = len(self.combat_skills_code.splitlines())
        self.assertLessEqual(lines, 500, f"combat_skills.js has {lines} lines (Hard Cap 500)")

    def test_monster_system_line_cap(self):
        """monster_system.js must not exceed 500 lines hard cap."""
        lines = len(self.monster_system_code.splitlines())
        self.assertLessEqual(lines, 500, f"monster_system.js has {lines} lines (Hard Cap 500)")


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