"""
Adversarial Stress & Edge Case Test Suite for Milestone 1:
- Dual Fluid HUD Orbs (Canvas 2D Sinusoidal Crest & Extreme Values)
- Skill Bar Controller (Cooldown Spam, Zero/Negative Durations, Slot Indexing)
- Concurrency, Layout Thrashing & State Machine Edge Cases
"""

import json
import subprocess
import unittest
from pathlib import Path

PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent


def run_node_eval(js_code: str) -> dict:
    """Executes ES module code in Node.js with mock DOM and canvas environment."""
    bootstrap = f"""
const createMockElement = (id) => ({{
  id, dataset: {{}},
  classList: {{
    _c: new Set(),
    add(c) {{ this._c.add(c); }}, remove(c) {{ this._c.delete(c); }},
    toggle(c, f) {{ f = f === undefined ? !this._c.has(c) : f; f ? this._c.add(c) : this._c.delete(c); return f; }},
    contains(c) {{ return this._c.has(c); }}
  }},
  style: {{ _p: {{}}, setProperty(k, v) {{ this._p[k] = v; }}, getProperty(k) {{ return this._p[k]; }} }},
  innerText: '', textContent: '', offsetWidth: 44, offsetHeight: 44,
  addEventListener: () => {{}},
  getContext: () => ({{
    clearRect: () => {{}}, save: () => {{}}, restore: () => {{}}, beginPath: () => {{}},
    arc: () => {{}}, clip: () => {{}}, fillRect: () => {{}}, moveTo: () => {{}}, lineTo: () => {{}},
    closePath: () => {{}}, fill: () => {{}}, stroke: () => {{}}, ellipse: () => {{}},
    createRadialGradient: () => ({{ addColorStop: () => {{}} }}),
    createLinearGradient: () => ({{ addColorStop: () => {{}} }})
  }}),
  querySelector: (sel) => createMockElement('sub_' + sel)
}});
global.document = {{
  getElementById: (id) => createMockElement(id),
  querySelector: (sel) => createMockElement(sel),
  readyState: 'complete', addEventListener: () => {{}}
}};
global.window = {{ addEventListener: () => {{}}, performance: {{ now: () => Date.now() }} }};
global.performance = global.window.performance;
global.requestAnimationFrame = () => 1;
global.cancelAnimationFrame = () => {{}};
try {{
  {js_code}
}} catch (err) {{
  console.error(err); process.exit(1);
}}
"""
    res = subprocess.run(
        ["node", "--input-type=module", "-e", bootstrap],
        capture_output=True, text=True, cwd=str(PROJECT_ROOT)
    )
    if res.returncode != 0:
        raise RuntimeError(f"Node execution failed (code {res.returncode}):\n{res.stderr}")
    return json.loads(res.stdout)


class TestHudOrbsExtremeValues(unittest.TestCase):
    """Stress tests HudOrbs with adversarial, extreme, and malformed numeric values."""

    def test_negative_hp_clamped_to_zero(self):
        """Asserts negative HP is clamped to 0 and dead state does not trigger low-HP pulse."""
        js = """
        const { HudOrbs } = await import('./client/webapp/js/ui/hud_orbs.js');
        HudOrbs.init(); HudOrbs.displayedHp = 50;
        HudOrbs.update(-50, 100, 50, 50, 0.016);
        console.log(JSON.stringify({ displayedHp: HudOrbs.displayedHp, isLowHp: HudOrbs.isLowHp }));
        process.exit(0);
        """
        data = run_node_eval(js)
        self.assertGreaterEqual(data["displayedHp"], 0)
        self.assertLessEqual(data["displayedHp"], 50)
        self.assertFalse(data["isLowHp"])

    def test_float_precision_rounding(self):
        """Asserts float HP/Mana values render properly rounded integer text."""
        js = """
        const { HudOrbs } = await import('./client/webapp/js/ui/hud_orbs.js');
        HudOrbs.init();
        HudOrbs.update(33.33333, 100, 17.8888, 50, 0.016);
        console.log(JSON.stringify({
          lifeCur: HudOrbs.txtLifeCur?.innerText, manaCur: HudOrbs.txtManaCur?.innerText
        }));
        process.exit(0);
        """
        data = run_node_eval(js)
        self.assertTrue(str(data["lifeCur"]).isdigit())
        self.assertTrue(str(data["manaCur"]).isdigit())

    def test_extreme_large_hp_values(self):
        """Asserts HP values > 10,000 scale smoothly within max HP boundaries."""
        js = """
        const { HudOrbs } = await import('./client/webapp/js/ui/hud_orbs.js');
        HudOrbs.init();
        HudOrbs.update(15000, 20000, 8000, 10000, 0.016);
        console.log(JSON.stringify({
          maxHp: HudOrbs.displayedMaxHp, maxMana: HudOrbs.displayedMaxMana
        }));
        process.exit(0);
        """
        data = run_node_eval(js)
        self.assertEqual(data["maxHp"], 20000)
        self.assertEqual(data["maxMana"], 10000)

    def test_rapid_oscillation_stability(self):
        """Asserts 10% <-> 90% HP oscillation does not diverge or produce NaN."""
        js = """
        const { HudOrbs } = await import('./client/webapp/js/ui/hud_orbs.js');
        HudOrbs.init(); HudOrbs.displayedHp = 50;
        let hp = 10;
        for (let i = 0; i < 60; i++) {
          hp = (hp === 10) ? 90 : 10;
          HudOrbs.update(hp, 100, 50, 50, 0.016);
        }
        console.log(JSON.stringify({
          displayedHp: HudOrbs.displayedHp, isFinite: Number.isFinite(HudOrbs.displayedHp)
        }));
        process.exit(0);
        """
        data = run_node_eval(js)
        self.assertTrue(data["isFinite"])
        self.assertGreater(data["displayedHp"], 10)
        self.assertLess(data["displayedHp"], 90)

    def test_nan_input_poisoning_vulnerability(self):
        """Adversarial Test: Validates resilience against NaN input poisoning in HudOrbs."""
        js = """
        const { HudOrbs } = await import('./client/webapp/js/ui/hud_orbs.js');
        HudOrbs.init(); HudOrbs.displayedHp = 100;
        HudOrbs.update(NaN, 100, 50, 50, 0.016);
        const isPoisoned = Number.isNaN(HudOrbs.displayedHp);
        HudOrbs.update(80, 100, 50, 50, 0.016);
        const recoveryFailed = Number.isNaN(HudOrbs.displayedHp);
        console.log(JSON.stringify({ isPoisoned, recoveryFailed }));
        process.exit(0);
        """
        data = run_node_eval(js)
        self.assertFalse(data["isPoisoned"], "HudOrbs must not be poisoned by NaN input")
        self.assertFalse(data["recoveryFailed"], "HudOrbs must cleanly recover from NaN")


class TestSkillBarCooldownAndConcurrency(unittest.TestCase):
    """Stress tests SkillBarController under high-frequency cooldown calls and edge cases."""

    def test_cooldown_spam_1000_calls(self):
        """Asserts 1,000 rapid cooldown triggers execute without timing out or crashing."""
        js = """
        const { SkillBarController } = await import('./client/webapp/js/ui/skill_bar_controller.js');
        const ctrl = new SkillBarController();
        ctrl.init();
        const t0 = Date.now();
        for (let i = 0; i < 1000; i++) ctrl.triggerCooldown('1', 1.5);
        const elapsed = Date.now() - t0;
        console.log(JSON.stringify({ elapsedMs: elapsed, remaining: ctrl.getRemainingCooldown('1') }));
        process.exit(0);
        """
        data = run_node_eval(js)
        self.assertLess(data["elapsedMs"], 500, "1000 calls must take < 500ms")
        self.assertEqual(data["remaining"], 1.5)

    def test_cooldown_during_active_sweep(self):
        """Asserts re-triggering cooldown during an active sweep resets remaining time."""
        js = """
        const { SkillBarController } = await import('./client/webapp/js/ui/skill_bar_controller.js');
        const ctrl = new SkillBarController();
        ctrl.init();
        ctrl.triggerCooldown('1', 5.0);
        ctrl._updateAllCooldowns(2.0);
        const midRemaining = ctrl.getRemainingCooldown('1');
        ctrl.triggerCooldown('1', 4.0);
        console.log(JSON.stringify({ midRemaining, resetRemaining: ctrl.getRemainingCooldown('1') }));
        process.exit(0);
        """
        data = run_node_eval(js)
        self.assertAlmostEqual(data["midRemaining"], 3.0, places=1)
        self.assertEqual(data["resetRemaining"], 4.0)

    def test_cooldown_duration_lte_zero_fallback_anomaly(self):
        """Adversarial Test: durationSec <= 0 clears cooldown immediately."""
        js = """
        const { SkillBarController } = await import('./client/webapp/js/ui/skill_bar_controller.js');
        const ctrl = new SkillBarController();
        ctrl.init();
        ctrl.registerSlot('zero_test', { cooldownTotal: 3.0 });
        const resZero = ctrl.triggerCooldown('zero_test', 0);
        const remZero = ctrl.getRemainingCooldown('zero_test');
        console.log(JSON.stringify({ resZero, remZero }));
        process.exit(0);
        """
        data = run_node_eval(js)
        self.assertTrue(data["resZero"])
        self.assertEqual(data["remZero"], 0, "Passing 0 clears cooldown immediately")

    def test_charged_skill_depletion_and_recovery_cycle(self):
        """Asserts charged skills clamp charges at 0 and restore charges on tick."""
        js = """
        const { SkillBarController } = await import('./client/webapp/js/ui/skill_bar_controller.js');
        const ctrl = new SkillBarController();
        ctrl.init();
        ctrl.registerSlot('charge_slot', { cooldownTotal: 2.0, charges: 3, rechargeDuration: 2.0 });
        for (let i = 0; i < 10; i++) ctrl.activateSlot('charge_slot');
        const chargesAtDepletion = ctrl.slots.get('charge_slot').charges;
        const isOnCd = ctrl.isSlotOnCooldown('charge_slot');
        ctrl._updateAllCooldowns(2.1);
        console.log(JSON.stringify({
          chargesAtDepletion, isOnCd,
          chargesRecovered: ctrl.slots.get('charge_slot').charges,
          canActivateNow: !ctrl.isSlotOnCooldown('charge_slot')
        }));
        process.exit(0);
        """
        data = run_node_eval(js)
        self.assertEqual(data["chargesAtDepletion"], 0)
        self.assertTrue(data["isOnCd"])
        self.assertEqual(data["chargesRecovered"], 1)
        self.assertTrue(data["canActivateNow"])


class TestSkillSlotIndexingAndEdgeCases(unittest.TestCase):
    """Stress tests SkillBarController slot registration, lookups, and unmapped inputs."""

    def test_out_of_bounds_and_invalid_slot_lookups(self):
        """Asserts querying out-of-bounds or non-existent slots returns false/0."""
        js = """
        const { SkillBarController } = await import('./client/webapp/js/ui/skill_bar_controller.js');
        const ctrl = new SkillBarController();
        ctrl.init();
        console.log(JSON.stringify({
          negTrigger: ctrl.triggerCooldown(-1, 2.0),
          oobTrigger: ctrl.triggerCooldown(999, 2.0),
          emptyTrigger: ctrl.triggerCooldown('', 2.0),
          nullTrigger: ctrl.triggerCooldown(null, 2.0),
          oobCooldown: ctrl.isSlotOnCooldown(999),
          oobRemaining: ctrl.getRemainingCooldown(999),
          oobActivate: ctrl.activateSlot(999)
        }));
        process.exit(0);
        """
        data = run_node_eval(js)
        self.assertFalse(data["negTrigger"])
        self.assertFalse(data["oobTrigger"])
        self.assertFalse(data["emptyTrigger"])
        self.assertFalse(data["nullTrigger"])
        self.assertFalse(data["oobCooldown"])
        self.assertEqual(data["oobRemaining"], 0)
        self.assertFalse(data["oobActivate"])

    def test_unregistered_skill_atlas_mapping(self):
        """Asserts setting an unknown skill ID handles missing metadata gracefully."""
        js = """
        const { SkillBarController } = await import('./client/webapp/js/ui/skill_bar_controller.js');
        const ctrl = new SkillBarController();
        ctrl.init();
        ctrl.setSlotSkill('1', 'unknown_martial_art_xyz');
        console.log(JSON.stringify({ skillId: ctrl.slots.get('1').skillId }));
        process.exit(0);
        """
        data = run_node_eval(js)
        self.assertEqual(data["skillId"], "unknown_martial_art_xyz")

    def test_handle_key_press_empty_string_edge_case(self):
        """Adversarial Test: Tests whether handleKeyPress('') triggers unkeyed slots."""
        js = """
        const { SkillBarController } = await import('./client/webapp/js/ui/skill_bar_controller.js');
        const ctrl = new SkillBarController();
        ctrl.init();
        ctrl.registerSlot('unkeyed', { key: '' });
        console.log(JSON.stringify({ triggered: ctrl.handleKeyPress('') }));
        process.exit(0);
        """
        data = run_node_eval(js)
        self.assertFalse(data["triggered"], "Remediated: handleKeyPress('') must not trigger unkeyed slots")


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