"""
Adversarial Stress Testing & Empirical Verification Suite:
Monster Pack Affixes, Leader Auras, Radial Placement & Coordinated AI (Milestone 3).

Verifies:
1. Affix scaling across levels 1 to 100: T1-T15 distribution, stat multiplier boundaries, no NaNs/negatives.
2. Deterministic seed reproduction: identical packs for same seed, distinct for different seeds.
3. Extreme minion counts (10-20): radial placement 1.2m <= r <= 3.5m, no overlapping coordinates.
4. Aura buff propagation (<= 14.0m) and instant decay (14.01m / 14.1m / leader death / overkill).
5. Coordinated pursuit alerting: single minion alert alerts all living pack members; dead/leashing uncorrupted.
6. Client-side Node.js pack populator, aura threshold (6.5m), and safe haven gating.
"""

from __future__ import annotations
import json
import math
import subprocess
from typing import List, Tuple
import pytest

from server.world.monster_types import (
    MonsterRank,
    MonsterAIState,
    PackAuraType,
    MonsterAffixCategory,
    MonsterRuntimeState,
    MonsterPack,
)
from server.world.monster_procedural_engine import MonsterProceduralEngine
from server.world.monster_affix_catalog import (
    get_tier_for_level,
    roll_monster_affixes,
    apply_affixes_to_stats,
    MONSTER_AFFIX_CATALOG,
    TIER_SPECS,
)
from server.world.monster_ai_engine import MonsterAIEngine


class TestPackAffixesAndAurasAdversarial:
    """Empirical adversarial test suite for Milestone 3 pack and aura systems."""

    def test_affix_scaling_levels_1_to_100_no_nans_or_negatives(self) -> None:
        """Verifies affix scaling, tier boundaries, and positive finite stats across levels 1-100."""
        for lvl in range(1, 101):
            tier = get_tier_for_level(lvl)
            assert 1 <= tier <= 15, f"Level {lvl} produced invalid tier {tier}"

        for lvl in (1, 10, 25, 50, 70, 85, 100):
            for rank in (MonsterRank.COMMON, MonsterRank.MAGIC, MonsterRank.RARE, MonsterRank.BOSS):
                for seed in range(5):
                    affixes = roll_monster_affixes(level=lvl, rank=rank, seed=seed * 100 + lvl)
                    for aff in affixes:
                        assert 1 <= aff.tier <= 15
                        for mult_key, mult_val in aff.stat_multipliers.items():
                            assert math.isfinite(mult_val) and mult_val > 0.0, f"Bad mult in {aff.affix_id}"
                    hp, dmg, arm = apply_affixes_to_stats(1000.0, 100.0, 50.0, affixes)
                    assert math.isfinite(hp) and hp > 0.0
                    assert math.isfinite(dmg) and dmg > 0.0
                    assert math.isfinite(arm) and arm > 0.0

        # Boundary checks: non-standard levels
        for boundary_lvl in (0, -5, 150):
            t = get_tier_for_level(boundary_lvl)
            assert 1 <= t <= 15
            affs = roll_monster_affixes(boundary_lvl, MonsterRank.RARE, 42)
            h, d, a = apply_affixes_to_stats(500.0, 50.0, 20.0, affs)
            assert h > 0 and d > 0 and a > 0

    def test_catalog_affixes_and_monotonicity(self) -> None:
        """Verifies catalog completeness and monotonic progression from T1 (Apex) to T15 (Novice)."""
        assert len(MONSTER_AFFIX_CATALOG) == 15
        for t in range(1, 16):
            affixes = MONSTER_AFFIX_CATALOG[t]
            assert len(affixes) == 6, f"Tier {t} missing affix categories"
            categories = {a.category for a in affixes}
            assert len(categories) == 6

        # Check T1 vs T15 multipliers
        t1_spec = next(s for s in TIER_SPECS if s["tier"] == 1)
        t15_spec = next(s for s in TIER_SPECS if s["tier"] == 15)
        assert t1_spec["hp_m"] > t15_spec["hp_m"]
        assert t1_spec["dmg_m"] > t15_spec["dmg_m"]
        assert t1_spec["arm_m"] > t15_spec["arm_m"]
        assert t1_spec["min_lvl"] == 85 and t15_spec["min_lvl"] == 1

    def test_deterministic_seed_reproduction_and_entropy(self) -> None:
        """Verifies identical reproduction for identical seeds and diversity across distinct seeds."""
        engine = MonsterProceduralEngine()
        for seed in (42, 777, 13377):
            p1 = engine.generate_pack("p_det", "zone_tang_kiem_nhai", 40, 10.0, 10.0, seed, 5)
            p2 = engine.generate_pack("p_det", "zone_tang_kiem_nhai", 40, 10.0, 10.0, seed, 5)
            assert p1.leader.monster_id == p2.leader.monster_id
            assert p1.leader.leader_aura == p2.leader.leader_aura
            assert len(p1.minions) == len(p2.minions)
            for m1, m2 in zip(p1.minions, p2.minions):
                assert m1.monster_id == m2.monster_id
                assert m1.max_hp == m2.max_hp

            st1 = engine.create_pack_runtime_states(p1, seed=seed)
            st2 = engine.create_pack_runtime_states(p2, seed=seed)
            for s1, s2 in zip(st1, st2):
                assert s1.current_x == s2.current_x and s1.current_y == s2.current_y

        # Different seeds produce different compositions
        leaders = {engine.generate_pack("p", "zone_tang_kiem_nhai", 30, 0, 0, s, 3).leader.monster_id for s in range(30)}
        assert len(leaders) > 20, "Insufficient seed entropy in pack generation"

    def test_extreme_minion_counts_and_radial_placement(self) -> None:
        """Verifies radial placement 1.2m <= r <= 3.5m and collision prevention for 10-20 minions."""
        engine = MonsterProceduralEngine()
        for count in (10, 15, 20):
            for seed in (101, 202, 303, 404):
                pack = engine.generate_pack("p_ext", "zone_tang_kiem_nhai", 30, 50.0, 50.0, seed, count)
                states = engine.create_pack_runtime_states(pack, seed=seed)
                leader_st = states[0]
                minion_states = states[1:]
                assert len(minion_states) == count

                # Radius check: 1.2m <= r <= 3.5m
                for m_st in minion_states:
                    r = math.hypot(m_st.current_x - leader_st.current_x, m_st.current_y - leader_st.current_y)
                    assert 1.20 <= r <= 3.50, f"Minion radius {r:.3f}m outside [1.2m, 3.5m]"

                # Collision / non-overlap check
                for i in range(len(minion_states)):
                    for j in range(i + 1, len(minion_states)):
                        d = math.hypot(
                            minion_states[i].current_x - minion_states[j].current_x,
                            minion_states[i].current_y - minion_states[j].current_y,
                        )
                        assert d > 0.05, f"Minions {i} and {j} overlap at distance {d:.4f}m"

    def test_aura_buff_decay_stress_distance_and_death(self) -> None:
        """Verifies aura propagation at <=14.0m, decay at >14.0m, and instant clear on leader death."""
        engine = MonsterProceduralEngine()
        ai = MonsterAIEngine()
        pack = engine.generate_pack("p_aura", "zone_tang_kiem_nhai", 30, 0.0, 0.0, 555, 2)
        states = engine.create_pack_runtime_states(pack)
        leader, minion = states[0], states[1]

        # 1. Distance thresholds: <=14.0m active, >14.0m decays
        minion.current_x, minion.current_y = 14.0, 0.0
        ai.update_pack(states, player_pos=(500.0, 500.0), dt=0.033)
        assert len(minion.active_buffs) > 0, "Buff not applied at 14.0m"

        minion.current_x = 14.05
        ai.update_pack(states, player_pos=(500.0, 500.0), dt=0.033)
        assert len(minion.active_buffs) == 0, "Buff did not decay at 14.05m"

        minion.current_x = 14.10
        ai.update_pack(states, player_pos=(500.0, 500.0), dt=0.033)
        assert len(minion.active_buffs) == 0, "Buff did not decay at 14.10m"

        # 2. Leader death conditions (hp=0, overkill hp=-50, DEAD state)
        minion.current_x = 5.0
        ai.update_pack(states, player_pos=(500.0, 500.0), dt=0.033)
        assert len(minion.active_buffs) > 0

        leader.current_hp = 0.0
        ai.update_pack(states, player_pos=(500.0, 500.0), dt=0.033)
        assert len(minion.active_buffs) == 0 and len(leader.active_buffs) == 0

        leader.current_hp = -50.0
        ai.update_pack(states, player_pos=(500.0, 500.0), dt=0.033)
        assert len(minion.active_buffs) == 0

        leader.current_hp = leader.monster.max_hp
        leader.ai_state = MonsterAIState.DEAD
        ai.update_pack(states, player_pos=(500.0, 500.0), dt=0.033)
        assert len(minion.active_buffs) == 0

    def test_coordinated_pack_pursuit_alerting(self) -> None:
        """Verifies alerting 1 minion alerts all alive pack members while preserving dead/leash states."""
        engine = MonsterProceduralEngine()
        ai = MonsterAIEngine()
        pack = engine.generate_pack("p_coord", "zone_tang_kiem_nhai", 25, 0.0, 0.0, 888, 6)
        states = engine.create_pack_runtime_states(pack)
        for s in states:
            s.ai_state = MonsterAIState.IDLE

        # Dead minion and actively leashing minion (pulled 8m from spawn)
        states[2].ai_state = MonsterAIState.DEAD
        states[2].current_hp = 0.0
        states[3].ai_state = MonsterAIState.LEASH_RETURN
        states[3].current_x = states[3].spawn_x + 8.0

        # Alert minion #5 by placing player near it
        target_m = states[5]
        player_near = (target_m.current_x + 0.5, target_m.current_y)
        ai.update_pack(states, player_pos=player_near, dt=0.033)

        assert states[0].ai_state == MonsterAIState.PURSUIT, "Leader failed to alert"
        assert states[1].ai_state == MonsterAIState.PURSUIT, "Minion 1 failed to alert"
        assert states[4].ai_state == MonsterAIState.PURSUIT, "Minion 4 failed to alert"
        assert states[5].ai_state in (MonsterAIState.PURSUIT, MonsterAIState.ATTACK)
        assert states[2].ai_state == MonsterAIState.DEAD, "Dead minion resurrected by alert"
        assert states[3].ai_state == MonsterAIState.LEASH_RETURN, "Leashing minion aborted leash"

    def test_client_monster_pack_system_node_empirical(self) -> None:
        """Executes Node.js harness to empirically test client pack spawning, 6.5m aura, and safe haven gating."""
        node_script = """
        import { populateZonePacks, updateMonsterPackAI } from './client/webapp/js/engine/monster_pack_system.js';
        let idCounter = 1;
        const mockCreate = (cfg) => ({ id: cfg.id || 'm_' + idCounter++, hp: cfg.maxHp || 1000, ...cfg, hasAggro: false, activeAura: null });

        // Safe Haven Check: 0 packs
        const safePacks = populateZonePacks('zone_player_hideout', mockCreate);
        if (safePacks.length !== 0) throw new Error('Safe haven populated hostile packs!');

        // Wilderness Check: populated packs
        const wildEntities = populateZonePacks('zone_tang_kiem_nhai', mockCreate);
        if (wildEntities.length < 5) throw new Error('Wilderness failed to populate packs');
        const leader = wildEntities.find(e => e.isPackLeader);
        const minion = wildEntities.find(e => !e.isPackLeader && e.packId === leader.packId);
        if (!leader || !minion) throw new Error('Missing leader or minion');

        // Aura proximity check (threshold <= 6.5m)
        minion.wx = leader.wx + 5.0; minion.wy = leader.wy;
        updateMonsterPackAI(wildEntities, 0.033);
        if (!minion.activeAura) throw new Error('Minion missing active aura within 6.5m');

        minion.wx = leader.wx + 8.0;
        updateMonsterPackAI(wildEntities, 0.033);
        if (minion.activeAura !== null) throw new Error('Minion aura did not decay at 8.0m');

        // Leader death check
        minion.wx = leader.wx + 3.0;
        leader.hp = 0;
        updateMonsterPackAI(wildEntities, 0.033);
        if (minion.activeAura !== null) throw new Error('Minion aura did not decay on leader death');

        // Coordinated alerting check
        leader.hp = 1000;
        minion.hasAggro = true;
        updateMonsterPackAI(wildEntities, 0.033);
        if (!leader.hasAggro) throw new Error('Leader did not receive coordinated aggro from minion');

        console.log(JSON.stringify({ success: true, count: wildEntities.length }));
        """
        res = subprocess.run(["node", "--input-type=module", "-e", node_script], capture_output=True, text=True, check=True)
        data = json.loads(res.stdout.strip())
        assert data["success"] is True
