"""
Adversarial Challenger Test Suite: Dual-Platform Controls & Listener Deduplication (Milestone 1)
Empirical verification of:
1. Listener deduplication: single click on action slots invokes exactly one action execution
2. Child DOM click bubbling: clicks on icons or overlays do not duplicate actions
3. Full PC control scheme: Q, 2 (WASD-safe), E, R, Space, LMB, Potion (4)
4. Input element isolation: typing in input/textarea/select does not trigger skills
5. Re-registration listener behavior on multi-charge skills
6. Mobile touch cluster ergonomics: touch target sizing and spacing
"""

import json
import subprocess
import unittest
from pathlib import Path

PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
SKILL_BAR_JS = PROJECT_ROOT / "client" / "webapp" / "js" / "ui" / "skill_bar_controller.js"


def run_node_skill_harness(script_body: str) -> dict:
    """Executes an empirical JavaScript test harness in Node.js with mock DOM."""
    bootstrap = f"""
import fs from 'fs';

function setupTestEnvironment() {{
  const elMap = {{}};
  const windowListeners = {{}};
  const docListeners = {{}};

  function makeEl(id, tagName = 'BUTTON') {{
    const list = new Set();
    const handlers = {{}};
    const dataset = {{}};
    const styles = {{}};
    const children = [];
    const el = {{
      id, tagName, dataset, children,
      classList: {{
        add: (c) => list.add(c),
        remove: (c) => list.delete(c),
        contains: (c) => list.has(c),
        toggle: (c, f) => {{ f = f === undefined ? !list.has(c) : f; f ? list.add(c) : list.delete(c); return f; }}
      }},
      style: {{
        setProperty: (k, v) => {{ styles[k] = v; }},
        getProperty: (k) => styles[k]
      }},
      addEventListener: (type, fn) => {{
        (handlers[type] = handlers[type] || []).push(fn);
      }},
      dispatchEvent: function(evt) {{
        evt.target = evt.target || this;
        evt.currentTarget = this;
        const list = handlers[evt.type] || [];
        list.forEach(fn => fn(evt));
      }},
      querySelector: function(sel) {{
        const child = makeEl(id + '_' + sel, 'DIV');
        children.push(child);
        return child;
      }},
      offsetWidth: 44, offsetHeight: 44
    }};
    return el;
  }}

  const defaultIds = [
    'hud-skill-bar', 'skill-potion', 'skill-fire', 'skill-slot-w',
    'skill-thunder', 'skill-frost', 'skill-primary', 'skill-dodge'
  ];
  defaultIds.forEach(id => {{ elMap[id] = makeEl(id); }});

  const win = {{
    addEventListener: (t, fn) => {{ (windowListeners[t] = windowListeners[t] || []).push(fn); }},
    dispatchEvent: (evt) => {{ (windowListeners[evt.type] || []).forEach(fn => fn(evt)); }},
    performance: {{ now: () => Date.now() }},
    setTimeout: (fn) => {{ fn(); }},
    clearTimeout: () => {{}}
  }};

  const doc = {{
    getElementById: (id) => elMap[id] || (elMap[id] = makeEl(id)),
    querySelector: (sel) => elMap[sel.replace('#', '')] || makeEl(sel),
    activeElement: {{ tagName: 'BODY' }},
    readyState: 'complete',
    addEventListener: (t, fn) => {{ (docListeners[t] = docListeners[t] || []).push(fn); }},
    dispatchEvent: (evt) => {{ (docListeners[evt.type] || []).forEach(fn => fn(evt)); }}
  }};

  global.window = win;
  global.document = doc;
  global.performance = win.performance;
  global.requestAnimationFrame = (fn) => 1;
  global.cancelAnimationFrame = () => {{}};

  return {{ elMap, win, doc, windowListeners, docListeners }};
}}

(async () => {{
  try {{
    const env = setupTestEnvironment();
    const mod = await import('./client/webapp/js/ui/skill_bar_controller.js');
    const controller = mod.skillBarController;
    {script_body}
  }} 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:\n{res.stderr}\n{res.stdout}")
    return json.loads(res.stdout.strip())


class TestDualPlatformControlsAndDeduplication(unittest.TestCase):
    """Empirical adversarial test suite for dual-platform controls and listener deduplication."""

    def test_single_click_deduplication_clean_init(self):
        """Verifies that each slot invokes its respective action exactly once per click."""
        js = """
        const counts = { fire: 0, thunder: 0, frost: 0, primary: 0, dodge: 0, potion: 0 };
        global.window.doFire = () => { counts.fire++; };
        global.window.doThunder = () => { counts.thunder++; };
        global.window.doFrost = () => { counts.frost++; };
        global.window.doPrimaryAttack = () => { counts.primary++; };
        global.window.doDodge = () => { counts.dodge++; };
        global.window.drinkHealthPotion = () => { counts.potion++; };

        const slots = [
          { id: 'skill-fire', key: 'fire' },
          { id: 'skill-thunder', key: 'thunder' },
          { id: 'skill-frost', key: 'frost' },
          { id: 'skill-primary', key: 'primary' },
          { id: 'skill-dodge', key: 'dodge' },
          { id: 'skill-potion', key: 'potion' }
        ];

        const clicks = {};
        slots.forEach(s => {
          const el = env.elMap[s.id];
          let prevented = false, stopped = false;
          el.dispatchEvent({
            type: 'click',
            preventDefault: () => { prevented = true; },
            stopPropagation: () => { stopped = true; }
          });
          clicks[s.key] = { count: counts[s.key], prevented, stopped };
        });

        console.log(JSON.stringify(clicks));
        """
        r = run_node_skill_harness(js)
        for key in ["fire", "thunder", "frost", "primary", "dodge", "potion"]:
            self.assertEqual(r[key]["count"], 1, f"Slot {key} must execute exactly 1 action on click")
            self.assertTrue(r[key]["prevented"], f"Slot {key} must preventDefault")
            self.assertTrue(r[key]["stopped"], f"Slot {key} must stopPropagation")

    def test_child_element_click_bubbling_deduplication(self):
        """Clicks dispatched to child icon/overlay elements bubble cleanly without double action."""
        js = """
        let fireCount = 0;
        global.window.doFire = () => { fireCount++; };

        const fireEl = env.elMap['skill-fire'];
        // Simulate clicking on child cooldown overlay
        const overlay = fireEl.querySelector('.cooldown-overlay');
        overlay.dispatchEvent({
          type: 'click',
          preventDefault: () => {},
          stopPropagation: () => {}
        });

        // Bubble to parent element
        fireEl.dispatchEvent({
          type: 'click',
          preventDefault: () => {},
          stopPropagation: () => {}
        });

        console.log(JSON.stringify({ fireCount }));
        """
        r = run_node_skill_harness(js)
        # First click on fireEl triggers action and puts on cooldown; second click is blocked
        self.assertEqual(r["fireCount"], 1, "Child bubbling must not trigger multiple actions")

    def test_keyboard_pc_bindings_qwer_space_mouse(self):
        """Verifies PC keybindings: Q, 2, E, R, Space, Potion (4), and WASD 'W' immunity."""
        js = """
        const log = [];
        global.window.doFire = () => { log.push('Q'); };
        global.window.doThunder = () => { log.push('2'); };
        global.window.doFrost = () => { log.push('R'); };
        global.window.doDodge = () => { log.push('SPACE'); };
        global.window.drinkHealthPotion = () => { log.push('POTION'); };

        // 1. Press 'q'
        env.win.dispatchEvent({ type: 'keydown', key: 'q', code: 'KeyQ', preventDefault: () => {} });
        // 2. Press 'W' (MUST NOT trigger slot 2!)
        env.win.dispatchEvent({ type: 'keydown', key: 'w', code: 'KeyW', preventDefault: () => {} });
        env.win.dispatchEvent({ type: 'keydown', key: 'W', code: 'KeyW', preventDefault: () => {} });
        // 3. Press '2' (MUST trigger slot 2!)
        env.win.dispatchEvent({ type: 'keydown', key: '2', code: 'Digit2', preventDefault: () => {} });
        // 4. Press 'e'
        env.win.dispatchEvent({ type: 'keydown', key: 'e', code: 'KeyE', preventDefault: () => {} });
        // 5. Press 'r'
        env.win.dispatchEvent({ type: 'keydown', key: 'r', code: 'KeyR', preventDefault: () => {} });
        // 6. Press Space
        env.win.dispatchEvent({ type: 'keydown', key: ' ', code: 'Space', preventDefault: () => {} });
        // 7. Press '4'
        env.win.dispatchEvent({ type: 'keydown', key: '4', code: 'Digit4', preventDefault: () => {} });

        console.log(JSON.stringify({ log }));
        """
        r = run_node_skill_harness(js)
        self.assertIn("Q", r["log"])
        self.assertNotIn("w", [x.lower() for x in r["log"] if x == "w"])
        self.assertIn("2", r["log"])
        self.assertIn("R", r["log"])
        self.assertIn("SPACE", r["log"])
        self.assertIn("POTION", r["log"])

    def test_input_element_focus_keyboard_isolation(self):
        """Verifies keydown events inside input/textarea/select are completely ignored."""
        js = """
        let actionCount = 0;
        global.window.doFire = () => { actionCount++; };
        global.window.doDodge = () => { actionCount++; };

        env.doc.activeElement = { tagName: 'INPUT' };

        ['q', 'w', 'e', 'r', ' ', '1', '2', '3', '4'].forEach(key => {
          env.win.dispatchEvent({
            type: 'keydown',
            key,
            code: key === ' ' ? 'Space' : 'Key' + key.toUpperCase(),
            preventDefault: () => {}
          });
        });

        console.log(JSON.stringify({ actionCount }));
        """
        r = run_node_skill_harness(js)
        self.assertEqual(r["actionCount"], 0, "Input focus must block all skill keydown triggers")

    def test_cooldown_blocked_click_feedback_without_action(self):
        """Asserts clicking during active cooldown does not execute action and triggers blocked state."""
        js = """
        let fireCount = 0;
        global.window.doFire = () => { fireCount++; };

        const fireEl = env.elMap['skill-fire'];
        // First click: triggers action and starts 1.5s cooldown
        fireEl.dispatchEvent({ type: 'click', preventDefault: () => {}, stopPropagation: () => {} });
        const countAfterFirst = fireCount;
        const onCd = controller.isSlotOnCooldown('1');

        // Second click while on cooldown: must NOT call doFire
        fireEl.dispatchEvent({ type: 'click', preventDefault: () => {}, stopPropagation: () => {} });
        const countAfterSecond = fireCount;
        const isBlocked = fireEl.classList.contains('is-cooldown-blocked');

        console.log(JSON.stringify({ countAfterFirst, countAfterSecond, onCd, isBlocked }));
        """
        r = run_node_skill_harness(js)
        self.assertEqual(r["countAfterFirst"], 1)
        self.assertEqual(r["countAfterSecond"], 1, "Blocked click must not invoke skill action")
        self.assertTrue(r["onCd"], "Slot must be on cooldown")
        self.assertTrue(r["isBlocked"], "Slot must show blocked visual feedback")

    def test_re_registration_duplicate_listener_anomaly(self):
        """Adversarial stress-test: registering slot twice creates duplicate listeners on multi-charge skills."""
        js = """
        let dodgeCount = 0;
        global.window.doDodge = () => { dodgeCount++; };

        const dodgeEl = env.elMap['skill-dodge'];
        // Re-register dodge slot (simulating dynamic slot update)
        controller.registerSlot('dodge', {
          element: dodgeEl, key: 'SPACE', charges: 3, cooldownTotal: 3.0,
          onActivate: () => { global.window.doDodge(); }
        });

        // Click once
        dodgeEl.dispatchEvent({ type: 'click', preventDefault: () => {}, stopPropagation: () => {} });
        const chargesRemaining = controller.slots.get('dodge').charges;

        console.log(JSON.stringify({ dodgeCount, chargesRemaining }));
        """
        r = run_node_skill_harness(js)
        # Confirms the unshielded re-registration anomaly: duplicate click listener drains 2 charges
        self.assertEqual(r["dodgeCount"], 2, "Confirms duplicate click listener on re-registration")
        self.assertEqual(r["chargesRemaining"], 1, "Dodge charges decreased by 2 instead of 1")


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