"""
Empirical Challenger Gen 3 Deep Stress & Verification Harness for Milestone 1.
Tests every fix independently with hostile, boundary, and fuzz inputs.
"""

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) => {{
  const children = new Map();
  return {{
    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, _listeners: {{}},
    addEventListener(e, fn) {{ (this._listeners[e] = this._listeners[e] || []).push(fn); }},
    dispatchEvent(e) {{ (this._listeners[e.type] || []).forEach(fn => fn(e)); }},
    getContext: () => ({{
      clearRect: () => {{}}, save: () => {{}}, restore: () => {{}}, beginPath: () => {{}},
      arc: () => {{}}, clip: () => {{}}, fillRect: () => {{}}, moveTo: () => {{}}, lineTo: () => {{}},
      closePath: () => {{}}, fill: () => {{}}, stroke: () => {{}}, ellipse: () => {{}},
      createRadialGradient: () => ({{ addColorStop: () => {{}} }}),
      createLinearGradient: () => ({{ addColorStop: () => {{}} }})
    }}),
    querySelector(sel) {{
      if (!children.has(sel)) children.set(sel, createMockElement('sub_' + sel));
      return children.get(sel);
    }}
  }};
}};
const mockDoc = {{
  'life-orb-canvas': createMockElement('life-orb-canvas'), 'mana-orb-canvas': createMockElement('mana-orb-canvas'),
  'txt-life-cur': createMockElement('txt-life-cur'), 'txt-life-max': createMockElement('txt-life-max'),
  'txt-mana-cur': createMockElement('txt-mana-cur'), 'txt-mana-max': createMockElement('txt-mana-max'),
  'hud-life-orb': createMockElement('hud-life-orb'), 'hud-mana-orb': createMockElement('hud-mana-orb'),
  'life-orb-warning': createMockElement('life-orb-warning'), 'hud-low-hp-vignette': createMockElement('hud-low-hp-vignette'),
  'hud-skill-bar': createMockElement('hud-skill-bar'), 'skill-potion': createMockElement('skill-potion'),
  'skill-fire': createMockElement('skill-fire'), 'skill-slot-w': createMockElement('skill-slot-w'),
  'skill-thunder': createMockElement('skill-thunder'), 'skill-frost': createMockElement('skill-frost'),
  'skill-primary': createMockElement('skill-primary'), 'skill-dodge': createMockElement('skill-dodge')
}};
global.windowListeners = {{}};
global.document = {{
  getElementById: (id) => mockDoc[id] || createMockElement(id),
  querySelector: (s) => createMockElement(s), readyState: 'complete',
  addEventListener: () => {{}}, activeElement: null
}};
global.window = {{
  addEventListener: (e, fn) => {{ (global.windowListeners[e] = global.windowListeners[e] || []).push(fn); }},
  removeEventListener: (e, fn) => {{
    if (global.windowListeners[e]) global.windowListeners[e] = global.windowListeners[e].filter(f => f !== fn);
  }},
  performance: {{ now: () => Date.now() }}, triggerHaptic: () => {{}},
  drinkHealthPotion: () => {{}}, doFire: () => {{}}, doThunder: () => {{}},
  doFrost: () => {{}}, doPrimaryAttack: () => {{}}, doDodge: () => {{}}, dodgeCharges: 3
}};
global.performance = global.window.performance;
global.requestAnimationFrame = (fn) => 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 TestHudOrbsHostileInputs(unittest.TestCase):
    """Hostile fuzzing of HudOrbs.update with non-finite, negative, and extreme values."""

    def test_nan_and_infinity_matrix(self):
        """Tests permutations of NaN, Infinity, -Infinity in all 5 parameters."""
        js = """
        const { HudOrbs } = await import('./client/webapp/js/ui/hud_orbs.js');
        HudOrbs.init();
        const testCases = [
          [NaN, 100, 50, 50, 0.016], [100, NaN, 50, 50, 0.016],
          [100, 100, NaN, 50, 0.016], [100, 100, 50, NaN, 0.016],
          [100, 100, 50, 50, NaN], [Infinity, 100, 50, 50, 0.016],
          [-Infinity, 100, 50, 50, 0.016], [100, Infinity, 50, 50, 0.016],
          [100, 100, Infinity, 50, 0.016], [100, 100, 50, Infinity, 0.016],
          [undefined, null, "string", Object.create(null), []]
        ];
        let poisonedAny = false;
        for (const tc of testCases) {
          HudOrbs.update(...tc);
          if (!Number.isFinite(HudOrbs.displayedHp) || !Number.isFinite(HudOrbs.displayedMana)) {
            poisonedAny = true; break;
          }
        }
        HudOrbs.update(75, 100, 40, 50, 0.016);
        console.log(JSON.stringify({
          poisonedAny, isHpFinite: Number.isFinite(HudOrbs.displayedHp),
          isManaFinite: Number.isFinite(HudOrbs.displayedMana), hp: HudOrbs.displayedHp
        }));
        process.exit(0);
        """
        data = run_node_eval(js)
        self.assertFalse(data["poisonedAny"])
        self.assertTrue(data["isHpFinite"])
        self.assertTrue(data["isManaFinite"])
        self.assertGreater(data["hp"], 0)


class TestSkillBarCooldownDurationZeroAndNegative(unittest.TestCase):
    """Empirical verification that duration <= 0 clears cooldown and resets charges."""

    def test_duration_zero_and_negative_clears_cooldown(self):
        js = """
        const { SkillBarController } = await import('./client/webapp/js/ui/skill_bar_controller.js');
        const ctrl = new SkillBarController();
        ctrl.init();
        ctrl.registerSlot('test_normal', { cooldownTotal: 4.0 });
        ctrl.registerSlot('test_charge', { cooldownTotal: 3.0, charges: 3, rechargeDuration: 3.0 });
        ctrl.triggerCooldown('test_normal', 4.0);
        ctrl.triggerCooldown('test_charge', 3.0);
        const resZero = ctrl.triggerCooldown('test_normal', 0);
        const normalAfterZero = ctrl.getRemainingCooldown('test_normal');
        const normalIsOnCd = ctrl.isSlotOnCooldown('test_normal');
        const resNeg = ctrl.triggerCooldown('test_charge', -5.0);
        const chargeAfterNeg = ctrl.slots.get('test_charge').charges;
        const chargeIsOnCd = ctrl.isSlotOnCooldown('test_charge');
        ctrl.registerSlot('test_inf', { cooldownTotal: 2.0 });
        const resInf = ctrl.triggerCooldown('test_inf', Infinity);
        console.log(JSON.stringify({
          resZero, normalAfterZero, normalIsOnCd, resNeg, chargeAfterNeg, chargeIsOnCd, resInf
        }));
        process.exit(0);
        """
        data = run_node_eval(js)
        self.assertTrue(data["resZero"])
        self.assertEqual(data["normalAfterZero"], 0)
        self.assertFalse(data["normalIsOnCd"])
        self.assertTrue(data["resNeg"])
        self.assertEqual(data["chargeAfterNeg"], 3)
        self.assertFalse(data["chargeIsOnCd"])


class TestDodgeChargesVisualAndStateRecovery(unittest.TestCase):
    """Empirical verification that Dodge recovers from 0 to 1 with visual reset and badge update."""

    def test_dodge_charge_zero_to_one_recovery(self):
        js = """
        const { SkillBarController } = await import('./client/webapp/js/ui/skill_bar_controller.js');
        const ctrl = new SkillBarController();
        ctrl.init();
        const slot = ctrl.slots.get('dodge');
        let readyCallbackFired = false;
        ctrl.onReady('dodge', () => { readyCallbackFired = true; });
        ctrl.activateSlot('dodge'); ctrl.activateSlot('dodge'); ctrl.activateSlot('dodge');
        const chargesDepleted = slot.charges;
        const hasClassOnCd = slot.element.classList.contains('is-on-cooldown');
        ctrl._updateAllCooldowns(3.1);
        console.log(JSON.stringify({
          chargesDepleted, hasClassOnCd,
          chargesRecovered: slot.charges,
          isOnCdRecovered: ctrl.isSlotOnCooldown('dodge'),
          hasClassOnCdAfter: slot.element.classList.contains('is-on-cooldown'),
          windowChargesSynced: global.window.dodgeCharges,
          badgeTextRecovered: slot.element.querySelector('.skill-charges-badge').textContent,
          readyCallbackFired
        }));
        process.exit(0);
        """
        data = run_node_eval(js)
        self.assertEqual(data["chargesDepleted"], 0)
        self.assertTrue(data["hasClassOnCd"])
        self.assertEqual(data["chargesRecovered"], 1)
        self.assertFalse(data["isOnCdRecovered"])
        self.assertFalse(data["hasClassOnCdAfter"])
        self.assertEqual(data["windowChargesSynced"], 1)
        self.assertEqual(data["badgeTextRecovered"], "1")
        self.assertTrue(data["readyCallbackFired"])


class TestKeybindingWDisambiguation(unittest.TestCase):
    """Empirical verification that Key 'W' does NOT activate skill slot 2."""

    def test_w_key_does_not_activate_skill_2(self):
        js = """
        const { SkillBarController } = await import('./client/webapp/js/ui/skill_bar_controller.js');
        const ctrl = new SkillBarController();
        ctrl.init();
        let slot2Activated = false;
        const slot2 = ctrl.slots.get('2');
        slot2.onActivate = () => { slot2Activated = true; };
        const resLowerW = ctrl.handleKeyPress('w');
        const resUpperW = ctrl.handleKeyPress('W');
        const keydownHandlers = global.windowListeners['keydown'] || [];
        for (const handler of keydownHandlers) {
          handler({ key: 'w', preventDefault: () => {} });
          handler({ key: 'W', preventDefault: () => {} });
        }
        const wasActivatedByW = slot2Activated;
        const resKey2 = ctrl.handleKeyPress('2');
        for (const handler of keydownHandlers) handler({ key: '2', preventDefault: () => {} });
        console.log(JSON.stringify({
          resLowerW, resUpperW, wasActivatedByW, resKey2,
          wasActivatedByKey2: slot2Activated, slot2Key: slot2.key
        }));
        process.exit(0);
        """
        data = run_node_eval(js)
        self.assertFalse(data["resLowerW"])
        self.assertFalse(data["resUpperW"])
        self.assertFalse(data["wasActivatedByW"])
        self.assertTrue(data["resKey2"])
        self.assertTrue(data["wasActivatedByKey2"])
        self.assertEqual(data["slot2Key"], "2")


class TestRapidSpamAndConcurrencyStress(unittest.TestCase):
    """1000 spam calls and concurrency simulation without desync or memory explosion."""

    def test_1000_spam_activations_and_triggers(self):
        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.activateSlot('1'); ctrl.triggerCooldown('1', 1.5);
        }
        const elapsedMs = Date.now() - t0;
        console.log(JSON.stringify({
          elapsedMs, finalRemaining: ctrl.getRemainingCooldown('1'),
          uniqueSlotsSize: ctrl.uniqueSlots.size
        }));
        process.exit(0);
        """
        data = run_node_eval(js)
        self.assertLess(data["elapsedMs"], 1000)
        self.assertEqual(data["finalRemaining"], 1.5)
        self.assertEqual(data["uniqueSlotsSize"], 7)


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