"""
Unit & Integration Test Suite: Player Death & Revival State Machine Verification.
Verifies:
1. When killed by a monster, player is flagged as dead (isDead = true) and takes no further damage.
2. Upon revival, player is cleanly teleported back to Safe Haven Hub (zone_boundless_sanctuary)
   with valid coordinates (0.0, 0.0), clearing wilderness procedural grids.
3. Player action locks, animState ('hurt' -> 'idle'), and inputs (joystick, keys, dirX/dirY)
   are fully restored so player can immediately walk, attack, and cast skills.
4. Player movement is not blocked by collision after reviving.
"""

import json
import subprocess
import unittest
from pathlib import Path

PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
WEBAPP_DIR = PROJECT_ROOT / "client" / "webapp"
COLLISION_ENGINE_JS = WEBAPP_DIR / "js" / "engine" / "collision_engine.js"
MONSTER_SYSTEM_JS = WEBAPP_DIR / "js" / "engine" / "monster_system.js"
COMBAT_SKILLS_JS = WEBAPP_DIR / "js" / "engine" / "combat_skills.js"


def run_revival_node_harness(script_body: str) -> dict:
    """Runs a simulated Node.js harness evaluating client scripts."""
    runner = f"""
import fs from 'fs';
import path from 'path';

// Setup Mock DOM & Global Environment
const elements = {{}};
function mkElement(id) {{
  const listeners = {{}};
  return {{
    id,
    style: {{}},
    dataset: {{}},
    classList: {{
      _classes: new Set(['hidden']),
      add(c) {{ this._classes.add(c); }},
      remove(c) {{ this._classes.delete(c); }},
      contains(c) {{ return this._classes.has(c); }},
      toggle(c, force) {{
        if (force === undefined) force = !this._classes.has(c);
        if (force) this.add(c); else this.remove(c);
        return force;
      }}
    }},
    innerText: '',
    innerHTML: '',
    value: '',
    appendChild(child) {{}},
    addEventListener(evt, fn) {{ (listeners[evt] = listeners[evt] || []).push(fn); }},
    dispatchEvent(evt) {{ (listeners[evt.type] || []).forEach(fn => fn(evt)); }},
    closest(sel) {{ return null; }}
  }};
}}

const doc = {{
  getElementById: (id) => (elements[id] = elements[id] || mkElement(id)),
  createElement: (tag) => mkElement(tag),
  querySelectorAll: () => [],
  addEventListener: () => {{}},
  removeEventListener: () => {{}}
}};

const timeouts = [];
global.setTimeout = (fn, ms) => {{
  timeouts.push(fn);
  return timeouts.length;
}};
global.runAllTimeouts = () => {{
  while (timeouts.length > 0) {{
    const fn = timeouts.shift();
    fn();
  }}
}};
global.clearTimeout = () => {{}};

const win = {{
  document: doc,
  addEventListener: () => {{}},
  removeEventListener: () => {{}},
  setTimeout: global.setTimeout,
  clearTimeout: global.clearTimeout,
  keys: {{ 'w': true, 'a': true }},
  resetJoystick: () => {{ win.joystickResetCalled = true; }}
}};

global.window = win;
global.document = doc;
global.keys = win.keys;

// Mock audio / haptics / rendering functions
global.sfxEngine = new Proxy({{}}, {{ get: (target, prop) => () => {{}} }});
global.playAudioFx = () => {{}};
global.triggerHaptic = () => {{}};
global.triggerScreenShake = () => {{}};
global.spawnDamageText = () => {{}};
global.spawnGoreAndBoneBurst = () => {{}};
global.spawnParticles = () => {{}};
global.spawnInfernalSlash = () => {{}};
global.spawnLightningStrike = () => {{}};
global.spawnGlacialSpikes = () => {{}};
global.particles = [];
global.groundDrops = [];
global.worldToIso = (wx, wy) => ({{ x: wx * 32, y: wy * 16 }});
global.updatePlayerHpUI = () => {{}};

// Mock player entity
global.player = {{
  level: 1, maxHp: 100, hp: 100, isDead: false,
  wx: 10.0, wy: 10.0, dirX: 1.0, dirY: 0.5,
  animState: 'idle', facing: 1, speed: 5.5,
  attackTimer: 0.2, attackDuration: 0.32,
  destination: {{ wx: 15, wy: 15, active: true }}
}};
win.player = global.player;

// Load collision engine
(0, eval)(fs.readFileSync({json.dumps(str(COLLISION_ENGINE_JS))}, 'utf8'));

// Import worldmap dungeons
const {{ teleportToZone, showNoticeModal }} = await import('./client/webapp/js/ui/worldmap_dungeons.js');
global.teleportToZone = teleportToZone;
win.teleportToZone = teleportToZone;
global.showNoticeModal = showNoticeModal;
win.showNoticeModal = showNoticeModal;

// Load monster system in global scope
(0, eval)(fs.readFileSync({json.dumps(str(MONSTER_SYSTEM_JS))}, 'utf8'));

// Load combat skills in global scope
(0, eval)(fs.readFileSync({json.dumps(str(COMBAT_SKILLS_JS))}, 'utf8'));

{script_body}
"""
    res = subprocess.run(["node", "--input-type=module", "-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 TestPlayerDeathAndRevival(unittest.TestCase):
    """Test suite ensuring player death cleanly revives and restores all mobility & actions."""

    def test_revival_restores_mobility_and_clears_wilderness_grid(self):
        """When killed in a wilderness zone, player must respawn at sanctuary (0,0) with valid collision and idle animState."""
        script = """
// Simulate player in wilderness zone with procedural grid loaded
window.currentZoneId = 'zone_tang_kiem_nhai';
window.currentMapWidth = 40;
window.currentMapHeight = 40;
window.currentMapGrid = new Uint8Array(40 * 40);
window.getTileAt = (tx, ty) => 1; // 1 = WALKABLE_DIRT

// Fatal attack on player
executeMonsterAttackOnPlayer({ attackDmg: 500, id: 'mob_hound', wx: 10, wy: 10 });

// Trigger the scheduled revival timeout(s)
global.runAllTimeouts();

// Check state after revival
const isBlocked = isPositionBlocked(player.wx, player.wy, 0.35);
const slideMove = resolveMovementWithSliding(player.wx, player.wy, 0.5, 0.0, 0.35);

const result = {
  hp: player.hp,
  maxHp: player.maxHp,
  isDead: player.isDead,
  animState: player.animState,
  wx: player.wx,
  wy: player.wy,
  zoneId: window.currentZoneId,
  mapGridCleared: window.currentMapGrid === null,
  isPositionBlocked: isBlocked,
  slideMovementAllowed: (slideMove.wx !== player.wx || slideMove.wy !== player.wy),
  destActive: player.destination?.active,
  dirX: player.dirX,
  dirY: player.dirY
};
console.log(JSON.stringify(result));
"""
        data = run_revival_node_harness(script)
        # Check all requirements:
        self.assertEqual(data["hp"], data["maxHp"], "Player HP must be fully restored upon revival")
        self.assertFalse(data["isDead"], "Player isDead must be false after revival")
        self.assertEqual(data["zoneId"], "zone_boundless_sanctuary", "Player must respawn at Boundless Sanctuary")
        self.assertTrue(data["mapGridCleared"], "Wilderness map grid must be cleared upon respawning in sanctuary")
        self.assertEqual(data["wx"], 0.0, "Player respawn wx must be 0.0 at Sanctuary")
        self.assertEqual(data["wy"], 0.0, "Player respawn wy must be 0.0 at Sanctuary")
        self.assertFalse(data["isPositionBlocked"], "Player spawn location must NOT be blocked by collision")
        self.assertEqual(data["animState"], "idle", "Player animState must be idle after revival")
        self.assertFalse(data["destActive"], "Old movement destination must be cancelled")
        self.assertEqual(data["dirX"], 0, "dirX must be reset to 0 upon revival")
        self.assertEqual(data["dirY"], 0, "dirY must be reset to 0 upon revival")

    def test_dead_player_actions_locked_and_revival_restores_combat(self):
        """When player is dead, actions and skills are locked. After revival, attacks, dodge and potions function normally."""
        script = """
window.currentZoneId = 'zone_tang_kiem_nhai';
// 1. Kill player
executeMonsterAttackOnPlayer({ attackDmg: 500, id: 'mob_hound', wx: 10, wy: 10 });

const deadState = {
  isDead: player.isDead,
  hp: player.hp
};

// Try acting while dead
doPrimaryAttack();
const attackWhileDeadFired = (player.attackTimer > 0 || player.animState === 'attack');

doDodge();
const dodgeWhileDeadFired = !!player.isIFrame;

drinkHealthPotion();
const potionWhileDeadHealed = (player.hp > 0);

// Extra hit while dead should be ignored
executeMonsterAttackOnPlayer({ attackDmg: 200, id: 'mob_hound', wx: 10, wy: 10 });
const hitWhileDeadHp = player.hp;

// 2. Complete revival
global.runAllTimeouts();

const revivedState = {
  isDead: player.isDead,
  hp: player.hp,
  maxHp: player.maxHp,
  animState: player.animState
};

// 3. Act after revival
doPrimaryAttack();
const attackAfterRevival = {
  attackTimer: player.attackTimer,
  animState: player.animState
};

doDodge();
const dodgeAfterRevival = !!player.isIFrame;

console.log(JSON.stringify({
  deadState,
  attackWhileDeadFired,
  dodgeWhileDeadFired,
  potionWhileDeadHealed,
  hitWhileDeadHp,
  revivedState,
  attackAfterRevival,
  dodgeAfterRevival
}));
"""
        data = run_revival_node_harness(script)
        self.assertTrue(data["deadState"]["isDead"])
        self.assertEqual(data["deadState"]["hp"], 0)
        self.assertFalse(data["attackWhileDeadFired"], "Attacking while dead must be prohibited")
        self.assertFalse(data["dodgeWhileDeadFired"], "Dodging while dead must be prohibited")
        self.assertFalse(data["potionWhileDeadHealed"], "Drinking potion while dead must be prohibited")
        self.assertEqual(data["hitWhileDeadHp"], 0, "Taking damage while dead must not alter HP")

        self.assertFalse(data["revivedState"]["isDead"])
        self.assertEqual(data["revivedState"]["hp"], data["revivedState"]["maxHp"])
        self.assertEqual(data["revivedState"]["animState"], "idle")

        self.assertGreater(data["attackAfterRevival"]["attackTimer"], 0, "Player must be able to attack after revival")
        self.assertEqual(data["attackAfterRevival"]["animState"], "attack")
        self.assertTrue(data["dodgeAfterRevival"], "Player must be able to dodge after revival")


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