"""
Adversarial Challenger Test Suite: PointerEvents & Ergonomics Verifier (Milestone 1)
Empirical verification of:
1. Multi-touch pointer isolation (left thumb joystick vs simultaneous 3-skill right thumb taps)
2. Dead-zone model: strict clamping (< 0.15) and smooth scaling (0.151 -> 1.0 -> over-drag)
3. Lost pointer capture resilience (pointercancel, off-canvas tracking, window blur, resize)
4. Mixed Pointer/Touch event defense & second-finger contention
"""

import json
import subprocess
import unittest
from pathlib import Path

PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
JOYSTICK_JS = PROJECT_ROOT / "client" / "webapp" / "js" / "engine" / "joystick.js"


def run_node_harness(script_body: str) -> dict:
    """Executes an empirical JavaScript test harness in Node.js."""
    runner = f"""
const fs = require('fs');
function setupEnvironment() {{
  const wL = {{}}, el = {{}};
  function mkEl(id) {{
    const l = {{}};
    return {{
      id, style: {{}}, classList: {{ add() {{}}, remove() {{}}, contains() {{ return false; }} }},
      getBoundingClientRect: () => ({{ left: 0, top: 0, width: 320, height: 480 }}),
      addEventListener(e, h) {{ (l[e] = l[e] || []).push(h); }},
      dispatchEvent(e) {{ (l[e.type] || []).forEach(h => h(e)); }},
      setPointerCapture(id) {{ this.cap = id; }},
      releasePointerCapture(id) {{ if (this.cap === id) this.cap = null; }}
    }};
  }}
  const win = {{
    addEventListener(e, h) {{ (wL[e] = wL[e] || []).push(h); }},
    dispatchEvent(e) {{ (wL[e.type] || []).forEach(h => h(e)); }},
    setTimeout: (fn) => fn(), clearTimeout: () => {{}}
  }};
  const doc = {{ getElementById: (id) => (el[id] = el[id] || mkEl(id)), activeElement: null }};
  const player = {{ dirX: 0, dirY: 0 }};
  ['joystick-zone', 'joystick-base', 'joystick-stick', 'app-viewport', 'skill-fire', 'skill-thunder', 'skill-frost'].forEach(id => doc.getElementById(id));
  global.window = win; global.document = doc; global.player = player;
  eval(fs.readFileSync({json.dumps(str(JOYSTICK_JS))}, 'utf8'));
  return {{ win, doc, player, el, wL }};
}}
{script_body}
"""
    res = subprocess.run(["node", "-e", runner], capture_output=True, text=True, cwd=str(PROJECT_ROOT))
    if res.returncode != 0:
        raise RuntimeError(f"Harness failed:\n{res.stderr}\n{res.stdout}")
    return json.loads(res.stdout.strip())


class TestAdversarialJoystickPointerEvents(unittest.TestCase):
    """Adversarial stress-testing suite for PointerEvents and ergonomics."""

    def test_multitouch_pointer_isolation_during_skill_spam(self):
        """Simultaneous left thumb movement + right thumb 3-skill spam."""
        js = """
        const env = setupEnvironment(), jz = env.el['joystick-zone'];
        const [sF, sT, sFr] = [env.el['skill-fire'], env.el['skill-thunder'], env.el['skill-frost']];
        const res = {};
        jz.dispatchEvent({ type: 'pointerdown', pointerId: 101, clientX: 100, clientY: 200, pointerType: 'touch' });
        res.down = { active: env.win.joystickActive, cap: jz.cap };
        jz.dispatchEvent({ type: 'pointermove', pointerId: 101, clientX: 130, clientY: 200 });
        res.move1 = { dirX: global.player.dirX, dirY: global.player.dirY };
        [sF, sT, sFr].forEach((s, idx) => {
          s.dispatchEvent({ type: 'pointerdown', pointerId: 102 + idx, clientX: 280, clientY: 400 });
          s.dispatchEvent({ type: 'pointerup', pointerId: 102 + idx, clientX: 280, clientY: 400 });
        });
        res.afterSkills = { active: env.win.joystickActive, dirX: global.player.dirX };
        jz.dispatchEvent({ type: 'pointermove', pointerId: 101, clientX: 130, clientY: 230 });
        res.move2 = { dirX: global.player.dirX, dirY: global.player.dirY };
        jz.dispatchEvent({ type: 'pointerup', pointerId: 101, clientX: 130, clientY: 230 });
        res.up = { active: env.win.joystickActive, dirX: global.player.dirX, cap: jz.cap };
        console.log(JSON.stringify(res));
        """
        r = run_node_harness(js)
        self.assertTrue(r["down"]["active"])
        self.assertEqual(r["down"]["cap"], 101)
        self.assertGreater(r["move1"]["dirX"], 0.0)
        self.assertTrue(r["afterSkills"]["active"])
        self.assertAlmostEqual(r["afterSkills"]["dirX"], r["move1"]["dirX"], places=4)
        self.assertGreater(r["move2"]["dirX"], 0.0)
        self.assertGreater(r["move2"]["dirY"], 0.0)
        self.assertFalse(r["up"]["active"])
        self.assertEqual(r["up"]["dirX"], 0.0)
        self.assertIsNone(r["up"]["cap"])

    def test_dead_zone_clamping_and_smooth_scaling(self):
        """Dead-zone clamp (< 0.15), boundary onset (0.151), and smooth scaling."""
        js = """
        const env = setupEnvironment(), jz = env.el['joystick-zone'];
        jz.dispatchEvent({ type: 'pointerdown', pointerId: 1, clientX: 100, clientY: 200, pointerType: 'touch' });
        const testMags = [0.05, 0.10, 0.149, 0.151, 0.25, 0.50, 0.75, 1.00, 1.50];
        const res = {};
        testMags.forEach(m => {
          jz.dispatchEvent({ type: 'pointermove', pointerId: 1, clientX: 100 + m * 46, clientY: 200 });
          res[m] = { dirX: global.player.dirX, dirY: global.player.dirY, stick: env.el['joystick-stick'].style.transform };
        });
        console.log(JSON.stringify(res));
        """
        r = run_node_harness(js)
        for m in ["0.05", "0.1", "0.149"]:
            self.assertEqual(r[m]["dirX"], 0.0, f"Mag {m} must strictly clamp to 0")
            self.assertEqual(r[m]["dirY"], 0.0, f"Mag {m} must strictly clamp to 0")
        self.assertGreater(r["0.151"]["dirX"], 0.0)
        self.assertLess(r["0.151"]["dirX"], 0.005)
        self.assertLess(r["0.151"]["dirX"], r["0.25"]["dirX"])
        self.assertLess(r["0.25"]["dirX"], r["0.5"]["dirX"])
        self.assertLess(r["0.5"]["dirX"], r["0.75"]["dirX"])
        self.assertLess(r["0.75"]["dirX"], r["1"]["dirX"])
        self.assertAlmostEqual(r["1"]["dirX"], 1.0, places=4)
        self.assertAlmostEqual(r["1.5"]["dirX"], 1.0, places=4)
        self.assertIn("46px", r["1.5"]["stick"])

    def test_lost_pointer_capture_and_edge_resilience(self):
        """pointercancel on active vs foreign, off-canvas, blur, and resize."""
        js = """
        const env = setupEnvironment(), jz = env.el['joystick-zone'];
        jz.dispatchEvent({ type: 'pointerdown', pointerId: 55, clientX: 100, clientY: 200, pointerType: 'touch' });
        jz.dispatchEvent({ type: 'pointermove', pointerId: 55, clientX: 130, clientY: 200 });
        jz.dispatchEvent({ type: 'pointercancel', pointerId: 999 });
        const foreignActive = env.win.joystickActive;
        jz.dispatchEvent({ type: 'pointercancel', pointerId: 55 });
        const activeCancel = { active: env.win.joystickActive, dirX: global.player.dirX, cap: jz.cap };
        jz.dispatchEvent({ type: 'pointerdown', pointerId: 77, clientX: 100, clientY: 200, pointerType: 'touch' });
        jz.dispatchEvent({ type: 'pointermove', pointerId: 77, clientX: -800, clientY: 3000 });
        const offCanvas = { dirX: global.player.dirX, dirY: global.player.dirY };
        env.win.dispatchEvent({ type: 'blur' });
        const blur = { active: env.win.joystickActive, dirX: global.player.dirX };
        env.win.dispatchEvent({ type: 'resize' });
        const restLeft = env.el['joystick-base'].style.left;
        console.log(JSON.stringify({ foreignActive, activeCancel, offCanvas, blur, restLeft }));
        """
        r = run_node_harness(js)
        self.assertTrue(r["foreignActive"])
        self.assertFalse(r["activeCancel"]["active"])
        self.assertEqual(r["activeCancel"]["dirX"], 0.0)
        self.assertIsNone(r["activeCancel"]["cap"])
        self.assertNotEqual(r["offCanvas"]["dirX"], 0.0)
        self.assertFalse(r["blur"]["active"])
        self.assertEqual(r["blur"]["dirX"], 0.0)
        self.assertEqual(r["restLeft"], "90px")

    def test_multitouch_second_finger_on_joystick_zone_pure_pointer(self):
        """Second finger on joystickZone must not steal pointer or cancel movement."""
        js = """
        const env = setupEnvironment(), jz = env.el['joystick-zone'];
        jz.dispatchEvent({ type: 'pointerdown', pointerId: 1, clientX: 100, clientY: 200, pointerType: 'touch' });
        jz.dispatchEvent({ type: 'pointermove', pointerId: 1, clientX: 130, clientY: 200 });
        const dir1 = global.player.dirX;
        jz.dispatchEvent({ type: 'pointerdown', pointerId: 2, clientX: 110, clientY: 210, pointerType: 'touch' });
        const capDuring = jz.cap;
        jz.dispatchEvent({ type: 'pointermove', pointerId: 2, clientX: 150, clientY: 250 });
        const dirDuring = global.player.dirX;
        jz.dispatchEvent({ type: 'pointerup', pointerId: 2, clientX: 150, clientY: 250 });
        const activeAfter = env.win.joystickActive;
        jz.dispatchEvent({ type: 'pointermove', pointerId: 1, clientX: 140, clientY: 200 });
        const dirFinal = global.player.dirX;
        console.log(JSON.stringify({ capDuring, dir1, dirDuring, activeAfter, dirFinal }));
        """
        r = run_node_harness(js)
        self.assertEqual(r["capDuring"], 1)
        self.assertEqual(r["dirDuring"], r["dir1"])
        self.assertTrue(r["activeAfter"])
        self.assertGreater(r["dirFinal"], r["dir1"])

    def test_multitouch_mixed_legacy_touchend_vulnerability(self):
        """Empirically confirms that unshielded touchend at line 145 calls resetJoystick()."""
        js = """
        const env = setupEnvironment(), jz = env.el['joystick-zone'];
        jz.dispatchEvent({ type: 'pointerdown', pointerId: 1, clientX: 100, clientY: 200, pointerType: 'touch' });
        jz.dispatchEvent({ type: 'pointermove', pointerId: 1, clientX: 130, clientY: 200 });
        const activeBefore = env.win.joystickActive;
        jz.dispatchEvent({ type: 'touchend', preventDefault: () => {} });
        const activeAfter = env.win.joystickActive;
        console.log(JSON.stringify({ activeBefore, activeAfter }));
        """
        r = run_node_harness(js)
        self.assertTrue(r["activeBefore"])
        # Finding: Line 145 lacks activePointerId check, resetting joystick on legacy touchend
        self.assertFalse(r["activeAfter"], "Confirms unshielded touchend resets active joystick")


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