#!/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_url(base: str, port: int, session_id: str, params: dict[str, str]) -> str:
    query = urlencode({**params, "_": 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_scene(port: int, session_id: str, expected: 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,
  map: typeof map === 'undefined' || !map ? '' : map.name,
  musicCue: window.HWANSE_LAST_MUSIC_CUE || null,
  musicCueLog: window.HWANSE_MUSIC_CUE_LOG || [],
  musicCueLogLength: (window.HWANSE_MUSIC_CUE_LOG || []).length,
  musicCues: window.HWANSE_MUSIC_CUES || null,
  musicSynth: window.HWANSE_LAST_MUSIC_SYNTH || null,
  musicSynthLog: window.HWANSE_MUSIC_SYNTH_LOG || [],
  musicSynthLogLength: (window.HWANSE_MUSIC_SYNTH_LOG || []).length,
  battleSummaryMusicCue: (window.HWANSE_LAST_BATTLE_PROTOTYPE || {}).musicCue || null,
};
""",
            timeout=3,
        )
        if state.get("scene") == expected and state.get("musicCue"):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"scene {expected!r} did not publish music cue")


def music_synth_state_script() -> str:
    return """
return {
  readyState: document.readyState,
  scene: typeof scene === 'undefined' ? '' : scene,
  map: typeof map === 'undefined' || !map ? '' : map.name,
  musicCue: window.HWANSE_LAST_MUSIC_CUE || null,
  musicCueLog: window.HWANSE_MUSIC_CUE_LOG || [],
  musicCueLogLength: (window.HWANSE_MUSIC_CUE_LOG || []).length,
  musicCues: window.HWANSE_MUSIC_CUES || null,
  musicSynth: window.HWANSE_LAST_MUSIC_SYNTH || null,
  musicSynthLog: window.HWANSE_MUSIC_SYNTH_LOG || [],
  musicSynthLogLength: (window.HWANSE_MUSIC_SYNTH_LOG || []).length,
  musicSynthState: {
    userActivated: Boolean((window.HWANSE_MUSIC_SYNTH_STATE || {}).userActivated),
    activeCue: ((window.HWANSE_MUSIC_SYNTH_STATE || {}).activeCue || {}).cue || '',
    pendingCue: ((window.HWANSE_MUSIC_SYNTH_STATE || {}).pendingCue || {}).cue || '',
  },
  battleSummaryMusicCue: (window.HWANSE_LAST_BATTLE_PROTOTYPE || {}).musicCue || null,
};
"""


def trigger_music_synth(port: int, session_id: str, expected_cue: str, timeout: float = 12) -> dict:
    execute_js(
        port,
        session_id,
        """
if (typeof window.HWANSE_UNLOCK_MUSIC_SYNTH === 'function') {
  window.HWANSE_UNLOCK_MUSIC_SYNTH({
    inputSource: 'browser-smoke',
    reason: 'runtime-music-synth-smoke',
    force: true,
  });
}
return {
  cue: (window.HWANSE_LAST_MUSIC_CUE || {}).cue || '',
  synth: window.HWANSE_LAST_MUSIC_SYNTH || null,
};
""",
        timeout=3,
    )
    deadline = time.monotonic() + timeout
    last_state: dict = {}
    while time.monotonic() < deadline:
        state = execute_js(port, session_id, music_synth_state_script(), timeout=3)
        last_state = state
        synth = state.get("musicSynth") or {}
        cue = state.get("musicCue") or {}
        if (
            cue.get("cue") == expected_cue
            and synth.get("cue") == expected_cue
            and synth.get("status") == "scheduled"
            and int(synth.get("scheduledNoteCount") or 0) > 0
        ):
            return state
        if synth.get("status") in {"error", "unsupported-audio-context", "no-midi-notes"}:
            raise WebDriverError(f"runtime music synth failed: {state!r}")
        time.sleep(0.2)
    raise WebDriverError(f"runtime music synth did not schedule {expected_cue!r}: {last_state!r}")


def verify_music_cue(state: dict, expected: dict) -> None:
    cue = state.get("musicCue") or {}
    log = state.get("musicCueLog") or []
    cues = state.get("musicCues") or {}
    table_cue = cues.get(expected["cue"]) or {}
    synth = state.get("musicSynth") or {}
    synth_log = state.get("musicSynthLog") or []
    if (
        cue.get("source") != "runtime-music-cue"
        or cue.get("cue") != expected["cue"]
        or cue.get("scene") != expected["scene"]
        or cue.get("src") != expected["src"]
        or cue.get("index") != expected["index"]
        or cue.get("archive") != "MIDDATA.MLK"
        or cue.get("browserRuntimeMusicCueImplemented") is not True
        or cue.get("browserMidiSynthPlaybackImplemented") is not True
        or int(cue.get("scheduledMidiSynthNoteCount") or 0) <= 0
        or cue.get("originalMidiPlaybackImplemented") is not False
        or cue.get("originalMusicSelectionRuntimeImplemented") is not False
        or not log
        or (log[-1] or {}).get("cue") != expected["cue"]
        or table_cue.get("src") != expected["src"]
        or table_cue.get("index") != expected["index"]
    ):
        raise WebDriverError(f"runtime music cue mismatch: expected={expected!r} state={state!r}")
    if (
        synth.get("source") != "runtime-midi-synth-playback"
        or synth.get("cue") != expected["cue"]
        or synth.get("src") != expected["src"]
        or synth.get("index") != expected["index"]
        or synth.get("archive") != "MIDDATA.MLK"
        or synth.get("status") != "scheduled"
        or synth.get("browserMidiSynthPlaybackImplemented") is not True
        or synth.get("originalDirectMusicPlaybackImplemented") is not False
        or synth.get("originalMusicSelectionRuntimeImplemented") is not False
        or int(synth.get("midiNoteCount") or 0) <= 0
        or int(synth.get("scheduledNoteCount") or 0) <= 0
        or not synth_log
        or (synth_log[-1] or {}).get("cue") != expected["cue"]
    ):
        raise WebDriverError(f"runtime music synth mismatch: expected={expected!r} state={state!r}")
    if expected.get("map") is not None and cue.get("map") != expected["map"]:
        raise WebDriverError(f"runtime music cue map mismatch: expected={expected!r} state={state!r}")
    battle_summary = state.get("battleSummaryMusicCue") or {}
    if expected["scene"] == "battle" and (
        battle_summary.get("cue") != "battle"
        or battle_summary.get("src") != "../extract_mlk/08.mid"
        or battle_summary.get("index") != 8
    ):
        raise WebDriverError(f"battle summary did not preserve music cue: {state!r}")


def summarize(state: dict) -> str:
    cue = state.get("musicCue") or {}
    synth = state.get("musicSynth") or {}
    return (
        f"scene={state.get('scene')} map={state.get('map')} "
        f"cue={cue.get('cue')} src={cue.get('src')} index={cue.get('index')} "
        f"archive={cue.get('archive')} log={state.get('musicCueLogLength')} "
        f"synth={synth.get('status')} notes={synth.get('scheduledNoteCount')} "
        f"synthSrc={synth.get('src')} "
        f"browserMusicCue={cue.get('browserRuntimeMusicCueImplemented')} "
        f"browserMidiSynth={cue.get('browserMidiSynthPlaybackImplemented')} "
        f"originalMidiPlayback={cue.get('originalMidiPlaybackImplemented')}"
    )


def write_report(base: str, title: dict, map_state: dict, battle: dict) -> None:
    out_dir = ROOT / "out"
    out_dir.mkdir(parents=True, exist_ok=True)
    payload = {
        "status": "passed",
        "base": base,
        "title": summarize(title),
        "map": summarize(map_state),
        "battle": summarize(battle),
        "snapshots": {
            "title": title,
            "map": map_state,
            "battle": battle,
        },
    }
    (out_dir / "runtime_music_cue_browser_smoke.json").write_text(
        json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True),
        encoding="utf-8",
    )
    lines = [
        "# Runtime Music Cue Browser Smoke",
        "",
        "- status: `passed`",
        f"- title: `{payload['title']}`",
        f"- map: `{payload['map']}`",
        f"- battle: `{payload['battle']}`",
    ]
    (out_dir / "runtime_music_cue_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" / "runtime_music_cue_webkitdriver.log"
    log_path.parent.mkdir(parents=True, exist_ok=True)
    with log_path.open("wb") as log:
        proc = subprocess.Popen(
            [
                driver_path,
                "--host=127.0.0.1",
                f"--port={port}",
                "--replace-on-new-session",
            ],
            stdout=log,
            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": 390, "height": 844},
                timeout=8,
            )

            load_url(base, port, session_id, {})
            wait_for_scene(port, session_id, "title")
            title = trigger_music_synth(port, session_id, "title")
            verify_music_cue(
                title,
                {"scene": "title", "cue": "title", "src": "../extract_mlk/00.mid", "index": 0, "map": None},
            )

            load_url(base, port, session_id, {"map": "map1_02b", "startTile": "11,12"})
            wait_for_map_runtime(port, session_id, "map1_02b")
            wait_for_scene(port, session_id, "map")
            map_state = trigger_music_synth(port, session_id, "map")
            verify_music_cue(
                map_state,
                {"scene": "map", "cue": "map", "src": "../extract_mlk/01.mid", "index": 1, "map": "map1_02b"},
            )

            load_url(base, port, session_id, {"map": "map1_02b", "startTile": "11,12", "battle": "1"})
            wait_for_scene(port, session_id, "battle", timeout=10)
            battle = trigger_music_synth(port, session_id, "battle")
            verify_music_cue(
                battle,
                {"scene": "battle", "cue": "battle", "src": "../extract_mlk/08.mid", "index": 8, "map": "map1_02b"},
            )

            write_report(base, title, map_state, battle)
            print(
                "ok runtime music cue "
                f"title={summarize(title)} map={summarize(map_state)} battle={summarize(battle)}"
            )
        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()
