"""Unit Test Suite: Game Pause Mechanism & Overlay System (PoE2 Standard - Milestone 3).

Empirical verification of:
1. Initial pause state (window.isGamePaused === false, #overlay-pause hidden)
2. Direct state toggling (setGamePaused(true/false), toggleGamePause())
3. Settings modal integration (openSettingsModal pauses, closeSettingsModal unpauses)
4. Desktop Escape key integration (toggles pause when clean; unpauses when closing settings)
5. Pause overlay tap/click unpause handler
6. Skill bar and click input freeze (activation rejected, effectiveDt = 0)
7. Kinematics freeze (player position does not advance when paused)
8. Structural hygiene & line caps (canvas_renderer <= 350, index.html <= 400, no modal- prefix on overlay)
"""

import json
import subprocess
from pathlib import Path
import pytest

PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
CLIENT_WEBAPP = PROJECT_ROOT / "client" / "webapp"
CANVAS_RENDERER_JS = CLIENT_WEBAPP / "js" / "engine" / "canvas_renderer.js"
INDEX_HTML = CLIENT_WEBAPP / "index.html"
FEEDBACK_JS = CLIENT_WEBAPP / "js" / "ui" / "feedback.js"
MAIN_JS = CLIENT_WEBAPP / "js" / "main.js"
SKILL_BAR_JS = CLIENT_WEBAPP / "js" / "ui" / "skill_bar_controller.js"
COMBAT_SKILLS_JS = CLIENT_WEBAPP / "js" / "engine" / "combat_skills.js"

MOCK_DOM_PREAMBLE = """
import fs from 'fs';
const elements = new Map(), docListeners = new Map();
function makeElement(id, tagName = 'DIV') {
  const classes = new Set(['hidden']), styles = {}, handlers = new Map(), children = [];
  return {
    id, tagName, dataset: {}, offsetWidth: 44, offsetHeight: 44, children,
    classList: {
      add: c => classes.add(c), remove: c => classes.delete(c), contains: c => classes.has(c),
      toggle: (c, f) => { const h = classes.has(c); const s = f === undefined ? !h : Boolean(f); s ? classes.add(c) : classes.delete(c); return s; }
    },
    style: { setProperty: (k, v) => { styles[k] = v; }, getProperty: k => styles[k] },
    addEventListener: (t, fn) => { if (!handlers.has(t)) handlers.set(t, []); handlers.get(t).push(fn); },
    removeEventListener: (t, fn) => { if (handlers.has(t)) handlers.set(t, handlers.get(t).filter(h => h !== fn)); },
    dispatchEvent: function(evt) { evt.target = evt.target || this; evt.currentTarget = this; (handlers.get(evt.type) || []).forEach(fn => fn(evt)); },
    closest: function(sel) { return (sel === '#' + id || sel.includes(id)) ? this : null; },
    querySelectorAll: () => [], querySelector: sel => { const c = makeElement(id + '_' + sel); children.push(c); return c; }
  };
}
function getOrCreate(id) { if (!elements.has(id)) elements.set(id, makeElement(id)); return elements.get(id); }
['hud-skill-bar', 'skill-potion', 'skill-fire', 'skill-slot-w', 'skill-thunder', 'skill-frost', 'skill-primary', 'skill-dodge', 'overlay-pause', 'modal-settings', 'modal-item-link-tooltip', 'chat-expanded-panel'].forEach(id => getOrCreate(id));
const doc = {
  getElementById: id => getOrCreate(id), querySelector: sel => getOrCreate(sel.replace('#', '')),
  querySelectorAll: sel => sel.includes('[id^="modal-"]:not(.hidden)') ? Array.from(elements.entries()).filter(([id, el]) => id.startsWith('modal-') && !el.classList.contains('hidden')).map(([, el]) => el) : [],
  activeElement: { tagName: 'BODY' }, readyState: 'complete',
  addEventListener: (t, fn) => { if (!docListeners.has(t)) docListeners.set(t, []); docListeners.get(t).push(fn); },
  removeEventListener: (t, fn) => { if (docListeners.has(t)) docListeners.set(t, docListeners.get(t).filter(h => h !== fn)); },
  dispatchEvent: evt => (docListeners.get(evt.type) || []).forEach(fn => fn(evt))
};
const win = { isGamePaused: false, document: doc, performance: { now: () => 1000 }, requestAnimationFrame: () => 1, cancelAnimationFrame: () => {}, addEventListener: () => {}, removeEventListener: () => {}, resetJoystick: () => { win.resetJoystickCalled = true; }, resetJoystickCalled: false };
global.window = win; global.document = doc; global.resetJoystick = win.resetJoystick; global.performance = win.performance; global.requestAnimationFrame = win.requestAnimationFrame; global.cancelAnimationFrame = win.cancelAnimationFrame;
"""


def run_node_harness(script_body: str) -> dict:
    """Executes a JavaScript test harness in Node.js with simulated browser environment."""
    bootstrap = f"{MOCK_DOM_PREAMBLE}\n(async () => {{\n  try {{\n{script_body}\n  }} catch (err) {{\n    console.error(err);\n    process.exit(1);\n  }}\n}})();\n"
    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 TestPauseStateAndController:
    """Verifies state management, overlay toggling, and controller exports."""

    def test_initial_state_unpaused_and_overlay_hidden(self) -> None:
        """Initial state must have isGamePaused === false and #overlay-pause hidden."""
        js_code = """
        const rendererCode = fs.readFileSync('./client/webapp/js/engine/canvas_renderer.js', 'utf8');
        eval(rendererCode);
        const overlay = document.getElementById('overlay-pause');
        console.log(JSON.stringify({
            isGamePaused: window.isGamePaused,
            overlayHidden: overlay.classList.contains('hidden'),
            hasSetGamePaused: typeof window.setGamePaused === 'function',
            hasToggleGamePause: typeof window.toggleGamePause === 'function'
        }));
        """
        data = run_node_harness(js_code)
        assert data["isGamePaused"] is False
        assert data["overlayHidden"] is True
        assert data["hasSetGamePaused"] is True
        assert data["hasToggleGamePause"] is True

    def test_set_game_paused_controls_overlay_and_joystick(self) -> None:
        """setGamePaused(true) shows overlay and resets joystick; setGamePaused(false) hides overlay."""
        js_code = """
        const rendererCode = fs.readFileSync('./client/webapp/js/engine/canvas_renderer.js', 'utf8');
        eval(rendererCode);
        const overlay = document.getElementById('overlay-pause');
        window.setGamePaused(true);
        const stateAfterPause = { isPaused: window.isGamePaused, overlayHidden: overlay.classList.contains('hidden'), joystickReset: window.resetJoystickCalled };
        window.setGamePaused(false);
        const stateAfterResume = { isPaused: window.isGamePaused, overlayHidden: overlay.classList.contains('hidden') };
        console.log(JSON.stringify({ stateAfterPause, stateAfterResume }));
        """
        data = run_node_harness(js_code)
        assert data["stateAfterPause"]["isPaused"] is True
        assert data["stateAfterPause"]["overlayHidden"] is False
        assert data["stateAfterPause"]["joystickReset"] is True
        assert data["stateAfterResume"]["isPaused"] is False
        assert data["stateAfterResume"]["overlayHidden"] is True

    def test_toggle_game_pause_alternates_state(self) -> None:
        """toggleGamePause() toggles state between true and false."""
        js_code = """
        const rendererCode = fs.readFileSync('./client/webapp/js/engine/canvas_renderer.js', 'utf8');
        eval(rendererCode);
        const initial = window.isGamePaused;
        window.toggleGamePause();
        const first = window.isGamePaused;
        window.toggleGamePause();
        const second = window.isGamePaused;
        console.log(JSON.stringify({ initial, first, second }));
        """
        data = run_node_harness(js_code)
        assert data["initial"] is False
        assert data["first"] is True
        assert data["second"] is False


class TestSettingsModalAndEscapeIntegration:
    """Verifies pause integration with Settings modal and Escape key handling."""

    def test_settings_modal_open_and_close_triggers_pause(self) -> None:
        """Opening settings modal pauses game; closing settings unpauses."""
        js_code = """
        const rendererCode = fs.readFileSync('./client/webapp/js/engine/canvas_renderer.js', 'utf8');
        eval(rendererCode);
        const { openSettingsModal, closeSettingsModal } = await import('./client/webapp/js/ui/feedback.js');
        const modal = document.getElementById('modal-settings');
        openSettingsModal();
        const pausedOnOpen = window.isGamePaused, modalVisible = !modal.classList.contains('hidden');
        closeSettingsModal();
        const pausedOnClose = window.isGamePaused, modalHidden = modal.classList.contains('hidden');
        console.log(JSON.stringify({ pausedOnOpen, modalVisible, pausedOnClose, modalHidden }));
        """
        data = run_node_harness(js_code)
        assert data["pausedOnOpen"] is True
        assert data["modalVisible"] is True
        assert data["pausedOnClose"] is False
        assert data["modalHidden"] is True

    def test_escape_toggles_pause_when_no_modal_open(self) -> None:
        """Pressing Escape when no modal is open toggles game pause."""
        js_code = """
        const rendererCode = fs.readFileSync('./client/webapp/js/engine/canvas_renderer.js', 'utf8');
        eval(rendererCode);
        await import('./client/webapp/js/main.js');
        document.dispatchEvent({ type: 'keydown', key: 'Escape' });
        const pausedFirst = window.isGamePaused;
        document.dispatchEvent({ type: 'keydown', key: 'Escape' });
        const pausedSecond = window.isGamePaused;
        console.log(JSON.stringify({ pausedFirst, pausedSecond }));
        """
        data = run_node_harness(js_code)
        assert data["pausedFirst"] is True
        assert data["pausedSecond"] is False

    def test_escape_closes_settings_modal_and_unpauses(self) -> None:
        """Pressing Escape while settings modal is open closes it and unpauses."""
        js_code = """
        const rendererCode = fs.readFileSync('./client/webapp/js/engine/canvas_renderer.js', 'utf8');
        eval(rendererCode);
        const { openSettingsModal } = await import('./client/webapp/js/ui/feedback.js');
        await import('./client/webapp/js/main.js');
        openSettingsModal();
        const stateBefore = { isPaused: window.isGamePaused };
        document.dispatchEvent({ type: 'keydown', key: 'Escape' });
        const modalSettings = document.getElementById('modal-settings');
        const stateAfter = { isPaused: window.isGamePaused, modalHidden: modalSettings.classList.contains('hidden') };
        console.log(JSON.stringify({ stateBefore, stateAfter }));
        """
        data = run_node_harness(js_code)
        assert data["stateBefore"]["isPaused"] is True
        assert data["stateAfter"]["isPaused"] is False
        assert data["stateAfter"]["modalHidden"] is True

    def test_overlay_pause_click_unpauses(self) -> None:
        """Clicking on #overlay-pause calls setGamePaused(false) and resumes game."""
        js_code = """
        const rendererCode = fs.readFileSync('./client/webapp/js/engine/canvas_renderer.js', 'utf8');
        eval(rendererCode);
        await import('./client/webapp/js/main.js');
        window.setGamePaused(true);
        const statePaused = window.isGamePaused;
        const overlay = document.getElementById('overlay-pause');
        document.dispatchEvent({ type: 'click', target: overlay });
        const stateResumed = window.isGamePaused;
        console.log(JSON.stringify({ statePaused, stateResumed }));
        """
        data = run_node_harness(js_code)
        assert data["statePaused"] is True
        assert data["stateResumed"] is False


class TestSimulationGatingAndInputFreeze:
    """Verifies that player input, skill activations, and kinematics are completely frozen during pause."""

    def test_skill_activation_rejected_when_paused(self) -> None:
        """activateSlot() returns false immediately when window.isGamePaused is true."""
        js_code = """
        const { SkillBarController } = await import('./client/webapp/js/ui/skill_bar_controller.js');
        const controller = new SkillBarController();
        controller.slots.set('fire', {
            slotId: 'fire', skillId: 'fire_slash', charges: null, maxCharges: null,
            cooldownRemaining: 0, cooldownTotal: 5.0, onActivate: () => {}
        });
        window.isGamePaused = false;
        const successWhenUnpaused = controller.activateSlot('fire');
        window.isGamePaused = true;
        const successWhenPaused = controller.activateSlot('fire');
        console.log(JSON.stringify({ successWhenUnpaused, successWhenPaused }));
        """
        data = run_node_harness(js_code)
        assert data["successWhenUnpaused"] is True
        assert data["successWhenPaused"] is False

    def test_kinematics_frozen_in_canvas_renderer(self) -> None:
        """Canvas renderer code contains strict !isPaused gate around kinematics and updates."""
        content = CANVAS_RENDERER_JS.read_text(encoding="utf-8")
        assert "const isPaused = Boolean(window.isGamePaused);" in content
        assert "if (!isPaused) {" in content
        assert "renderDt = isPaused ? 0 : dt" in content
        assert "!isPaused && screenShake.timer > 0" in content

    def test_combat_skills_click_rejected_when_paused(self) -> None:
        """combat_skills.js canvas click handler contains isGamePaused early return."""
        content = COMBAT_SKILLS_JS.read_text(encoding="utf-8")
        assert "if (typeof window !== 'undefined' && window.isGamePaused) return;" in content


class TestCodeHygieneAndLayoutCompliance:
    """Enforces quantitative line caps, structural requirements, and no dummy implementations."""

    def test_canvas_renderer_line_cap_350(self) -> None:
        """canvas_renderer.js must strictly not exceed 350 lines."""
        lines = len(CANVAS_RENDERER_JS.read_text(encoding="utf-8").splitlines())
        assert lines <= 350, f"canvas_renderer.js has {lines} lines (exceeds 350 cap)"

    def test_index_html_line_cap_400(self) -> None:
        """index.html must strictly not exceed 400 lines."""
        lines = len(INDEX_HTML.read_text(encoding="utf-8").splitlines())
        assert lines <= 400, f"index.html has {lines} lines (exceeds 400 cap)"

    def test_index_html_pause_overlay_structure(self) -> None:
        """#overlay-pause must exist inside #app-viewport, above #modal-container, with z-[35]."""
        content = INDEX_HTML.read_text(encoding="utf-8")
        assert 'id="overlay-pause"' in content
        assert "z-[35]" in content
        assert "TẠM DỪNG / PAUSED" in content
        assert 'id="modal-pause"' not in content
        overlay_idx = content.find('id="overlay-pause"')
        modal_container_idx = content.find('id="modal-container"')
        assert overlay_idx != -1 and modal_container_idx != -1
        assert overlay_idx < modal_container_idx, "#overlay-pause must be placed above #modal-container"

    def test_feedback_js_line_caps(self) -> None:
        """feedback.js line count must remain <= 350 soft cap and <= 500 hard cap."""
        lines = len(FEEDBACK_JS.read_text(encoding="utf-8").splitlines())
        assert lines <= 350, f"feedback.js has {lines} lines (exceeds 350 soft cap)"
        assert lines <= 500, f"feedback.js has {lines} lines (exceeds 500 hard cap)"

    def test_skill_bar_controller_line_caps(self) -> None:
        """skill_bar_controller.js line count must remain <= 350 soft cap and <= 500 hard cap."""
        lines = len(SKILL_BAR_JS.read_text(encoding="utf-8").splitlines())
        assert lines <= 350, f"skill_bar_controller.js has {lines} lines (exceeds 350 soft cap)"
        assert lines <= 500, f"skill_bar_controller.js has {lines} lines (exceeds 500 hard cap)"
