#!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
import shutil
import subprocess
import time
from pathlib import Path
from urllib.parse import urlencode, urljoin

from verify_mobile_browser_controls import (
    WebDriverError,
    execute_js,
    free_port,
    request_json,
    wait_for_driver,
    wait_for_map_runtime,
    wait_for_page,
)


ROOT = Path(__file__).resolve().parents[1]


def load_title(base: str, port: int, session_id: str) -> str:
    query = urlencode({"game": "1", "_": str(time.time_ns())})
    url = urljoin(base.rstrip("/") + "/", f"/web/game.html?{query}")
    request_json(port, "POST", f"/session/{session_id}/url", {"url": url}, timeout=30)
    wait_for_page(port, session_id)
    return url


def wait_for_title(port: int, session_id: str, timeout: float = 8) -> dict:
    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        state = execute_js(
            port,
            session_id,
            """
return {
  readyState: document.readyState,
  scene: typeof scene === 'undefined' ? '' : scene,
  titleMenuMode: typeof titleMenuMode === 'undefined' ? '' : titleMenuMode,
  selectedTitleMenuIndex: typeof selectedTitleMenuIndex === 'undefined' ? null : selectedTitleMenuIndex,
  titleMenuKeys: typeof titleMenuItems === 'function' ? titleMenuItems().map((item) => item.key) : [],
  titleMenuLabels: typeof titleMenuItems === 'function' ? titleMenuItems().map((item) => item.label) : [],
};
""",
            timeout=3,
        )
        if state.get("scene") == "title" and state.get("titleMenuKeys"):
            return state
        time.sleep(0.2)
    raise WebDriverError("title scene did not become ready")


def title_menu_sound_input_script() -> str:
    return """
localStorage.removeItem(RUNTIME_SAVE_KEY);
window.HWANSE_TITLE_MENU_SOUND_LOG = [];
window.HWANSE_LAST_TITLE_MENU_SOUND = null;
window.HWANSE_SOUND_LOG = [];
window.HWANSE_SOUND_COUNTS = {};
titleMenuMode = 'main';
selectedTitleMenuIndex = 1;
function titleSnapshot() {
  const items = titleMenuItems();
  const selected = items[selectedTitleMenuIndex] || null;
  return {
    scene,
    titleMenuMode,
    selectedTitleMenuIndex,
    selectedKey: selected?.key || '',
    selectedLabel: selected?.label || '',
    keys: items.map((item) => item.key),
  };
}
function dispatchTitleKey(code) {
  const event = new KeyboardEvent('keydown', {
    bubbles: true,
    cancelable: true,
    code,
    key: code === 'Enter' ? 'Enter' : code.replace('Arrow', ''),
  });
  window.dispatchEvent(event);
  return {
    code,
    defaultPrevented: event.defaultPrevented,
    snapshot: titleSnapshot(),
  };
}
const before = titleSnapshot();
const down = dispatchTitleKey('ArrowDown');
const up = dispatchTitleKey('ArrowUp');
const enter = dispatchTitleKey('Enter');
return {
  before,
  down,
  up,
  enter,
  soundLog: window.HWANSE_TITLE_MENU_SOUND_LOG || [],
  soundCounts: window.HWANSE_SOUND_COUNTS || {},
};
"""


def title_menu_pointer_select_script() -> str:
    return """
localStorage.removeItem(RUNTIME_SAVE_KEY);
window.HWANSE_TITLE_MENU_SOUND_LOG = [];
window.HWANSE_LAST_TITLE_MENU_SOUND = null;
window.HWANSE_SOUND_LOG = [];
window.HWANSE_SOUND_COUNTS = {};
titleMenuMode = 'main';
selectedTitleMenuIndex = 1;
const items = titleMenuItems();
const menuWindow = titleMenuWindow(items);
const geometry = titleMenuGeometry(items);
const row = selectedTitleMenuIndex - menuWindow.start;
const canvas = document.getElementById('screen');
const rect = canvas.getBoundingClientRect();
const x = rect.left + ((geometry.x + geometry.width / 2) / canvas.width) * rect.width;
const y = rect.top + ((geometry.y + 10 + row * geometry.rowHeight + geometry.rowHeight / 2) / canvas.height) * rect.height;
const before = {
  scene,
  titleMenuMode,
  selectedTitleMenuIndex,
  selectedKey: items[selectedTitleMenuIndex]?.key || '',
  selectedLabel: items[selectedTitleMenuIndex]?.label || '',
};
const event = new PointerEvent('pointerdown', {
  bubbles: true,
  cancelable: true,
  pointerId: 91,
  pointerType: 'mouse',
  isPrimary: true,
  clientX: x,
  clientY: y,
  buttons: 1,
});
canvas.dispatchEvent(event);
return {
  before,
  defaultPrevented: event.defaultPrevented,
  x,
  y,
  soundLog: window.HWANSE_TITLE_MENU_SOUND_LOG || [],
  soundCounts: window.HWANSE_SOUND_COUNTS || {},
};
"""


def title_menu_mobile_action_script() -> str:
    return """
localStorage.removeItem(RUNTIME_SAVE_KEY);
window.HWANSE_TITLE_MENU_SOUND_LOG = [];
window.HWANSE_LAST_TITLE_MENU_SOUND = null;
window.HWANSE_SOUND_LOG = [];
window.HWANSE_SOUND_COUNTS = {};
titleMenuMode = 'main';
selectedTitleMenuIndex = 1;
const button = document.getElementById('virtualActionButton');
const rect = button.getBoundingClientRect();
const x = rect.left + Math.max(1, rect.width / 2);
const y = rect.top + Math.max(1, rect.height / 2);
const beforeItems = titleMenuItems();
const before = {
  scene,
  titleMenuMode,
  selectedTitleMenuIndex,
  selectedKey: beforeItems[selectedTitleMenuIndex]?.key || '',
  selectedLabel: beforeItems[selectedTitleMenuIndex]?.label || '',
  buttonHidden: button.hidden,
};
const down = new PointerEvent('pointerdown', {
  bubbles: true,
  cancelable: true,
  pointerId: 92,
  pointerType: 'touch',
  isPrimary: true,
  clientX: x,
  clientY: y,
  buttons: 1,
});
const up = new PointerEvent('pointerup', {
  bubbles: true,
  cancelable: true,
  pointerId: 92,
  pointerType: 'touch',
  isPrimary: true,
  clientX: x,
  clientY: y,
  buttons: 0,
});
button.dispatchEvent(down);
button.dispatchEvent(up);
return {
  before,
  downDefaultPrevented: down.defaultPrevented,
  upDefaultPrevented: up.defaultPrevented,
  soundLog: window.HWANSE_TITLE_MENU_SOUND_LOG || [],
  soundCounts: window.HWANSE_SOUND_COUNTS || {},
};
"""


def title_menu_sound_state_script() -> str:
    return """
return {
  scene,
  map: typeof map === 'undefined' || !map ? '' : map.name,
  titleMenuSoundLog: window.HWANSE_TITLE_MENU_SOUND_LOG || [],
  titleMenuSoundLogLength: (window.HWANSE_TITLE_MENU_SOUND_LOG || []).length,
  titleMenuSoundLogLimit: TITLE_MENU_SOUND_LOG_LIMIT,
  lastTitleMenuSound: window.HWANSE_LAST_TITLE_MENU_SOUND || null,
  soundCounts: window.HWANSE_SOUND_COUNTS || {},
  soundLog: window.HWANSE_SOUND_LOG || [],
  lastSound: (window.HWANSE_SOUND_LOG || []).slice(-1)[0] || null,
  sessionLastTitleMenuSound: JSON.parse(sessionStorage.getItem('HWANSE_LAST_TITLE_MENU_SOUND') || 'null'),
  routeState: typeof currentRouteState === 'function' ? currentRouteState() : null,
};
"""


def verify_title_menu_sound(input_state: dict, final_state: dict) -> None:
    before = input_state.get("before") or {}
    down = input_state.get("down") or {}
    up = input_state.get("up") or {}
    enter = input_state.get("enter") or {}
    log = final_state.get("titleMenuSoundLog") or []
    move_down = log[0] if len(log) > 0 else {}
    move_up = log[1] if len(log) > 1 else {}
    select = log[2] if len(log) > 2 else {}
    counts = final_state.get("soundCounts") or {}
    session_last = final_state.get("sessionLastTitleMenuSound") or {}
    if (
        before.get("scene") != "title"
        or before.get("selectedTitleMenuIndex") != 1
        or before.get("selectedKey") != "playStart"
        or down.get("defaultPrevented") is not True
        or (down.get("snapshot") or {}).get("selectedKey") != "startEncounters"
        or up.get("defaultPrevented") is not True
        or (up.get("snapshot") or {}).get("selectedKey") != "playStart"
        or enter.get("defaultPrevented") is not True
        or final_state.get("scene") != "map"
        or final_state.get("map") != "map1_02b"
        or final_state.get("titleMenuSoundLogLength") != 3
        or final_state.get("titleMenuSoundLogLimit") != 64
        or counts.get("menuMove") != 2
        or counts.get("menuConfirm") != 1
        or move_down.get("source") != "title-menu-sound"
        or move_down.get("action") != "move"
        or move_down.get("direction") != "down"
        or move_down.get("soundKey") != "menuMove"
        or move_down.get("soundSrc") != "../extract_wlk/03.wav"
        or move_down.get("selectedKey") != "startEncounters"
        or move_down.get("inputSource") != "keyboard"
        or move_down.get("browserTitleMenuSoundImplemented") is not True
        or move_down.get("originalTitleMenuSoundRuntimeImplemented") is not False
        or move_up.get("source") != "title-menu-sound"
        or move_up.get("action") != "move"
        or move_up.get("direction") != "up"
        or move_up.get("soundKey") != "menuMove"
        or move_up.get("soundSrc") != "../extract_wlk/03.wav"
        or move_up.get("selectedKey") != "playStart"
        or move_up.get("inputSource") != "keyboard"
        or select.get("source") != "title-menu-sound"
        or select.get("action") != "select"
        or select.get("inputSource") != "keyboard"
        or select.get("soundKey") != "menuConfirm"
        or select.get("soundSrc") != "../extract_wlk/04.wav"
        or select.get("selectedKey") != "playStart"
        or select.get("soundPlayed") is not True
        or select.get("browserTitleMenuSoundImplemented") is not True
        or select.get("originalDirectSoundTimingImplemented") is not False
        or session_last.get("source") != "title-menu-sound"
        or session_last.get("soundSrc") != "../extract_wlk/04.wav"
    ):
        raise WebDriverError(f"title menu sound verification failed: input={input_state!r} final={final_state!r}")


def verify_title_menu_single_select(input_state: dict, final_state: dict, input_source: str) -> None:
    before = input_state.get("before") or {}
    log = final_state.get("titleMenuSoundLog") or []
    select = log[0] if log else {}
    counts = final_state.get("soundCounts") or {}
    session_last = final_state.get("sessionLastTitleMenuSound") or {}
    if (
        before.get("scene") != "title"
        or before.get("selectedTitleMenuIndex") != 1
        or before.get("selectedKey") != "playStart"
        or final_state.get("scene") != "map"
        or final_state.get("map") != "map1_02b"
        or final_state.get("titleMenuSoundLogLength") != 1
        or final_state.get("titleMenuSoundLogLimit") != 64
        or counts.get("menuConfirm") != 1
        or select.get("source") != "title-menu-sound"
        or select.get("action") != "select"
        or select.get("inputSource") != input_source
        or select.get("soundKey") != "menuConfirm"
        or select.get("soundSrc") != "../extract_wlk/04.wav"
        or select.get("selectedKey") != "playStart"
        or select.get("soundPlayed") is not True
        or select.get("browserTitleMenuSoundImplemented") is not True
        or select.get("originalTitleMenuSoundRuntimeImplemented") is not False
        or select.get("originalDirectSoundTimingImplemented") is not False
        or session_last.get("source") != "title-menu-sound"
        or session_last.get("inputSource") != input_source
        or session_last.get("soundSrc") != "../extract_wlk/04.wav"
    ):
        raise WebDriverError(
            f"title menu {input_source} sound verification failed: input={input_state!r} final={final_state!r}"
        )


def summarize(final_state: dict) -> str:
    log = final_state.get("titleMenuSoundLog") or []
    actions = ",".join(str((entry or {}).get("action") or "") for entry in log)
    sources = ",".join(str((entry or {}).get("soundSrc") or "") for entry in log)
    keys = ",".join(str((entry or {}).get("selectedKey") or "") for entry in log)
    input_sources = ",".join(str((entry or {}).get("inputSource") or "") for entry in log)
    counts = final_state.get("soundCounts") or {}
    return (
        f"scene={final_state.get('scene')} map={final_state.get('map')} "
        f"titleMenuSound=title-menu-sound:{actions} "
        f"titleMenuSoundSrcs={sources} selectedKeys={keys} inputSources={input_sources} "
        f"menuMoveSoundCount={counts.get('menuMove')} menuConfirmSoundCount={counts.get('menuConfirm')} "
        f"browserTitleMenuSound={all((entry or {}).get('browserTitleMenuSoundImplemented') is True for entry in log)}"
    )


def write_report(
    base: str,
    keyboard_input_state: dict,
    keyboard_final_state: dict,
    pointer_input_state: dict,
    pointer_final_state: dict,
    mobile_input_state: dict,
    mobile_final_state: dict,
) -> None:
    out_dir = ROOT / "out"
    out_dir.mkdir(parents=True, exist_ok=True)
    payload = {
        "status": "passed",
        "base": base,
        "titleMenuSound": summarize(keyboard_final_state),
        "titleMenuPointerSound": summarize(pointer_final_state),
        "titleMenuMobileSound": summarize(mobile_final_state),
        "snapshots": {
            "keyboardInput": keyboard_input_state,
            "keyboardFinal": keyboard_final_state,
            "pointerInput": pointer_input_state,
            "pointerFinal": pointer_final_state,
            "mobileInput": mobile_input_state,
            "mobileFinal": mobile_final_state,
        },
    }
    (out_dir / "title_menu_sound_browser_smoke.json").write_text(
        json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True),
        encoding="utf-8",
    )
    lines = [
        "# Title Menu Sound Browser Smoke",
        "",
        "- status: `passed`",
        f"- title menu sound: `{payload['titleMenuSound']}`",
        f"- title menu pointer sound: `{payload['titleMenuPointerSound']}`",
        f"- title menu mobile sound: `{payload['titleMenuMobileSound']}`",
    ]
    (out_dir / "title_menu_sound_browser_smoke.md").write_text("\n".join(lines), encoding="utf-8")


def verify_browser(base: str) -> None:
    driver_path = shutil.which("WebKitWebDriver")
    if not driver_path:
        raise WebDriverError("WebKitWebDriver is not installed; install webkit2gtk-driver and run under Xvfb")

    port = free_port()
    log_path = ROOT / "out" / "title_menu_sound_webkitdriver.log"
    log_path.parent.mkdir(parents=True, exist_ok=True)
    with log_path.open("wb") as log_file:
        proc = subprocess.Popen(
            [
                driver_path,
                "--host=127.0.0.1",
                f"--port={port}",
                "--replace-on-new-session",
            ],
            stdout=log_file,
            stderr=subprocess.STDOUT,
        )
        session_id = ""
        try:
            wait_for_driver(port, proc)
            session = request_json(
                port,
                "POST",
                "/session",
                {"capabilities": {"alwaysMatch": {"browserName": "MiniBrowser"}}},
                timeout=30,
            )
            session_id = str(session["value"]["sessionId"])
            request_json(
                port,
                "POST",
                f"/session/{session_id}/window/rect",
                {"x": 0, "y": 0, "width": 860, "height": 620},
                timeout=8,
            )
            load_title(base, port, session_id)
            wait_for_title(port, session_id)
            keyboard_input_state = execute_js(port, session_id, title_menu_sound_input_script(), timeout=5)
            wait_for_map_runtime(port, session_id, "map1_02b")
            keyboard_final_state = execute_js(port, session_id, title_menu_sound_state_script(), timeout=5)
            verify_title_menu_sound(keyboard_input_state, keyboard_final_state)

            load_title(base, port, session_id)
            wait_for_title(port, session_id)
            pointer_input_state = execute_js(port, session_id, title_menu_pointer_select_script(), timeout=5)
            wait_for_map_runtime(port, session_id, "map1_02b")
            pointer_final_state = execute_js(port, session_id, title_menu_sound_state_script(), timeout=5)
            verify_title_menu_single_select(pointer_input_state, pointer_final_state, "canvas-pointer")

            load_title(base, port, session_id)
            wait_for_title(port, session_id)
            mobile_input_state = execute_js(port, session_id, title_menu_mobile_action_script(), timeout=5)
            wait_for_map_runtime(port, session_id, "map1_02b")
            mobile_final_state = execute_js(port, session_id, title_menu_sound_state_script(), timeout=5)
            verify_title_menu_single_select(mobile_input_state, mobile_final_state, "mobile-action")

            write_report(
                base,
                keyboard_input_state,
                keyboard_final_state,
                pointer_input_state,
                pointer_final_state,
                mobile_input_state,
                mobile_final_state,
            )
            print(
                "ok title menu sound "
                f"keyboard={summarize(keyboard_final_state)} "
                f"pointer={summarize(pointer_final_state)} "
                f"mobile={summarize(mobile_final_state)}"
            )
        finally:
            if session_id:
                try:
                    request_json(port, "DELETE", f"/session/{session_id}", timeout=5)
                except Exception:
                    pass
            proc.terminate()
            try:
                proc.wait(timeout=5)
            except subprocess.TimeoutExpired:
                proc.kill()


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--base", default="http://127.0.0.1:8013")
    args = parser.parse_args()
    verify_browser(args.base)


if __name__ == "__main__":
    main()
