"""
Empirical Challenger Test Suite: Milestone 3 Remediation.
Adversarial stress-testing of Client Script Integration & Leader Aura Ground Decals.

Verifies:
1. client/webapp/index.html script tags, attributes, and ordering.
2. Node.js evaluation of wilderness_zone_packs.js, monster_pack_system.js, and ambush_trigger_system.js.
3. Window global bridge exports.
4. Canvas 2D renderLeaderAuraDecal across 5 aura types with mock context.
5. Adversarial edge cases: dead mobs, non-leaders, negative coords, undefined time, NaN guards.
6. High-frequency 10,000-frame render stress loop (zero NaN, zero throw, budget <= 0.05ms/call).
7. Safe haven firewall on pack spawning & ambush triggers.
"""

import json
import os
import subprocess
from pathlib import Path
import pytest
from bs4 import BeautifulSoup

PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
WEBAPP_DIR = PROJECT_ROOT / "client" / "webapp"
INDEX_HTML = WEBAPP_DIR / "index.html"
DATA_DIR = WEBAPP_DIR / "js" / "data"
ENGINE_DIR = WEBAPP_DIR / "js" / "engine"


class TestIndexHtmlScriptIntegration:
    """Adversarial DOM parsing and script order verification."""

    def test_index_html_line_cap_and_existence(self) -> None:
        assert INDEX_HTML.exists(), f"index.html missing at {INDEX_HTML}"
        lines = INDEX_HTML.read_text(encoding="utf-8").splitlines()
        line_count = len(lines)
        assert line_count <= 200, f"index.html exceeded soft cap (200): {line_count}"
        assert line_count <= 400, f"index.html exceeded hard cap (400): {line_count}"

    def test_script_tags_presence_and_attributes(self) -> None:
        html_content = INDEX_HTML.read_text(encoding="utf-8")
        soup = BeautifulSoup(html_content, "html.parser")
        scripts = soup.find_all("script")

        expected_scripts = [
            "js/data/wilderness_zone_packs.js",
            "js/engine/monster_pack_system.js",
            "js/engine/ambush_trigger_system.js",
        ]
        script_srcs = [s.get("src") for s in scripts if s.get("src")]

        for expected in expected_scripts:
            assert expected in script_srcs, f"Expected script tag missing: {expected}"
            tag = soup.find("script", attrs={"src": expected})
            assert tag is not None
            assert tag.get("type") == "module", f"Script {expected} must be type='module', got {tag.get('type')}"

    def test_script_tags_ordering(self) -> None:
        html_content = INDEX_HTML.read_text(encoding="utf-8")
        soup = BeautifulSoup(html_content, "html.parser")
        script_srcs = [s.get("src") for s in soup.find_all("script") if s.get("src")]

        idx_data = script_srcs.index("js/data/wilderness_zone_packs.js")
        idx_pack = script_srcs.index("js/engine/monster_pack_system.js")
        idx_ambush = script_srcs.index("js/engine/ambush_trigger_system.js")
        idx_monster = script_srcs.index("js/engine/monster_system.js")
        idx_entity = script_srcs.index("js/engine/entity_renderer.js")
        idx_main = script_srcs.index("js/main.js")

        assert idx_monster < idx_data, "monster_system.js should precede wilderness_zone_packs.js"
        assert idx_data < idx_pack, "wilderness_zone_packs.js must precede monster_pack_system.js"
        assert idx_pack < idx_ambush, "monster_pack_system.js must precede ambush_trigger_system.js"
        assert idx_ambush < idx_entity, "ambush_trigger_system.js must precede entity_renderer.js"
        assert idx_entity < idx_main, "entity_renderer.js must precede main.js"


class TestNodeJsSimulationAndAuraDecalStress:
    """Node.js runtime simulation of module execution and Canvas 2D decals."""

    NODE_TEST_SCRIPT = """
    import { isSafeHavenZone, getWildernessPacks, getZoneAmbushTriggers, SAFE_HAVEN_ZONES } from './client/webapp/js/data/wilderness_zone_packs.js';
    import { populateZonePacks, updateMonsterPackAI, renderLeaderAuraDecal } from './client/webapp/js/engine/monster_pack_system.js';
    import { activeAmbushTriggers, initZoneAmbushTriggers, updateAmbushTriggers } from './client/webapp/js/engine/ambush_trigger_system.js';

    // Mock Window environment
    globalThis.window = globalThis;
    window.SAFE_HAVEN_ZONES = SAFE_HAVEN_ZONES;
    window.isSafeHavenZone = isSafeHavenZone;
    window.getWildernessPacks = getWildernessPacks;
    window.getZoneAmbushTriggers = getZoneAmbushTriggers;
    window.populateZonePacks = populateZonePacks;
    window.updateMonsterPackAI = updateMonsterPackAI;
    window.renderLeaderAuraDecal = renderLeaderAuraDecal;
    window.getWildernessZoneMonsters = (z, fn) => populateZonePacks(z, fn);
    window.activeAmbushTriggers = activeAmbushTriggers;
    window.initZoneAmbushTriggers = initZoneAmbushTriggers;
    window.updateAmbushTriggers = updateAmbushTriggers;

    const results = {
      globalsDefined: {},
      auraStress: [],
      adversarialEdgeCases: [],
      highFreqStress: null,
      safeHavenAudit: null
    };

    const requiredGlobals = [
      'getWildernessZoneMonsters',
      'initZoneAmbushTriggers',
      'updateMonsterPackAI',
      'updateAmbushTriggers',
      'renderLeaderAuraDecal'
    ];
    for (const g of requiredGlobals) results.globalsDefined[g] = typeof window[g] === 'function';

    class MockCanvasContext {
      constructor() {
        this.saveCount = 0;
        this.restoreCount = 0;
        this.strokeStyle = '';
        this.fillStyle = '';
        this.globalAlpha = 1.0;
        this.ellipses = [];
      }
      save() { this.saveCount++; }
      restore() { this.restoreCount++; }
      beginPath() {}
      stroke() {}
      fill() {}
      ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle) {
        for (const arg of [x, y, radiusX, radiusY, rotation, startAngle, endAngle]) {
          if (typeof arg !== 'number' || Number.isNaN(arg) || !Number.isFinite(arg)) {
            throw new Error(`NaN/Non-finite argument in ellipse: ${arg}`);
          }
        }
        this.ellipses.push({ x, y, radiusX, radiusY, rotation, startAngle, endAngle });
      }
      createRadialGradient() { return { addColorStop() {} }; }
    }

    const canonicalAuras = [
      { type: 'HASTE', expectedColor: '#a855f7' },
      { type: 'DAMAGE_RESISTANCE', expectedColor: '#38bdf8' },
      { type: 'ELEMENTAL_EMPOWERMENT', expectedColor: '#f59e0b' },
      { type: 'VAMPIRIC', expectedColor: '#ef4444' },
      { type: 'MORTAL_MIGHT', expectedColor: '#ec4899' }
    ];

    for (const aura of canonicalAuras) {
      const ctx = new MockCanvasContext();
      const mob = { isPackLeader: true, hp: 5000, leaderAura: aura.type };
      let threw = false, errorMsg = null;
      try {
        renderLeaderAuraDecal(ctx, mob, 450.5, 320.25, 12.345);
      } catch (err) { threw = true; errorMsg = err.message; }

      results.auraStress.push({
        aura: aura.type, threw, errorMsg,
        saveBalanced: ctx.saveCount === 1 && ctx.restoreCount === 1,
        ellipseCount: ctx.ellipses.length,
        colorMatches: ctx.strokeStyle === aura.expectedColor && ctx.fillStyle === aura.expectedColor,
        allRadiiPositive: ctx.ellipses.every(e => e.radiusX > 0 && e.radiusY > 0),
        yOffsetApplied: ctx.ellipses.every(e => Math.abs(e.y - 324.25) < 0.001)
      });
    }

    const edgeCases = [
      { name: 'dead_mob', mob: { isPackLeader: true, hp: 0, leaderAura: 'HASTE' }, x: 100, y: 100, t: 1.0, expectRender: false },
      { name: 'negative_hp', mob: { isPackLeader: true, hp: -250, leaderAura: 'HASTE' }, x: 100, y: 100, t: 1.0, expectRender: false },
      { name: 'non_leader', mob: { isPackLeader: false, hp: 1000, leaderAura: 'HASTE' }, x: 100, y: 100, t: 1.0, expectRender: false },
      { name: 'undef_aura', mob: { isPackLeader: true, hp: 1000, leaderAura: undefined }, x: 100, y: 100, t: 1.0, expectRender: true, expectColor: '#a855f7' },
      { name: 'unknown_aura', mob: { isPackLeader: true, hp: 1000, leaderAura: 'CHAOS' }, x: 100, y: 100, t: 1.0, expectRender: true, expectColor: '#a855f7' },
      { name: 'negative_coords', mob: { isPackLeader: true, hp: 1000, leaderAura: 'VAMPIRIC' }, x: -999.5, y: -450.2, t: 2.5, expectRender: true },
      { name: 'large_coords', mob: { isPackLeader: true, hp: 1000, leaderAura: 'DAMAGE_RESISTANCE' }, x: 1e6, y: 2e6, t: 3.0, expectRender: true },
      { name: 'undef_time', mob: { isPackLeader: true, hp: 1000, leaderAura: 'HASTE' }, x: 50, y: 50, t: undefined, expectRender: true },
      { name: 'zero_time', mob: { isPackLeader: true, hp: 1000, leaderAura: 'HASTE' }, x: 50, y: 50, t: 0, expectRender: true },
      { name: 'negative_time', mob: { isPackLeader: true, hp: 1000, leaderAura: 'MORTAL_MIGHT' }, x: 50, y: 50, t: -543.21, expectRender: true }
    ];

    for (const ec of edgeCases) {
      const ctx = new MockCanvasContext();
      let threw = false, errorMsg = null;
      try {
        renderLeaderAuraDecal(ctx, ec.mob, ec.x, ec.y, ec.t);
      } catch (err) { threw = true; errorMsg = err.message; }

      results.adversarialEdgeCases.push({
        name: ec.name, threw, errorMsg,
        saveBalanced: ec.expectRender ? (ctx.saveCount === 1 && ctx.restoreCount === 1) : (ctx.saveCount === 0 && ctx.restoreCount === 0),
        renderedMatchesExpectation: (ctx.ellipses.length === 2) === ec.expectRender,
        colorMatches: ec.expectColor ? ctx.strokeStyle === ec.expectColor : true
      });
    }

    // High Frequency 10,000 frames stress loop
    const hfCtx = new MockCanvasContext();
    const hfMob = { isPackLeader: true, hp: 5000, leaderAura: 'MORTAL_MIGHT' };
    const t0 = performance.now();
    let hfThrew = false, hfErrMsg = null;
    try {
      for (let f = 0; f < 10000; f++) {
        renderLeaderAuraDecal(hfCtx, hfMob, 100 + (f % 50), 200 + (f % 50), f * 0.016);
      }
    } catch (e) { hfThrew = true; hfErrMsg = e.message; }
    const durationMs = performance.now() - t0;
    results.highFreqStress = {
      iterations: 10000,
      threw: hfThrew,
      errorMsg: hfErrMsg,
      durationMs,
      avgMsPerFrame: durationMs / 10000,
      totalEllipses: hfCtx.ellipses.length
    };

    // Safe Haven Zone Audit
    const dummyCreator = (cfg) => ({ ...cfg });
    const hideoutMobs = populateZonePacks('zone_player_hideout', dummyCreator);
    const sanctuaryMobs = populateZonePacks('zone_boundless_sanctuary', dummyCreator);
    const wildernessMobs = populateZonePacks('zone_tang_kiem_nhai', dummyCreator);
    results.safeHavenAudit = {
      hideoutMobCount: hideoutMobs.length,
      sanctuaryMobCount: sanctuaryMobs.length,
      wildernessMobCount: wildernessMobs.length,
      wildernessHasLeader: wildernessMobs.some(m => m.isPackLeader && m.leaderAura)
    };

    console.log(JSON.stringify(results, null, 2));
    """

    def test_run_node_simulation_and_verify(self) -> None:
        cmd = ["node", "--input-type=module", "-e", self.NODE_TEST_SCRIPT]
        res = subprocess.run(cmd, cwd=str(PROJECT_ROOT), capture_output=True, text=True)
        assert res.returncode == 0, f"Node.js script failed with stderr: {res.stderr}"

        data = json.loads(res.stdout)

        # 1. Globals
        for name, is_def in data["globalsDefined"].items():
            assert is_def is True, f"Global window.{name} was not defined!"

        # 2. Canonical auras
        assert len(data["auraStress"]) == 5
        for aura_res in data["auraStress"]:
            aura_name = aura_res["aura"]
            assert not aura_res["threw"], f"Aura {aura_name} threw: {aura_res['errorMsg']}"
            assert aura_res["saveBalanced"], f"Aura {aura_name} save/restore unbalanced!"
            assert aura_res["ellipseCount"] == 2, f"Aura {aura_name} expected 2 ellipses, got {aura_res['ellipseCount']}"
            assert aura_res["colorMatches"], f"Aura {aura_name} color mismatch!"
            assert aura_res["allRadiiPositive"], f"Aura {aura_name} non-positive radius detected!"
            assert aura_res["yOffsetApplied"], f"Aura {aura_name} y-offset (+4) not applied correctly!"

        # 3. Adversarial edge cases
        for ec_res in data["adversarialEdgeCases"]:
            ec_name = ec_res["name"]
            assert not ec_res["threw"], f"Edge case {ec_name} threw: {ec_res['errorMsg']}"
            assert ec_res["saveBalanced"], f"Edge case {ec_name} save/restore state unbalanced!"
            assert ec_res["renderedMatchesExpectation"], f"Edge case {ec_name} render expectation mismatch!"
            assert ec_res["colorMatches"], f"Edge case {ec_name} fallback color mismatch!"

        # 4. High-frequency stress loop (10,000 frames)
        hf = data["highFreqStress"]
        assert not hf["threw"], f"10k stress threw: {hf['errorMsg']}"
        assert hf["totalEllipses"] == 20000, f"Expected 20,000 ellipses, got {hf['totalEllipses']}"
        assert hf["avgMsPerFrame"] < 0.05, f"Render decal too slow: {hf['avgMsPerFrame']:.4f}ms/frame"

        # 5. Safe Haven Audit
        sh = data["safeHavenAudit"]
        assert sh["hideoutMobCount"] == 0, f"Safe Haven hideout spawned {sh['hideoutMobCount']} mobs!"
        assert sh["sanctuaryMobCount"] == 0, f"Safe Haven sanctuary spawned {sh['sanctuaryMobCount']} mobs!"
        assert sh["wildernessMobCount"] > 0, "Wilderness zone spawned 0 mobs!"
        assert sh["wildernessHasLeader"] is True, "Wilderness zone missing rare pack leader with aura!"


class TestEntityRendererDecalCallSite:
    """Verifies entity_renderer.js has valid call site and layering for renderLeaderAuraDecal."""

    def test_callsite_in_entity_renderer(self) -> None:
        renderer_path = ENGINE_DIR / "entity_renderer.js"
        assert renderer_path.exists()
        content = renderer_path.read_text(encoding="utf-8")

        assert "window.renderLeaderAuraDecal" in content, "Missing renderLeaderAuraDecal call site in entity_renderer.js"
        assert "m.isPackLeader" in content, "Missing isPackLeader guard in entity_renderer.js"

        shadow_idx = content.find("ctx.ellipse(mPos.x, mPos.y + 4")
        decal_idx = content.find("window.renderLeaderAuraDecal")
        sprite_idx = content.find("AnimationEngine.drawEntityFrame")

        assert shadow_idx != -1, "Monster shadow ellipse not found in entity_renderer.js"
        assert decal_idx != -1, "Leader aura decal call not found in entity_renderer.js"
        assert sprite_idx != -1, "AnimationEngine.drawEntityFrame not found in entity_renderer.js"
        assert shadow_idx < decal_idx < sprite_idx, (
            f"Invalid visual layering! Expected shadow ({shadow_idx}) < decal ({decal_idx}) < sprite ({sprite_idx})"
        )
