"""
Empirical Adversarial Stress Test Suite for Milestone 2:
- Exact 250ms i-frame boundaries (t=0ms, 249ms, 250ms evaded; 251ms damaged; negative timestamps rejected)
- Sequential dodge resets and extension logic
- Animation cancelling during attack wind-up and action locks
- 3-charge pool budgeting, depletion lockout, and multi-tier recharge
- 4 Weapon swing archetypes parameter verification (SLASH, THRUST, SLAM, BOLT)
- Execution of Node.js empirical runtime harness for client modules
"""

from __future__ import annotations
import os
import subprocess
import sys
from pathlib import Path
import pytest

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, DamageEventResult
from world.martial_matrix import FiveElements


class TestExactIFrameBoundariesServer:
    """Adversarial stress-testing of server-side i-frame boundaries."""

    @pytest.fixture
    def setup_combat(self) -> tuple[CombatEngine, CombatActor, CombatActor]:
        engine = CombatEngine()
        attacker = CombatActor(1, "SavageHellhound", FiveElements.HOA, base_attack=120.0)
        defender = CombatActor(2, "PrimalHero", FiveElements.KIM, current_hp=500.0, max_hp=500.0)
        engine.register_actor(attacker)
        engine.register_actor(defender)
        return engine, attacker, defender

    @pytest.mark.parametrize("delta_ms, should_evade", [
        (0, True),
        (1, True),
        (50, True),
        (125, True),
        (248, True),
        (249, True),
        (250, True),
        (251, False),
        (252, False),
        (300, False),
        (500, False),
        (-1, False),
        (-50, False),
    ])
    def test_exact_iframe_boundaries(
        self,
        setup_combat: tuple[CombatEngine, CombatActor, CombatActor],
        delta_ms: int,
        should_evade: bool
    ) -> None:
        """Verifies damage is evaded at t=0ms, 249ms, 250ms, and damage is taken at t=251ms and negatives."""
        engine, attacker, defender = setup_combat
        t0 = 1_000_000
        engine.trigger_phantom_evasion(defender.actor_id, t0)
        hit_time = t0 + delta_ms

        res = engine.calculate_damage(attacker.actor_id, defender.actor_id, 100.0, FiveElements.HOA, hit_time)
        if should_evade:
            assert res.is_evaded is True, f"Attack at delta={delta_ms}ms should be evaded!"
            assert res.final_damage == 0.0, f"Evaded attack must yield 0 damage, got {res.final_damage}"
            assert defender.current_hp == 500.0, "Defender HP must not decrease on evaded attack"
        else:
            assert res.is_evaded is False, f"Attack at delta={delta_ms}ms should NOT be evaded!"
            assert res.final_damage > 0.0, f"Non-evaded attack must yield >0 damage, got {res.final_damage}"
            assert defender.current_hp < 500.0, "Defender HP must decrease on hit"

    def test_sequential_dodge_resets_and_extends_iframe(
        self,
        setup_combat: tuple[CombatEngine, CombatActor, CombatActor]
    ) -> None:
        """Second dodge during active i-frame resets the 250ms window forward."""
        engine, attacker, defender = setup_combat
        t0 = 1_000_000
        engine.trigger_phantom_evasion(defender.actor_id, t0)

        # Hit at t0 + 200ms (inside first dodge)
        res1 = engine.calculate_damage(attacker.actor_id, defender.actor_id, 100.0, FiveElements.HOA, t0 + 200)
        assert res1.is_evaded is True

        # Second dodge at t0 + 200ms -> new window: [t0+200, t0+450]
        engine.trigger_phantom_evasion(defender.actor_id, t0 + 200)

        # Hit at t0 + 400ms (would have expired under 1st dodge, but valid under 2nd)
        res2 = engine.calculate_damage(attacker.actor_id, defender.actor_id, 100.0, FiveElements.HOA, t0 + 400)
        assert res2.is_evaded is True

        # Hit at t0 + 450ms (boundary of 2nd dodge)
        res3 = engine.calculate_damage(attacker.actor_id, defender.actor_id, 100.0, FiveElements.HOA, t0 + 450)
        assert res3.is_evaded is True

        # Hit at t0 + 451ms (expired)
        res4 = engine.calculate_damage(attacker.actor_id, defender.actor_id, 100.0, FiveElements.HOA, t0 + 451)
        assert res4.is_evaded is False
        assert res4.final_damage > 0.0


class TestWeaponSwingArchetypesCatalog:
    """Verifies all 4 weapon swing archetypes and their parameters in weapon_swing_catalog.js."""

    @pytest.fixture(autouse=True)
    def load_catalog(self) -> None:
        catalog_path = PROJECT_ROOT / "client" / "webapp" / "js" / "data" / "weapon_swing_catalog.js"
        self.code = catalog_path.read_text(encoding="utf-8")

    def test_four_archetype_ids_exist(self) -> None:
        for arch in ["SLASH:", "THRUST:", "SLAM:", "BOLT:"]:
            assert arch in self.code, f"Archetype {arch} must exist in catalog"

    def test_slash_archetype_specs(self) -> None:
        assert "id: 'SLASH'" in self.code
        assert "style: 'arc'" in self.code
        assert "hitStopFrames: 3" in self.code
        assert "hitStopDuration: 0.05" in self.code
        assert "sweepAngleDeg: 135" in self.code
        assert "reachRadius: 56" in self.code
        assert "particleType: 'fire'" in self.code

    def test_thrust_archetype_specs(self) -> None:
        assert "id: 'THRUST'" in self.code
        assert "style: 'cone'" in self.code
        assert "hitStopFrames: 2" in self.code
        assert "hitStopDuration: 0.035" in self.code
        assert "sweepAngleDeg: 35" in self.code
        assert "reachRadius: 84" in self.code
        assert "particleType: 'thunder'" in self.code

    def test_slam_archetype_specs(self) -> None:
        assert "id: 'SLAM'" in self.code
        assert "style: 'radial'" in self.code
        assert "hitStopFrames: 4" in self.code
        assert "hitStopDuration: 0.066" in self.code
        assert "sweepAngleDeg: 360" in self.code
        assert "reachRadius: 76" in self.code
        assert "particleType: 'earth'" in self.code

    def test_bolt_archetype_specs(self) -> None:
        assert "id: 'BOLT'" in self.code
        assert "style: 'beam'" in self.code
        assert "hitStopFrames: 2" in self.code
        assert "hitStopDuration: 0.035" in self.code
        assert "sweepAngleDeg: 20" in self.code
        assert "reachRadius: 180" in self.code
        assert "particleType: 'phantom'" in self.code


class TestNodeJsEmpiricalHarnessExecution:
    """Executes the full Node.js runtime stress harness to empirically verify JS modules."""

    def test_run_nodejs_stress_harness(self) -> None:
        harness_path = Path(__file__).parent / "harness_m2_empirical.mjs"
        assert harness_path.exists(), "harness_m2_empirical.mjs must exist"

        proc = subprocess.run(
            ["node", str(harness_path)],
            capture_output=True,
            text=True,
            cwd=str(PROJECT_ROOT)
        )
        assert proc.returncode == 0, f"Node.js empirical harness failed:\nSTDOUT:\n{proc.stdout}\nSTDERR:\n{proc.stderr}"
        assert "=== ALL 5 EMPIRICAL HARNESS CHECKS PASSED ===" in proc.stdout
        assert "PASS: Animation cancelling interrupts wind-up" in proc.stdout
        assert "PASS: Client i-frame bounds strictly: evaded at 0ms, 249ms; damaged at 251ms" in proc.stdout
        assert "PASS: 3-charge pool limits, depletion lockout" in proc.stdout
        assert "PASS: All 4 archetypes (SLASH, THRUST, SLAM, BOLT)" in proc.stdout
        assert "PASS: Hit-stop freeze, NaN/undefined safety" in proc.stdout
