#!/usr/bin/env python3
"""Smoke-test web/midi_bgm_test.html in WebKit."""
from __future__ import annotations

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

from browser_smoke_helpers import (
    WebDriverError,
    execute_async_js,
    execute_js,
    free_port,
    request_json,
    wait_for_driver,
)


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"


def state_script() -> str:
    return """
return {
  readyState: document.readyState,
  ready: window.HWANSE_MIDI_BGM_TEST_READY === true,
  marker: window.HWANSE_LAST_MIDI_BGM_TEST || null,
  helper: window.HWANSE_MIDI_BGM_PLAYER ? {
    defaultMode: window.HWANSE_MIDI_BGM_PLAYER.defaultMode,
    profileCount: window.HWANSE_MIDI_BGM_PLAYER.SOUNDFONT_PROFILES.length,
    synthPlaybackImplemented: window.HWANSE_MIDI_BGM_PLAYER.synthPlaybackImplemented,
    synthMidiControlsImplemented: window.HWANSE_MIDI_BGM_PLAYER.synthMidiControlsImplemented,
    supportedSynthMidiControls: window.HWANSE_MIDI_BGM_PLAYER.supportedSynthMidiControls || [],
    soundfontPlaybackAdapterImplemented: window.HWANSE_MIDI_BGM_PLAYER.soundfontPlaybackAdapterImplemented,
    webMidiApiImplemented: window.HWANSE_MIDI_BGM_PLAYER.webMidiApiImplemented,
    webMidiSupport: window.HWANSE_MIDI_BGM_PLAYER.getWebMidiSupport ? window.HWANSE_MIDI_BGM_PLAYER.getWebMidiSupport() : null,
    webMidiPermissionDiagnostics: typeof window.HWANSE_MIDI_BGM_PLAYER.getWebMidiPermissionState === 'function',
    maxBytes: window.HWANSE_MIDI_BGM_PLAYER.MAX_CUSTOM_SOUNDFONT_BYTES,
  } : null,
  storage: window.HWANSE_MIDI_SOUNDFONT_STORAGE ? {
    indexedDbImplemented: window.HWANSE_MIDI_SOUNDFONT_STORAGE.indexedDbImplemented,
    opfsMirrorImplemented: window.HWANSE_MIDI_SOUNDFONT_STORAGE.opfsMirrorImplemented,
  } : null,
  spessa: window.HWANSE_SPESSASYNTH_ADAPTER ? {
    implemented: window.HWANSE_SPESSASYNTH_ADAPTER.implemented,
    libraryVersion: window.HWANSE_SPESSASYNTH_ADAPTER.libraryVersion,
  } : null,
  trackRows: document.querySelectorAll('[data-midi-track-row]').length,
  status: document.getElementById('playbackStatus')?.textContent || '',
  selectedMode: document.getElementById('modeSelect')?.value || '',
  text: document.body.textContent || '',
};
"""


def wait_for_ready(port: int, session_id: str, timeout: float = 12) -> dict:
    deadline = time.monotonic() + timeout
    last_state = {}
    while time.monotonic() < deadline:
        state = execute_js(port, session_id, state_script(), timeout=4)
        last_state = state
        marker = state.get("marker") or {}
        helper = state.get("helper") or {}
        storage = state.get("storage") or {}
        spessa = state.get("spessa") or {}
        if (
            state.get("readyState") == "complete"
            and state.get("ready") is True
            and state.get("trackRows") == 20
            and marker.get("defaultMode") == "webaudio-synth"
            and marker.get("midiTrackCount") == 20
            and marker.get("parsedTrackCount") == 20
            and marker.get("soundfontProfileCount") == 3
            and marker.get("soundfontUploadValidationImplemented") is True
            and marker.get("soundfontPlaybackAdapterImplemented") is True
            and marker.get("spessaSynthAdapterImplemented") is True
            and marker.get("soundfontLazyLoadingImplemented") is True
            and marker.get("bundledProfileCacheImplemented") is True
            and marker.get("indexedDbSoundfontStorageImplemented") is True
            and marker.get("opfsSoundfontMirrorImplemented") is True
            and marker.get("synthPlaybackImplemented") is True
            and marker.get("synthMidiControlsImplemented") is True
            and marker.get("webMidiApiImplemented") is True
            and isinstance(marker.get("webMidiSupport"), dict)
            and spessa.get("implemented") is True
            and storage.get("indexedDbImplemented") is True
            and helper.get("profileCount") == 3
            and helper.get("synthMidiControlsImplemented") is True
            and helper.get("webMidiApiImplemented") is True
            and helper.get("webMidiPermissionDiagnostics") is True
            and helper.get("soundfontPlaybackAdapterImplemented") is True
            and helper.get("maxBytes") == 104857600
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"MIDI BGM test did not become ready: {last_state!r}")


def require(condition: bool, message: str) -> None:
    if not condition:
        raise AssertionError(message)


def play_first_track(port: int, session_id: str) -> dict:
    execute_js(
        port,
        session_id,
        """
document.getElementById('modeSelect').value = 'webaudio-synth';
document.getElementById('modeSelect').dispatchEvent(new Event('change', { bubbles: true }));
document.getElementById('playButton').click();
return true;
""",
        timeout=3,
    )
    deadline = time.monotonic() + 10
    last_state = {}
    while time.monotonic() < deadline:
        state = execute_js(port, session_id, state_script(), timeout=4)
        last_state = state
        playback = ((state.get("marker") or {}).get("lastPlayback") or {})
        if (
            playback.get("status") == "scheduled"
            and playback.get("actualMode") == "webaudio-synth"
            and playback.get("synthPlaybackImplemented") is True
            and playback.get("synthEngine") == "webaudio-gm-lite-synth"
            and playback.get("synthPresetBank") == "gm-lite-v2"
            and "cc64-sustain" in (playback.get("synthMidiControls") or [])
            and int(playback.get("scheduledNoteCount") or 0) > 0
            and int(playback.get("midiNoteCount") or 0) > 0
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"MIDI BGM synth playback was not scheduled: {last_state!r}")


def verify_spessasynth_bundle_import(port: int, session_id: str) -> dict:
    state = execute_async_js(
        port,
        session_id,
        """
const done = arguments[arguments.length - 1];
(async () => {
  try {
    const adapter = window.HWANSE_SPESSASYNTH_ADAPTER;
    if (!adapter) {
      done({ ok: false, error: "adapter missing" });
      return;
    }
    const mod = await import(adapter.moduleUrl);
    let processorLoaded = false;
    const AudioContextCtor = window.AudioContext || window.webkitAudioContext;
    if (AudioContextCtor) {
      const ctx = new AudioContextCtor();
      if (ctx.audioWorklet && typeof ctx.audioWorklet.addModule === "function") {
        await ctx.audioWorklet.addModule(adapter.processorUrl);
        processorLoaded = true;
      }
      if (typeof ctx.close === "function") await ctx.close();
    }
    done({
      ok: true,
      hasWorkletSynthesizer: typeof mod.WorkletSynthesizer === "function",
      hasSequencer: typeof mod.Sequencer === "function",
      processorLoaded,
      exportCount: Object.keys(mod).length,
      libraryVersion: adapter.libraryVersion,
    });
  } catch (error) {
    done({ ok: false, error: String(error && error.message ? error.message : error) });
  }
})();
""",
        timeout=20,
    )
    if not state.get("ok") or not state.get("hasWorkletSynthesizer"):
        raise WebDriverError(f"SpessaSynth bundle import failed: {state!r}")
    if not state.get("processorLoaded"):
        raise WebDriverError(f"SpessaSynth processor addModule failed: {state!r}")
    return state


def verify_profile_cache_storage(port: int, session_id: str) -> dict:
    state = execute_async_js(
        port,
        session_id,
        """
const done = arguments[arguments.length - 1];
(async () => {
  try {
    const storage = window.HWANSE_MIDI_SOUNDFONT_STORAGE;
    if (!storage) {
      done({ ok: false, error: "storage helper missing" });
      return;
    }
    const profile = {
      id: "verify-cache",
      label: "Verify Cache",
      family: "GM",
      expectedPath: "soundfonts/verify-cache.sf2",
    };
    const bytes = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]);
    await storage.saveProfileSoundfont(profile, bytes.buffer);
    const loaded = await storage.loadProfileSoundfont(profile.id);
    await storage.deleteProfileSoundfont(profile.id);
    done({
      ok: true,
      profileId: loaded && loaded.profileId,
      expectedPath: loaded && loaded.expectedPath,
      size: loaded && loaded.size,
      dataLength: loaded && loaded.data ? loaded.data.byteLength : 0,
      source: loaded && loaded.source,
    });
  } catch (error) {
    done({ ok: false, error: String(error && error.message ? error.message : error) });
  }
})();
""",
        timeout=20,
    )
    if not state.get("ok") or state.get("profileId") != "verify-cache" or state.get("dataLength") != 8:
        raise WebDriverError(f"bundled profile soundfont cache verification failed: {state!r}")
    return state


def verify_browser(base: str, keep_log: bool = False) -> dict:
    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 = OUT / "midi_bgm_test_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)
            response = request_json(
                port,
                "POST",
                "/session",
                {"capabilities": {"alwaysMatch": {"browserName": "MiniBrowser"}}},
                timeout=60,
            )
            value = (response or {}).get("value") or {}
            session_id = value.get("sessionId") or ""
            if not session_id:
                raise WebDriverError(f"could not create WebDriver session: {response!r}")
            url = urljoin(base, "/web/midi_bgm_test.html")
            request_json(port, "POST", f"/session/{session_id}/url", {"url": url}, timeout=30)
            ready = wait_for_ready(port, session_id)
            require("Classic GS: GeneralUser GS" in ready.get("text", ""), "profile text should include GeneralUser GS")
            require("Standard GM: FluidR3Mono GM" in ready.get("text", ""), "profile text should include FluidR3Mono")
            require("Clean GM: MuseScore General Small" in ready.get("text", ""), "profile text should include MuseScore small")
            require("내장 사운드폰트 캐시 삭제" in ready.get("text", ""), "page should expose bundled soundfont cache cleanup")
            require("SpessaSynth SF2/SF3 adapter" in ready.get("text", ""), "page should expose SpessaSynth SF2/SF3 adapter")
            require("IndexedDB" in ready.get("text", ""), "page should expose IndexedDB upload storage")
            require("Web MIDI API Output" in ready.get("text", ""), "page should expose Web MIDI API output mode")
            require("Web MIDI 권한/출력 요청" in ready.get("text", ""), "page should expose explicit Web MIDI permission request")
            require("Web MIDI 권한" in ready.get("text", ""), "page should expose Web MIDI permission state")
            require("반영 MIDI 컨트롤" in ready.get("text", ""), "page should expose visible GM Lite MIDI controls")
            require("cc64-sustain" in ready.get("text", ""), "page should list implemented synth MIDI controls")
            profile_cache = verify_profile_cache_storage(port, session_id)
            require(profile_cache.get("source") == "bundled-profile-cache", "profile cache should store bundled profile records")
            bundle = verify_spessasynth_bundle_import(port, session_id)
            require(bundle.get("hasSequencer") is True, "SpessaSynth bundle should expose Sequencer")
            played = play_first_track(port, session_id)
            marker = played.get("marker") or {}
            require(marker.get("synthPresetBank") == "gm-lite-v2", "page should expose GM Lite preset bank")
            require(marker.get("synthMidiControlsImplemented") is True, "page should expose synth MIDI control support")
            playback = ((played.get("marker") or {}).get("lastPlayback") or {})
            require(playback.get("exeId") == "00", "default selected track should be EXE id 00")
            require(playback.get("existingMidiSoundfontHelperReused") is None, "playback result should come from new BGM player")
            execute_js(port, session_id, "window.HWANSE_MIDI_BGM_PLAYER.stop('verify-stop'); return true;", timeout=3)
            return played
        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()
            if not keep_log:
                try:
                    log_path.unlink()
                except FileNotFoundError:
                    pass


def write_outputs(state: dict) -> None:
    marker = state.get("marker") or {}
    output = {
        "status": "passed",
        "source": "web/midi_bgm_test.html",
        "marker": marker,
        "summary": (
            f"mode={marker.get('selectedMode')} tracks={marker.get('midiTrackCount')} "
            f"profiles={marker.get('soundfontProfileCount')} synth={marker.get('synthPresetBank')} "
            f"webMidi={((marker.get('webMidiSupport') or {}).get('status'))}"
        ),
    }
    OUT.mkdir(parents=True, exist_ok=True)
    (OUT / "midi_bgm_test_browser_smoke.json").write_text(
        json.dumps(output, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
        encoding="utf-8",
    )


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--base", default="http://127.0.0.1:8013")
    parser.add_argument("--keep-log", action="store_true")
    args = parser.parse_args()
    state = verify_browser(args.base.rstrip("/") + "/", keep_log=args.keep_log)
    write_outputs(state)
    print(json.dumps(state.get("marker") or {}, ensure_ascii=False, indent=2, sort_keys=True))
    print("midi bgm test browser verification ok")


if __name__ == "__main__":
    main()
