"""
Comprehensive E2E Test Suite for FreeExile PoE2 Spatial Zoning & Encounter Systems.
Enforces 4-Tier Testing Methodology:
- Tier 1: Feature Coverage (>=5 test cases per feature area)
- Tier 2: Boundary & Corner Cases
- Tier 3: Cross-Feature Combinations
- Tier 4: Real-World End-to-End Application Scenarios
"""

import math
import os
import sys
import time
from pathlib import Path
from typing import Dict, List, Tuple
import pytest

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

from server.world.zone_engine import ZoneEngine
from server.world.zone_types import ZoneType, ZoneDefinition
from server.world.hideout_engine import HideoutEngine, AstralMap
from server.world.combat_engine import CombatEngine, CombatActor
from server.world.martial_matrix import FiveElements
from server.world.monster_types import MonsterAIState, MonsterRuntimeState, MonsterGenus
from server.world.monster_ai_engine import MonsterAIEngine
from server.world.monster_procedural_engine import MonsterProceduralEngine

@pytest.fixture
def zone_engine() -> ZoneEngine:
    return ZoneEngine()

@pytest.fixture
def hideout_engine() -> HideoutEngine:
    return HideoutEngine()

@pytest.fixture
def combat_setup() -> Tuple[CombatEngine, CombatActor, CombatActor]:
    engine = CombatEngine()
    attacker = CombatActor(actor_id=1, name="Quái Tinh Anh", element=FiveElements.HOA, base_attack=85.0)
    defender = CombatActor(actor_id=2, name="Lưu Đày Giả", element=FiveElements.KIM, current_hp=1000.0, max_hp=1000.0)
    engine.register_actor(attacker)
    engine.register_actor(defender)
    return engine, attacker, defender

class TestTier1FeatureCoverage:
    """Tier 1: Feature Coverage (>=5 test cases per feature area from ORIGINAL_REQUEST)."""
    # --- Area 1: Safe Haven Purity (5 cases) ---
    def test_safe_haven_sanctuary_hostile_rejected(self, zone_engine: ZoneEngine) -> None:
        """Sanctuary hub rejects hostile mob spawning."""
        assert zone_engine.can_spawn_hostile_monsters("zone_boundless_sanctuary") is False
    def test_safe_haven_hideout_hostile_rejected(self, zone_engine: ZoneEngine) -> None:
        """Player hideout rejects hostile mob spawning."""
        assert zone_engine.can_spawn_hostile_monsters("zone_player_hideout") is False
    def test_safe_haven_sanctuary_validate_rejects_hostile(self, zone_engine: ZoneEngine) -> None:
        """validate_monster_spawn rejects hostile mob in sanctuary."""
        allowed, reason = zone_engine.validate_monster_spawn("zone_boundless_sanctuary", is_dummy=False)
        assert allowed is False and ("An Toàn" in reason or "Sanctuary" in reason)
    def test_safe_haven_hideout_validate_rejects_hostile(self, zone_engine: ZoneEngine) -> None:
        """validate_monster_spawn rejects hostile mob in hideout."""
        allowed, reason = zone_engine.validate_monster_spawn("zone_player_hideout", is_dummy=False)
        assert allowed is False and ("An Toàn" in reason or "Hideout" in reason)
    def test_safe_haven_client_js_no_hostile_mobs(self) -> None:
        """Client monster_system.js must not spawn hostile hounds/skeletons in safe havens."""
        content = Path("client/webapp/js/engine/monster_system.js").read_text(encoding="utf-8")
        assert "hellhound_sanctuary" not in content and "skeleton_sanctuary" not in content

    # --- Area 2: Target Dummy Validation (5 cases) ---
    def test_dummy_allowed_in_sanctuary(self, zone_engine: ZoneEngine) -> None:
        """Target Dummy is permitted in sanctuary."""
        assert zone_engine.validate_monster_spawn("zone_boundless_sanctuary", is_dummy=True)[0] is True
    def test_dummy_allowed_in_hideout(self, zone_engine: ZoneEngine) -> None:
        """Target Dummy is permitted in hideout."""
        assert zone_engine.validate_monster_spawn("zone_player_hideout", is_dummy=True)[0] is True
    def test_dummy_allowed_in_wilderness(self, zone_engine: ZoneEngine) -> None:
        """Target Dummy is permitted in wilderness."""
        assert zone_engine.validate_monster_spawn("zone_tang_kiem_nhai", is_dummy=True)[0] is True
    def test_dummy_configured_in_client_js(self) -> None:
        """Client monster_system.js configures dummy with isDummy: true and isNeutral: true."""
        content = Path("client/webapp/js/engine/monster_system.js").read_text(encoding="utf-8")
        assert "dummy_primal_shrine" in content and "isDummy: true" in content and "isNeutral: true" in content
    def test_dummy_stationary_in_client_js(self) -> None:
        """Client monster_system.js skips movement and attacks for target dummy."""
        assert "if (m.isDummy) continue;" in Path("client/webapp/js/engine/monster_system.js").read_text(encoding="utf-8")

    # --- Area 3: Wilderness Encounter Gating (5 cases) ---
    def test_wilderness_bone_strand_allows_hostile(self, zone_engine: ZoneEngine) -> None:
        """Bone Strand wilderness map allows hostile mobs."""
        assert zone_engine.can_spawn_hostile_monsters("zone_tang_kiem_nhai") is True

    def test_wilderness_rotten_swamp_allows_hostile(self, zone_engine: ZoneEngine) -> None:
        """Rotten Swamp wilderness map allows hostile mobs."""
        assert zone_engine.can_spawn_hostile_monsters("zone_ancient_sword_barrow") is True

    def test_wilderness_bone_wastes_allows_hostile(self, zone_engine: ZoneEngine) -> None:
        """Bone Wastes wilderness map allows hostile mobs."""
        assert zone_engine.can_spawn_hostile_monsters("zone_boundless_sandstorm") is True

    def test_wilderness_validate_spawn_allows_hostile(self, zone_engine: ZoneEngine) -> None:
        """validate_monster_spawn permits hostile spawns in outer maps."""
        assert zone_engine.validate_monster_spawn("zone_tang_kiem_nhai", is_dummy=False)[0] is True

    def test_wilderness_procedural_monster_attributes(self) -> None:
        """Procedural monster generation generates valid wilderness enemies."""
        m = MonsterProceduralEngine().generate_monster(42, target_level=15)
        assert m.max_hp > 0 and m.base_damage > 0 and m.genus in MonsterGenus

    # --- Area 4: Dodge i-Frame (250ms) (5 cases) ---
    def test_dodge_trigger_records_timestamp(self, combat_setup) -> None:
        """Triggering Huyễn Ảnh Bộ records last evasion timestamp."""
        engine, _, defn = combat_setup
        now_ms = int(time.time() * 1000)
        assert engine.trigger_phantom_evasion(actor_id=defn.actor_id, timestamp_ms=now_ms) is True
        assert defn.last_evasion_timestamp_ms == now_ms

    def test_dodge_iframe_active_at_zero_ms(self, combat_setup) -> None:
        """Attack landing at t+0ms is completely evaded."""
        engine, atk, defn = combat_setup
        now_ms = int(time.time() * 1000)
        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)
        assert res.is_evaded is True and res.final_damage == 0.0

    def test_dodge_iframe_active_at_hundred_ms(self, combat_setup) -> None:
        """Attack landing at t+100ms is completely evaded."""
        engine, atk, defn = combat_setup
        now_ms = int(time.time() * 1000)
        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 + 100)
        assert res.is_evaded is True and res.final_damage == 0.0

    def test_dodge_iframe_active_at_boundary_249ms(self, combat_setup) -> None:
        """Attack landing at t+249ms (inside 250ms window) is evaded."""
        engine, atk, defn = combat_setup
        now_ms = int(time.time() * 1000)
        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)
        assert res.is_evaded is True and res.final_damage == 0.0

    def test_dodge_client_js_sets_iframe_window(self) -> None:
        """Client combat_skills.js activates 0.25s i-frame window."""
        c = Path("client/webapp/js/engine/combat_skills.js").read_text(encoding="utf-8")
        assert "player.isIFrame = true;" in c and "player.iFrameTimer = 0.25;" in c

    # --- Area 5: Poise & Stagger (5 cases) ---
    def test_poise_initialized_on_monster(self) -> None:
        """Monsters in monster_system.js initialize poise and maxPoise."""
        c = Path("client/webapp/js/engine/monster_system.js").read_text(encoding="utf-8")
        assert "poise: config.poise || 100" in c and "maxPoise: config.poise || 100" in c

    def test_stagger_state_stun_skips_actions(self) -> None:
        """Monster AI loop pauses during active staggerTimer."""
        c = Path("client/webapp/js/engine/monster_system.js").read_text(encoding="utf-8")
        assert "if (m.staggerTimer > 0)" in c and "continue; // Stunned during stagger window!" in c

    def test_boss_stagger_four_second_duration(self) -> None:
        """Boss stagger sets 4.0s timer in combat_skills.js."""
        c = Path("client/webapp/js/engine/combat_skills.js").read_text(encoding="utf-8")
        assert "b.staggerTimer = 4.0;" in c and "b.poise = 0;" in c

    def test_stagger_combat_actor_evasion_intact(self, combat_setup) -> None:
        """Staggered combat state integrates cleanly with damage calculation."""
        engine, atk, defn = combat_setup
        res = engine.calculate_damage(atk.actor_id, defn.actor_id, 50.0, FiveElements.HOA, 1000)
        assert res.final_damage > 0 and defn.current_hp < defn.max_hp

    def test_telegraph_renderer_module_baseline(self) -> None:
        """Verifies telegraph_renderer.js exists for warning cones & circles."""
        assert Path("client/webapp/js/engine/telegraph_renderer.js").exists()

    # --- Area 6: AI Leashing (5 cases) ---
    def test_ai_pursuit_within_aggro(self) -> None:
        """Monster enters PURSUIT when player is inside aggro radius."""
        ai, m = MonsterAIEngine(), MonsterProceduralEngine().generate_monster(10, target_level=10)
        st = MonsterRuntimeState(monster=m, current_x=0.0, current_y=0.0, spawn_x=0.0, spawn_y=0.0, current_hp=m.max_hp)
        act = ai.update(st, player_pos=(3.0, 0.0), dt=0.033)
        assert st.ai_state == MonsterAIState.PURSUIT and act.action_type == "MOVE_TOWARDS"

    def test_ai_attack_within_melee(self) -> None:
        """Monster enters ATTACK when player is within melee range."""
        ai, m = MonsterAIEngine(), MonsterProceduralEngine().generate_monster(10, target_level=10)
        st = MonsterRuntimeState(monster=m, current_x=0.0, current_y=0.0, spawn_x=0.0, spawn_y=0.0, current_hp=m.max_hp)
        act = ai.update(st, player_pos=(0.8, 0.0), dt=0.033)
        assert st.ai_state == MonsterAIState.ATTACK and act.action_type == "ATTACK"

    def test_ai_leash_triggered_beyond_radius(self) -> None:
        """Monster enters LEASH_RETURN when pulled beyond leash radius."""
        ai, m = MonsterAIEngine(), MonsterProceduralEngine().generate_monster(10, target_level=10)
        st = MonsterRuntimeState(monster=m, current_x=80.0, current_y=80.0, spawn_x=0.0, spawn_y=0.0, current_hp=m.max_hp)
        act = ai.update(st, player_pos=(81.0, 80.0), dt=0.033)
        assert st.ai_state == MonsterAIState.LEASH_RETURN and act.action_type == "RETURN_TO_SPAWN"

    def test_ai_leash_return_grants_invulnerability(self) -> None:
        """Monster becomes invulnerable during LEASH_RETURN."""
        ai, m = MonsterAIEngine(), MonsterProceduralEngine().generate_monster(10, target_level=10)
        st = MonsterRuntimeState(monster=m, current_x=80.0, current_y=80.0, spawn_x=0.0, spawn_y=0.0, current_hp=m.max_hp)
        ai.update(st, player_pos=(81.0, 80.0), dt=0.033)
        assert st.is_invulnerable is True

    def test_ai_leash_return_regenerates_health(self) -> None:
        """Monster regenerates 30% HP/sec during LEASH_RETURN."""
        ai, m = MonsterAIEngine(), MonsterProceduralEngine().generate_monster(10, target_level=10)
        st = MonsterRuntimeState(monster=m, current_x=50.0, current_y=50.0, spawn_x=0.0, spawn_y=0.0, current_hp=m.max_hp * 0.2)
        initial_hp = st.current_hp
        ai.update(st, player_pos=(51.0, 50.0), dt=1.0)
        assert st.current_hp > initial_hp
        assert math.isclose(st.current_hp - initial_hp, m.max_hp * 0.30, rel_tol=1e-2)

    # --- Area 7: Map Device 6-Portal (5 cases) ---
    def test_map_device_activates_six_portals_t1(self, hideout_engine: HideoutEngine) -> None:
        """Astral Map Device opens exactly 6 portals for Tier 1 map."""
        ok, _, portals = hideout_engine.activate_map_device("p1", AstralMap("m1", "T1", 1, "zone_tang_kiem_nhai"))
        assert ok is True and len(portals) == 6

    def test_map_device_activates_six_portals_t16(self, hideout_engine: HideoutEngine) -> None:
        """Astral Map Device opens exactly 6 portals for Tier 16 map."""
        ok, _, portals = hideout_engine.activate_map_device("p1", AstralMap("m16", "T16", 16, "zone_blood_scale_ruins"))
        assert ok is True and len(portals) == 6

    def test_map_portal_single_consumption(self, hideout_engine: HideoutEngine) -> None:
        """Entering map portal consumes exactly 1 portal charge (6 -> 5)."""
        hideout_engine.activate_map_device("p1", AstralMap("m5", "T5", 5, "zone_ancient_sword_barrow"))
        ok, _, res = hideout_engine.enter_map_portal("p1")
        assert ok is True and res["portals_remaining"] == 5

    def test_map_portal_full_depletion_to_zero(self, hideout_engine: HideoutEngine) -> None:
        """Entering map portal 6 times depletes charges to 0."""
        hideout_engine.activate_map_device("p1", AstralMap("m5", "T5", 5, "zone_ancient_sword_barrow"))
        for _ in range(6):
            hideout_engine.enter_map_portal("p1")
        assert hideout_engine.get_hideout_summary("p1")["map_device"]["portals_remaining"] == 0

    def test_map_device_sealed_at_zero(self, hideout_engine: HideoutEngine) -> None:
        """Further portal entry attempts after 6 consumptions are rejected."""
        hideout_engine.activate_map_device("p1", AstralMap("m5", "T5", 5, "zone_ancient_sword_barrow"))
        for _ in range(6):
            hideout_engine.enter_map_portal("p1")
        ok, msg, _ = hideout_engine.enter_map_portal("p1")
        assert ok is False and ("0/6" in msg or "hết lượt" in msg)


class TestTier2BoundaryAndCornerCases:
    """Tier 2: Boundary & Corner Cases."""
    def test_boundary_empty_and_unknown_zone_rejected(self, zone_engine: ZoneEngine) -> None:
        """Empty or unknown zone IDs reject hostile spawning safely."""
        assert zone_engine.can_spawn_hostile_monsters("") is False
        assert zone_engine.can_spawn_hostile_monsters("nonexistent_zone_999") is False

    def test_boundary_max_distance_leash_handles_extreme_coords(self) -> None:
        """Monsters dragged to extreme coordinates (5000m) handle leash return cleanly."""
        ai, m = MonsterAIEngine(), MonsterProceduralEngine().generate_monster(5, target_level=5)
        st = MonsterRuntimeState(monster=m, current_x=5000.0, current_y=5000.0, spawn_x=0.0, spawn_y=0.0, current_hp=100.0)
        ai.update(st, player_pos=(5001.0, 5000.0), dt=0.033)
        assert st.ai_state == MonsterAIState.LEASH_RETURN and not math.isnan(st.current_x) and st.is_invulnerable is True

    def test_boundary_iframe_expired_at_251ms(self, combat_setup) -> None:
        """Attack landing at t+251ms (beyond 250ms window) is NOT evaded."""
        engine, atk, defn = combat_setup
        now_ms = 10000
        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)
        assert res.is_evaded is False and res.final_damage > 0.0

    @pytest.mark.parametrize("invalid_tier", [0, -1, 17, 99])
    def test_boundary_invalid_map_tiers_rejected(self, hideout_engine: HideoutEngine, invalid_tier: int) -> None:
        """Map tiers <1 or >16 must be rejected with informative error."""
        ok, msg, portals = hideout_engine.activate_map_device("p1", AstralMap("b", "B", invalid_tier, "zone_tang_kiem_nhai"))
        assert ok is False and len(portals) == 0 and "Tier 1-16" in msg

    def test_boundary_player_movement_bounds_clamping(self, zone_engine: ZoneEngine) -> None:
        """Player movement within zone half-extents succeeds; outside fails."""
        zone_engine.spawn_player("p_move", "zone_boundless_sanctuary", 0.0, 0.0)
        assert zone_engine.update_player_position("p_move", 350.0, 350.0)[0] is True
        ok_out, msg = zone_engine.update_player_position("p_move", 450.0, 0.0)
        assert ok_out is False and "Vượt quá ranh giới" in msg

class TestTier3CrossFeatureCombinations:
    """Tier 3: Cross-Feature Combinations."""
    def test_portal_traversal_crosses_sanctuary_to_wilderness(self, zone_engine: ZoneEngine) -> None:
        """Player transitions through portal from Sanctuary to Wilderness, updating spatial state."""
        p_id = "cross_player"
        zone_engine.spawn_player(p_id, "zone_boundless_sanctuary", 0.0, 0.0)
        ok, target_zone, _ = zone_engine.traverse_portal(p_id, "portal_sanctuary_to_tang_kiem", player_level=5)
        assert ok is True and target_zone == "zone_tang_kiem_nhai"
        assert zone_engine.get_player_location(p_id).zone_id == "zone_tang_kiem_nhai"

    def test_dodge_during_monster_attack_windup(self, combat_setup) -> None:
        """Player executing dodge roll during incoming monster attack avoids lethal damage."""
        engine, atk, defn = combat_setup
        now_ms = 50000
        engine.trigger_phantom_evasion(defn.actor_id, now_ms)
        res = engine.calculate_damage(atk.actor_id, defn.actor_id, 250.0, FiveElements.HOA, now_ms + 120)
        assert res.is_evaded is True and defn.current_hp == defn.max_hp

    def test_leash_invulnerability_blocks_damage(self) -> None:
        """Monster in LEASH_RETURN state possesses authoritative invulnerability."""
        ai, m = MonsterAIEngine(), MonsterProceduralEngine().generate_monster(77, target_level=20)
        st = MonsterRuntimeState(monster=m, current_x=120.0, current_y=120.0, spawn_x=0.0, spawn_y=0.0, current_hp=m.max_hp * 0.5)
        ai.update(st, player_pos=(121.0, 120.0), dt=0.033)
        assert st.ai_state == MonsterAIState.LEASH_RETURN and st.is_invulnerable is True

    def test_staggered_monster_cannot_damage_player(self) -> None:
        """Staggered monster timer prevents attack generation in AI state machine."""
        assert "if (m.staggerTimer > 0) {" in Path("client/webapp/js/engine/monster_system.js").read_text(encoding="utf-8")

class TestTier4RealWorldScenarios:
    """Tier 4: Real-World Application Scenarios."""
    def test_e2e_full_player_journey_lifecycle(self, zone_engine: ZoneEngine, hideout_engine: HideoutEngine) -> None:
        """Complete E2E simulation: Sanctuary -> Wilderness -> Combat -> Dodge -> Victory -> Hideout -> Map Device."""
        player_id = "hero_e2e_journey"
        # 1. Spawn at Sanctuary Hub & verify purity
        loc = zone_engine.spawn_player(player_id, "zone_boundless_sanctuary", 0.0, 0.0)
        assert zone_engine.can_spawn_hostile_monsters(loc.zone_id) is False
        assert zone_engine.validate_monster_spawn(loc.zone_id, is_dummy=False)[0] is False
        assert zone_engine.validate_monster_spawn(loc.zone_id, is_dummy=True)[0] is True
        # 2. Portal to Wilderness
        ok, w_zone, _ = zone_engine.traverse_portal(player_id, "portal_sanctuary_to_tang_kiem", player_level=10)
        assert ok is True and zone_engine.can_spawn_hostile_monsters(w_zone) is True
        # 3. Wilderness Combat & Evasion
        combat = CombatEngine()
        mob = CombatActor(101, "Cuồng Khuyển", FiveElements.HOA, current_hp=300.0, max_hp=300.0, base_attack=70.0)
        hero = CombatActor(102, "Hero", FiveElements.THUY, current_hp=1000.0, max_hp=1000.0, base_attack=150.0)
        combat.register_actor(mob)
        combat.register_actor(hero)
        now_ms = 100000
        combat.trigger_phantom_evasion(hero.actor_id, now_ms)
        evaded_strike = combat.calculate_damage(mob.actor_id, hero.actor_id, mob.base_attack, mob.element, now_ms + 50)
        assert evaded_strike.is_evaded is True and hero.current_hp == 1000.0
        counter_strike = combat.calculate_damage(hero.actor_id, mob.actor_id, hero.base_attack, hero.element, now_ms + 300)
        assert counter_strike.is_evaded is False and counter_strike.final_damage >= 150.0 * 1.2
        # 4. Teleport to Hideout & Map Device 6-portals
        astral_map = AstralMap("map_endgame_t14", "Phế Tích Xẻ Thịt", 14, "zone_blood_scale_ruins")
        ok_dev, _, portals = hideout_engine.activate_map_device(player_id, astral_map)
        assert ok_dev is True and len(portals) == 6
        ok_enter, _, res = hideout_engine.enter_map_portal(player_id)
        assert ok_enter is True and res["portals_remaining"] == 5

    def test_e2e_canonical_hideout_registration_baseline(self, zone_engine: ZoneEngine) -> None:
        """Verifies zone_player_hideout is registered in ZoneEngine without KeyError."""
        zone = zone_engine.get_zone("zone_player_hideout")
        assert zone is not None and zone.zone_type == ZoneType.SANCTUARY

    def test_dummy_telemetry_module_baseline(self) -> None:
        """Verifies target_dummy_telemetry.js exists for combo and DPS telemetry."""
        assert Path("client/webapp/js/engine/target_dummy_telemetry.js").exists()
