#!/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_map(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)
    wait_for_map_runtime(port, session_id, params["map"])
    return url


def start_transition_progress_script() -> str:
    return """
window.__hwanseCandidateTransitionProgress = null;
localStorage.removeItem(RUNTIME_SAVE_KEY);
window.HWANSE_LAST_MAP_TRANSITION_AUTO_SAVE = null;
window.HWANSE_LAST_TRANSITION_EFFECT = null;
if (typeof activeTransitionEffect !== 'undefined') activeTransitionEffect = null;
ensureSceneEvents()
  .then(() => {
    selectedTransitionTarget = 'map1_01a';
    pendingTransitionTarget = 'map1_01a';
    transitionTargetExplicit = true;
    const tile = footTile();
    const before = {
      scene,
      map: map?.name || '',
      tile,
      targets: transitionTargetsAtFoot(),
      review: transitionReviewAt(tile.x, tile.y, 'map1_01a'),
    };
    render();
    const promptBefore = window.HWANSE_LAST_ACTION_PROMPT || null;
    const promptRect = actionPromptRect();
    const canvas = document.getElementById('screen');
    const canvasRect = canvas.getBoundingClientRect();
    const handled = Boolean(promptRect);
    if (promptRect) {
      canvas.dispatchEvent(new PointerEvent('pointerdown', {
        bubbles: true,
        cancelable: true,
        pointerId: 71,
        pointerType: 'mouse',
        isPrimary: true,
        clientX: canvasRect.left + (promptRect.x + promptRect.width / 2) / (canvas.width / canvasRect.width),
        clientY: canvasRect.top + (promptRect.y + promptRect.height / 2) / (canvas.height / canvasRect.height),
      }));
    }
    const deadline = performance.now() + 6000;
    function finishWhenReady() {
      const autoSave = window.HWANSE_LAST_MAP_TRANSITION_AUTO_SAVE || null;
      if (map?.name === 'map1_01a' && autoSave?.saved === true) {
        render();
        const raw = localStorage.getItem(RUNTIME_SAVE_KEY) || '{}';
        const payload = JSON.parse(raw);
        startPrototypeProgressReview();
        window.__hwanseCandidateTransitionProgress = {
          handled,
          saved: autoSave?.saved === true,
          autoSave,
          before,
          promptBefore,
          promptRect,
          canvasPointer: true,
          scene,
          map: map?.name || '',
          tile: footTile(),
          search: window.location.search,
          activeId: activeDialogue?.block?.blockId || '',
          activeLines: activeDialogue?.lines?.slice(0, 12) || [],
          progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
          progressRecord: window.HWANSE_LAST_PROGRESS_RECORD || null,
          confirmedEvent: window.HWANSE_LAST_CONFIRMED_EVENT_TRANSITION || null,
          transitionEffect: window.HWANSE_LAST_TRANSITION_EFFECT || null,
          savedProgress: payload.prototypeProgress || null,
          payloadMap: payload.map || '',
          payloadTile: payload.tile || null,
          trialTransitions,
          routeAssistActive: trialTransitions === 'routeAssist',
          labels: menuItems().map((item) => menuItemLabel(item)),
          routePathLabels: [...routePathSelect.options].map((option) => option.textContent || ''),
          routeNextText: routeNextButton.textContent || '',
          routeNextTitle: routeNextButton.title || '',
        };
        return;
      }
      if (performance.now() >= deadline) {
        window.__hwanseCandidateTransitionProgress = {
          error: `confirmed transition timed out at ${map?.name || ''}`,
          handled,
          before,
          map: map?.name || '',
          progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
          progressRecord: window.HWANSE_LAST_PROGRESS_RECORD || null,
        };
        return;
      }
      requestAnimationFrame(finishWhenReady);
    }
    finishWhenReady();
  })
  .catch((error) => {
    window.__hwanseCandidateTransitionProgress = { error: String(error && error.message || error) };
  });
return true;
"""


def restore_transition_progress_script() -> str:
    return """
window.__hwanseCandidateTransitionProgressRestore = null;
quickLoadRuntime()
  .then((loaded) => {
    startPrototypeProgressReview();
    const restored = {
      loaded,
      scene,
      map: map?.name || '',
      tile: footTile(),
      search: window.location.search,
      activeId: activeDialogue?.block?.blockId || '',
      activeLines: activeDialogue?.lines?.slice(0, 12) || [],
      progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
      labels: menuItems().map((item) => menuItemLabel(item)),
      routePathLabels: [...routePathSelect.options].map((option) => option.textContent || ''),
      routeNextText: routeNextButton.textContent || '',
      routeNextTitle: routeNextButton.title || '',
      trialTransitions,
      routeAssistActive: trialTransitions === 'routeAssist',
    };
    return setMap('map1_02b', { spawnTile: { x: 11, y: 12 } }).then(() => {
      selectedTransitionTarget = 'map1_01a';
      pendingTransitionTarget = 'map1_01a';
      transitionTargetExplicit = true;
      render();
      restored.sourcePrompt = window.HWANSE_LAST_ACTION_PROMPT || null;
      restored.sourceMapAfterPrompt = map?.name || '';
      window.__hwanseCandidateTransitionProgressRestore = restored;
    });
  })
  .catch((error) => {
    window.__hwanseCandidateTransitionProgressRestore = { error: String(error && error.message || error) };
  });
return true;
"""


def title_state_script() -> str:
    return """
const titleMenu = typeof titleMenuItems === 'function' ? titleMenuItems() : [];
return {
  readyState: document.readyState,
  href: location.href,
  search: location.search,
  hasScreen: !!document.getElementById('screen'),
  scene: typeof scene === 'undefined' ? '' : scene,
  mapName: typeof map === 'undefined' || !map ? '' : map.name,
  titleLoaded: !!images?.title?.complete && (images.title.naturalWidth || 0) > 0,
  quickLoadHidden: document.getElementById('quickLoadButton')?.hidden ?? null,
  quickLoadText: document.getElementById('quickLoadButton')?.textContent || '',
  titleMenuRender: window.HWANSE_LAST_TITLE_MENU_RENDER || null,
  titleMenuMode: typeof titleMenuMode === 'undefined' ? '' : titleMenuMode,
  titleMenuKeys: titleMenu.map((item) => item.key),
  titleMenuLabels: titleMenu.map((item) => item.label),
  selectedTitleMenuIndex: typeof selectedTitleMenuIndex === 'undefined' ? null : selectedTitleMenuIndex,
  bootError: window.HWANSE_BOOT_ERROR || null,
};
"""


def click_title_continue_script() -> str:
    return """
if (typeof updateRuntimeSaveControls === 'function') updateRuntimeSaveControls();
const items = typeof titleMenuItems === 'function' ? titleMenuItems() : [];
const button = document.getElementById('quickLoadButton');
if (!button || button.hidden || button.textContent !== '이어하기' || !items.some((item) => item.key === 'continue')) {
  return {
    ok: false,
    hidden: button?.hidden ?? null,
    text: button?.textContent || '',
    titleMenuKeys: items.map((item) => item.key),
    titleMenuLabels: items.map((item) => item.label),
  };
}
const index = items.findIndex((item) => item.key === 'continue');
selectedTitleMenuIndex = index;
activateSelectedTitleMenuItem();
return {
  ok: index >= 0,
  text: button.textContent,
  title: button.title,
  titleMenuKeys: items.map((item) => item.key),
  titleMenuLabels: items.map((item) => item.label),
  selectedTitleMenuIndex,
};
"""


def transition_progress_state_script() -> str:
    return "return window.__hwanseCandidateTransitionProgress || null;"


def transition_progress_restore_state_script() -> str:
    return "return window.__hwanseCandidateTransitionProgressRestore || null;"


def title_transition_progress_restore_state_script() -> str:
    return """
if (scene === 'map' && map?.name === 'map1_01a') {
  if (!activeDialogue && typeof startPrototypeProgressReview === 'function') startPrototypeProgressReview();
  if (typeof render === 'function') render();
}
const titleMenu = typeof titleMenuItems === 'function' ? titleMenuItems() : [];
return {
  readyState: document.readyState,
  href: location.href,
  search: location.search,
  hasScreen: !!document.getElementById('screen'),
  scene: typeof scene === 'undefined' ? '' : scene,
  map: typeof map === 'undefined' || !map ? '' : map.name,
  tile: typeof footTile === 'function' ? footTile() : null,
  activeId: typeof activeDialogue === 'undefined' || !activeDialogue ? '' : activeDialogue.block.blockId,
  activeLines: typeof activeDialogue === 'undefined' || !activeDialogue ? [] : activeDialogue.lines.slice(0, 12),
  progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
  labels: typeof menuItems === 'function' ? menuItems().map((item) => menuItemLabel(item)) : [],
  quickLoadHidden: document.getElementById('quickLoadButton')?.hidden ?? null,
  quickLoadText: document.getElementById('quickLoadButton')?.textContent || '',
  trialTransitions: typeof trialTransitions === 'undefined' ? '' : trialTransitions,
  routeAssistActive: typeof trialTransitions !== 'undefined' && trialTransitions === 'routeAssist',
  playHudLines: window.HWANSE_LAST_PLAY_HUD_LINES || [],
  titleMenuKeys: titleMenu.map((item) => item.key),
  titleMenuLabels: titleMenu.map((item) => item.label),
  bootError: window.HWANSE_BOOT_ERROR || null,
};
"""


def transition_completion_objective_capture_script() -> str:
    return """
window.HWANSE_LAST_OBJECTIVE_ACTION = null;
window.HWANSE_LAST_MAP_TRANSITION_COMPLETION_NOTICE = null;
window.HWANSE_MAP_TRANSITION_COMPLETION_FEEDBACK_LOG = [];
window.HWANSE_MAP_TRANSITION_COMPLETION_FEEDBACK_RENDER = [];
window.HWANSE_LAST_MAP_TRANSITION_COMPLETION_FEEDBACK = null;
window.HWANSE_LAST_MAP_TRANSITION_COMPLETION_FEEDBACK_RENDER = null;
if (typeof activeMapTransitionCompletionFeedbacks !== 'undefined') activeMapTransitionCompletionFeedbacks = [];
if (typeof activeDialogue !== 'undefined') activeDialogue = null;
if (typeof activeDialogueLine !== 'undefined') activeDialogueLine = 0;
if (typeof menuOpen !== 'undefined') menuOpen = false;
if (typeof render === 'function') render();
const objectiveBefore = typeof prototypeObjectiveState === 'function' ? prototypeObjectiveState() : null;
const completionBefore = typeof mapTransitionPrototypeCompletionState === 'function'
  ? mapTransitionPrototypeCompletionState()
  : null;
const handled = typeof activatePrototypeObjectiveAction === 'function'
  ? activatePrototypeObjectiveAction()
  : false;
if (typeof render === 'function') render();
return {
  scene: typeof scene === 'undefined' ? '' : scene,
  map: typeof map === 'undefined' || !map ? '' : map.name,
  tile: typeof footTile === 'function' ? footTile() : null,
  objectiveBefore,
  completionBefore,
  handled,
  objectiveAction: window.HWANSE_LAST_OBJECTIVE_ACTION || null,
  objectiveCompletionNotice: window.HWANSE_LAST_MAP_TRANSITION_COMPLETION_NOTICE || null,
  mapTransitionCompletionFeedbackLog: window.HWANSE_MAP_TRANSITION_COMPLETION_FEEDBACK_LOG || [],
  mapTransitionCompletionFeedbackRender: window.HWANSE_MAP_TRANSITION_COMPLETION_FEEDBACK_RENDER || [],
  mapTransitionCompletionFeedbackLast: window.HWANSE_LAST_MAP_TRANSITION_COMPLETION_FEEDBACK || null,
  mapTransitionCompletionFeedbackLastRender: window.HWANSE_LAST_MAP_TRANSITION_COMPLETION_FEEDBACK_RENDER || null,
  activeId: typeof activeDialogue === 'undefined' || !activeDialogue ? '' : activeDialogue.block.blockId,
  activeLines: typeof activeDialogue === 'undefined' || !activeDialogue ? [] : activeDialogue.lines.slice(0, 12),
  progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
  completion: window.HWANSE_LAST_PROTOTYPE_COMPLETION || null,
  trialTransitions: typeof trialTransitions === 'undefined' ? '' : trialTransitions,
  routeAssistActive: typeof trialTransitions !== 'undefined' && trialTransitions === 'routeAssist',
};
"""


def start_transition_movement_progress_script() -> str:
    return """
window.__hwanseCandidateTransitionMovementProgress = null;
localStorage.removeItem(RUNTIME_SAVE_KEY);
window.HWANSE_LAST_MAP_TRANSITION_AUTO_SAVE = null;
window.HWANSE_LAST_TRANSITION_EFFECT = null;
if (typeof activeTransitionEffect !== 'undefined') activeTransitionEffect = null;
ensureSceneEvents()
  .then(() => {
    selectedTransitionTarget = 'map1_01a';
    pendingTransitionTarget = 'map1_01a';
    transitionTargetExplicit = true;
    const tile = footTile();
    const component = transitionComponentAtFoot('map1_01a');
    const direction = transitionAttemptDirectionForRow(component?.row, tile) || [0, -1];
    const before = {
      scene,
      map: map?.name || '',
      tile,
      targets: transitionTargetsAtFoot(),
      review: transitionReviewAt(tile.x, tile.y, 'map1_01a'),
      direction,
    };
    render();
    const promptBefore = window.HWANSE_LAST_ACTION_PROMPT || null;
    const handled = checkConfirmedTransitionAttempt(direction[0], direction[1]);
    const deadline = performance.now() + 6000;
    function finishWhenReady() {
      const autoSave = window.HWANSE_LAST_MAP_TRANSITION_AUTO_SAVE || null;
      if (map?.name === 'map1_01a' && autoSave?.saved === true) {
        render();
        const raw = localStorage.getItem(RUNTIME_SAVE_KEY) || '{}';
        const payload = JSON.parse(raw);
        startPrototypeProgressReview();
        window.__hwanseCandidateTransitionMovementProgress = {
          handled,
          saved: autoSave?.saved === true,
          autoSave,
          before,
          promptBefore,
          scene,
          map: map?.name || '',
          tile: footTile(),
          search: window.location.search,
          activeId: activeDialogue?.block?.blockId || '',
          activeLines: activeDialogue?.lines?.slice(0, 12) || [],
          progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
          progressRecord: window.HWANSE_LAST_PROGRESS_RECORD || null,
          confirmedEvent: window.HWANSE_LAST_CONFIRMED_EVENT_TRANSITION || null,
          transitionEffect: window.HWANSE_LAST_TRANSITION_EFFECT || null,
          savedProgress: payload.prototypeProgress || null,
          payloadMap: payload.map || '',
          payloadTile: payload.tile || null,
          trialTransitions,
          routeAssistActive: trialTransitions === 'routeAssist',
          labels: menuItems().map((item) => menuItemLabel(item)),
          routePathLabels: [...routePathSelect.options].map((option) => option.textContent || ''),
          routeNextText: routeNextButton.textContent || '',
          routeNextTitle: routeNextButton.title || '',
        };
        return;
      }
      if (performance.now() >= deadline) {
        window.__hwanseCandidateTransitionMovementProgress = {
          error: `confirmed movement transition timed out at ${map?.name || ''}`,
          handled,
          before,
          map: map?.name || '',
          progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
          progressRecord: window.HWANSE_LAST_PROGRESS_RECORD || null,
        };
        return;
      }
      requestAnimationFrame(finishWhenReady);
    }
    finishWhenReady();
  })
  .catch((error) => {
    window.__hwanseCandidateTransitionMovementProgress = { error: String(error && error.message || error) };
  });
return true;
"""


def transition_movement_progress_state_script() -> str:
    return "return window.__hwanseCandidateTransitionMovementProgress || null;"


def wait_for_transition_progress_state(port: int, session_id: str, timeout: float = 8) -> dict:
    deadline = time.monotonic() + timeout
    state: dict = {}
    while time.monotonic() < deadline:
        value = execute_js(port, session_id, transition_progress_state_script(), timeout=3)
        state = value if isinstance(value, dict) else {}
        progress = state.get("progress") or {}
        counts = progress.get("counts") or {}
        if state and state.get("error") is None and state.get("map") == "map1_01a" and counts.get("map-transition") == 1:
            return state
        time.sleep(0.2)
    raise WebDriverError(f"candidate transition progress did not become ready: {state!r}")


def wait_for_transition_movement_state(port: int, session_id: str, timeout: float = 8) -> dict:
    deadline = time.monotonic() + timeout
    state: dict = {}
    while time.monotonic() < deadline:
        value = execute_js(port, session_id, transition_movement_progress_state_script(), timeout=3)
        state = value if isinstance(value, dict) else {}
        progress = state.get("progress") or {}
        counts = progress.get("counts") or {}
        if state and state.get("error") is None and state.get("map") == "map1_01a" and counts.get("map-transition") == 1:
            return state
        time.sleep(0.2)
    raise WebDriverError(f"candidate transition movement progress did not become ready: {state!r}")


def wait_for_transition_restore_state(port: int, session_id: str, timeout: float = 8) -> dict:
    deadline = time.monotonic() + timeout
    state: dict = {}
    while time.monotonic() < deadline:
        value = execute_js(port, session_id, transition_progress_restore_state_script(), timeout=3)
        state = value if isinstance(value, dict) else {}
        progress = state.get("progress") or {}
        counts = progress.get("counts") or {}
        if (
            state
            and state.get("error") is None
            and state.get("loaded") is True
            and state.get("map") == "map1_01a"
            and state.get("activeId") == "prototype-progress:map1_01a"
            and counts.get("map-transition") == 1
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"candidate transition progress restore did not become ready: {state!r}")


def wait_for_transition_title_continue_ready(port: int, session_id: str, timeout: float = 10) -> dict:
    deadline = time.monotonic() + timeout
    state: dict = {}
    while time.monotonic() < deadline:
        value = execute_js(port, session_id, title_state_script(), timeout=3)
        state = value if isinstance(value, dict) else {}
        labels = [str(label) for label in (state.get("titleMenuLabels") or [])]
        if (
            state.get("readyState") == "complete"
            and state.get("hasScreen") is True
            and state.get("scene") == "title"
            and state.get("titleLoaded") is True
            and state.get("bootError") is None
            and state.get("quickLoadHidden") is False
            and state.get("quickLoadText") == "이어하기"
            and "continue" in (state.get("titleMenuKeys") or [])
            and any("이어하기 map1_01a" in label for label in labels)
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"candidate transition title continue did not become ready: {state!r}")


def wait_for_title_transition_restore_state(port: int, session_id: str, timeout: float = 10) -> dict:
    deadline = time.monotonic() + timeout
    state: dict = {}
    while time.monotonic() < deadline:
        value = execute_js(port, session_id, title_transition_progress_restore_state_script(), timeout=3)
        state = value if isinstance(value, dict) else {}
        progress = state.get("progress") or {}
        counts = progress.get("counts") or {}
        if (
            state
            and state.get("readyState") == "complete"
            and state.get("hasScreen") is True
            and state.get("bootError") is None
            and state.get("scene") == "map"
            and state.get("map") == "map1_01a"
            and state.get("activeId") == "prototype-progress:map1_01a"
            and counts.get("map-transition") == 1
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"candidate transition title continue restore did not become ready: {state!r}")


def verify_map_transition_auto_save(auto_save: dict, expected_trigger: str) -> None:
    progress = auto_save.get("progress") or {}
    counts = progress.get("counts") or {}
    progress_event = auto_save.get("progressEvent") or {}
    detail = progress_event.get("detail") or {}
    expected_activation_tile = {"x": 11, "y": 12}
    expected_payload_tile = {"x": 11, "y": 11}
    if (
        auto_save.get("saved") is not True
        or auto_save.get("source") != "map-transition"
        or auto_save.get("map") != "map1_01a"
        or auto_save.get("payloadMap") != "map1_01a"
        or (auto_save.get("tile") or {}) != expected_activation_tile
        or (auto_save.get("payloadTile") or {}) != expected_payload_tile
        or auto_save.get("id") != "map1_02b->map1_01a"
        or auto_save.get("sourceMap") != "map1_02b"
        or auto_save.get("targetMap") != "map1_01a"
        or counts.get("map-transition") != 1
        or progress_event.get("kind") != "map-transition"
        or progress_event.get("id") != "map1_02b->map1_01a"
        or progress_event.get("map") != "map1_02b"
        or detail.get("trigger") != expected_trigger
        or detail.get("transitionKind") != "event-transition"
        or detail.get("transitionReviewState") != "confirmed"
        or detail.get("originalSceneRecordBacked") is not True
        or detail.get("originalStoryFlagRuntimeImplemented") is not False
        or auto_save.get("originalStoryFlagRuntimeImplemented") is not False
    ):
        raise WebDriverError(f"candidate transition auto-save is incomplete: {auto_save!r}")


def verify_transition_effect(effect: dict, expected_trigger: str) -> None:
    tile = effect.get("tile") or {}
    if (
        effect.get("sourceMap") != "map1_02b"
        or effect.get("targetMap") != "map1_01a"
        or effect.get("mapBefore") != "map1_02b"
        or effect.get("mapAfter") != "map1_01a"
        or effect.get("source") != "map-transition"
        or effect.get("trigger") != expected_trigger
        or effect.get("transitionKind") != "event-transition"
        or effect.get("transitionReviewState") != "confirmed"
        or effect.get("arrived") is not True
        or effect.get("changed") is not True
        or effect.get("durationMs") != 420
        or effect.get("maxAlpha") != 0.72
        or effect.get("browserFadeRuntimeImplemented") is not True
        or effect.get("originalTransitionFadeTimingImplemented") is not False
        or effect.get("originalStoryFlagRuntimeImplemented") is not False
        or tile.get("x") != 11
        or tile.get("y") != 12
    ):
        raise WebDriverError(f"candidate transition fade effect is incomplete: {effect!r}")


def transition_effect_summary(state: dict) -> str:
    effect = state.get("transitionEffect") or {}
    return (
        f"transitionEffect={effect.get('sourceMap')}->{effect.get('targetMap')} "
        f"effectTrigger={effect.get('trigger')} "
        f"effectKind={effect.get('transitionKind')} "
        f"fadeMs={effect.get('durationMs')} "
        f"browserFade={effect.get('browserFadeRuntimeImplemented')} "
        f"effectArrived={effect.get('arrived')} "
        f"effectChanged={effect.get('changed')}"
    )


def transition_completion_feedback_summary(state: dict) -> str:
    feedback = state.get("mapTransitionCompletionFeedbackLast") or {}
    rendered = state.get("mapTransitionCompletionFeedbackLastRender") or {}
    return (
        f"transitionCompletionFeedback={feedback.get('source')}:{feedback.get('text')} "
        f"transitionCompletionFeedbackRender={rendered.get('active')} "
        f"transitionCompletionSound={feedback.get('mapTransitionCompletionSound')}:"
        f"{feedback.get('mapTransitionCompletionSoundSrc')}:{feedback.get('mapTransitionCompletionSoundPlayed')}"
    )


def verify_transition_progress_state(state: dict, restored: bool = False) -> None:
    progress = state.get("progress") or {}
    counts = progress.get("counts") or {}
    labels = state.get("labels") or []
    lines = "\n".join(state.get("activeLines") or [])
    if (
        state.get("scene") != "map"
        or state.get("map") != "map1_01a"
        or state.get("activeId") != "prototype-progress:map1_01a"
        or progress.get("total") != 1
        or counts.get("map-transition") != 1
        or progress.get("originalStoryFlagRuntimeImplemented") is not False
        or ("진행 기록 1" not in labels and "진행 목표 1" not in labels)
        or "map1_02b->map1_01a" not in lines
        or "맵 전환" not in lines
        or "map=map1_01a" not in str(state.get("search") or "")
        or state.get("trialTransitions") not in ("", None)
        or state.get("routeAssistActive") is not False
    ):
        raise WebDriverError(f"candidate transition progress state is incomplete: {state!r}")
    if restored:
        if state.get("loaded") is not True:
            raise WebDriverError(f"candidate transition progress restore did not load: {state!r}")
        source_prompt = state.get("sourcePrompt") or {}
        source_prompt_text = str(source_prompt.get("text") or "")
        if (
            state.get("sourceMapAfterPrompt") != "map1_02b"
            or source_prompt.get("kind") != "map-transition"
            or "Move/Enter -> 전환 완료 map1_01a" not in source_prompt_text
            or "confirmed" not in source_prompt_text
            or source_prompt.get("completed") is not True
            or source_prompt.get("progressKind") != "map-transition"
            or source_prompt.get("targetMap") != "map1_01a"
        ):
            raise WebDriverError(f"candidate transition progress restore source prompt is incomplete: {state!r}")
        return

    record = state.get("progressRecord") or {}
    detail = record.get("detail") or {}
    confirmed_event = state.get("confirmedEvent") or {}
    saved_progress = state.get("savedProgress") or {}
    saved_counts = saved_progress.get("counts") or {}
    before = state.get("before") or {}
    prompt_before = state.get("promptBefore") or {}
    prompt_before_text = str(prompt_before.get("text") or "")
    verify_map_transition_auto_save(state.get("autoSave") or {}, "action")
    verify_transition_effect(state.get("transitionEffect") or {}, "action")
    if (
        state.get("handled") is not True
        or state.get("saved") is not True
        or state.get("canvasPointer") is not True
        or (state.get("promptRect") or {}).get("width", 0) <= 0
        or (state.get("promptRect") or {}).get("height", 0) <= 0
        or before.get("map") != "map1_02b"
        or "map1_01a" not in (before.get("targets") or [])
        or before.get("review") != "confirmed"
        or prompt_before.get("kind") != "map-transition"
        or "Move/Enter -> map1_01a" not in prompt_before_text
        or "confirmed" not in prompt_before_text
        or prompt_before.get("completed") is not False
        or prompt_before.get("progressKind") != "map-transition"
        or prompt_before.get("targetMap") != "map1_01a"
        or record.get("kind") != "map-transition"
        or record.get("id") != "map1_02b->map1_01a"
        or record.get("map") != "map1_02b"
        or record.get("originalStoryFlagRuntimeImplemented") is not False
        or detail.get("sourceMap") != "map1_02b"
        or detail.get("targetMap") != "map1_01a"
        or detail.get("trigger") != "action"
        or detail.get("transitionKind") != "event-transition"
        or detail.get("transitionReviewState") != "confirmed"
        or detail.get("originalSceneRecordBacked") is not True
        or detail.get("originalStoryFlagRuntimeImplemented") is not False
        or confirmed_event.get("sourceMap") != "map1_02b"
        or confirmed_event.get("targetMap") != "map1_01a"
        or confirmed_event.get("trigger") != "action"
        or confirmed_event.get("transitionKind") != "event-transition"
        or confirmed_event.get("transitionReviewState") != "confirmed"
        or confirmed_event.get("originalSceneRecordBacked") is not True
        or confirmed_event.get("originalStoryFlagRuntimeImplemented") is not False
        or saved_counts.get("map-transition") != 1
        or state.get("payloadMap") != "map1_01a"
    ):
        raise WebDriverError(f"candidate transition progress record is incomplete: {state!r}")


def verify_transition_movement_state(state: dict) -> None:
    progress = state.get("progress") or {}
    counts = progress.get("counts") or {}
    lines = "\n".join(state.get("activeLines") or [])
    record = state.get("progressRecord") or {}
    detail = record.get("detail") or {}
    confirmed_event = state.get("confirmedEvent") or {}
    saved_progress = state.get("savedProgress") or {}
    saved_counts = saved_progress.get("counts") or {}
    before = state.get("before") or {}
    direction = before.get("direction") or []
    prompt_before = state.get("promptBefore") or {}
    prompt_before_text = str(prompt_before.get("text") or "")
    verify_map_transition_auto_save(state.get("autoSave") or {}, "movement")
    verify_transition_effect(state.get("transitionEffect") or {}, "movement")
    if (
        state.get("scene") != "map"
        or state.get("map") != "map1_01a"
        or progress.get("total") != 1
        or counts.get("map-transition") != 1
        or "map1_02b->map1_01a" not in lines
        or "맵 전환" not in lines
        or state.get("handled") is not True
        or state.get("saved") is not True
        or before.get("map") != "map1_02b"
        or before.get("review") != "confirmed"
        or len(direction) != 2
        or prompt_before.get("kind") != "map-transition"
        or "Move/Enter -> map1_01a" not in prompt_before_text
        or "confirmed" not in prompt_before_text
        or record.get("kind") != "map-transition"
        or record.get("id") != "map1_02b->map1_01a"
        or record.get("map") != "map1_02b"
        or detail.get("sourceMap") != "map1_02b"
        or detail.get("targetMap") != "map1_01a"
        or detail.get("trigger") != "movement"
        or detail.get("transitionKind") != "event-transition"
        or detail.get("transitionReviewState") != "confirmed"
        or detail.get("originalSceneRecordBacked") is not True
        or detail.get("originalStoryFlagRuntimeImplemented") is not False
        or confirmed_event.get("sourceMap") != "map1_02b"
        or confirmed_event.get("targetMap") != "map1_01a"
        or confirmed_event.get("trigger") != "movement"
        or confirmed_event.get("transitionKind") != "event-transition"
        or confirmed_event.get("transitionReviewState") != "confirmed"
        or confirmed_event.get("originalSceneRecordBacked") is not True
        or confirmed_event.get("originalStoryFlagRuntimeImplemented") is not False
        or state.get("trialTransitions") not in ("", None)
        or state.get("routeAssistActive") is not False
        or saved_counts.get("map-transition") != 1
        or state.get("payloadMap") != "map1_01a"
    ):
        raise WebDriverError(f"candidate transition movement state is incomplete: {state!r}")


def verify_title_transition_continue_state(title_state: dict, click_state: dict, restored_state: dict) -> None:
    title_labels = [str(label) for label in (title_state.get("titleMenuLabels") or [])]
    click_labels = [str(label) for label in (click_state.get("titleMenuLabels") or [])]
    progress = restored_state.get("progress") or {}
    counts = progress.get("counts") or {}
    lines = "\n".join(restored_state.get("activeLines") or [])
    labels = restored_state.get("labels") or []
    if (
        title_state.get("scene") != "title"
        or title_state.get("quickLoadHidden") is not False
        or title_state.get("quickLoadText") != "이어하기"
        or "continue" not in (title_state.get("titleMenuKeys") or [])
        or not any("이어하기 map1_01a" in label for label in title_labels)
        or not isinstance(click_state, dict)
        or click_state.get("ok") is not True
        or click_state.get("text") != "이어하기"
        or "continue" not in (click_state.get("titleMenuKeys") or [])
        or not any("이어하기 map1_01a" in label for label in click_labels)
        or restored_state.get("scene") != "map"
        or restored_state.get("map") != "map1_01a"
        or restored_state.get("activeId") != "prototype-progress:map1_01a"
        or progress.get("total") != 1
        or counts.get("map-transition") != 1
        or progress.get("originalStoryFlagRuntimeImplemented") is not False
        or ("진행 기록 1" not in labels and "진행 목표 1" not in labels)
        or "map1_02b->map1_01a" not in lines
        or "맵 전환" not in lines
        or "map=map1_01a" not in str(restored_state.get("search") or "")
        or restored_state.get("quickLoadHidden") is not False
        or restored_state.get("quickLoadText") != "임시 불러오기"
        or restored_state.get("trialTransitions") not in ("", None)
        or restored_state.get("routeAssistActive") is not False
    ):
        raise WebDriverError(
            "candidate transition title continue did not restore progress: "
            f"title={title_state!r} click={click_state!r} restored={restored_state!r}"
        )


def verify_transition_completion_objective(state: dict) -> None:
    objective_before = state.get("objectiveBefore") or {}
    completion_before = state.get("completionBefore") or {}
    objective_action = state.get("objectiveAction") or {}
    action_objective = objective_action.get("objective") or {}
    notice = state.get("objectiveCompletionNotice") or {}
    notice_completion = notice.get("completion") or {}
    notice_feedback = notice.get("feedback") or {}
    feedback_log = state.get("mapTransitionCompletionFeedbackLog") or []
    feedback_render_log = state.get("mapTransitionCompletionFeedbackRender") or []
    feedback = state.get("mapTransitionCompletionFeedbackLast") or {}
    feedback_render = state.get("mapTransitionCompletionFeedbackLastRender") or {}
    published_completion = (state.get("completion") or {}).get("mapTransition") or {}
    lines = "\n".join(state.get("activeLines") or [])
    notice_lines = "\n".join(notice.get("lines") or [])
    if (
        state.get("scene") != "map"
        or state.get("map") != "map1_01a"
        or state.get("handled") is not True
        or state.get("activeId") != "transition-complete:map1_01a"
        or objective_before.get("title") != "후보 전환 완료 map1_01a"
        or objective_before.get("nextAction") != "완료 알림 확인"
        or objective_before.get("detail") != "map1_02b -> map1_01a"
        or objective_before.get("source") != "prototype-map-transition-completion"
        or action_objective.get("title") != "후보 전환 완료 map1_01a"
        or objective_action.get("action") != "map-transition-completion-notice"
        or objective_action.get("handled") is not True
        or objective_action.get("activeId") != "transition-complete:map1_01a"
        or objective_action.get("originalSceneRecordBacked") is not True
        or objective_action.get("originalRoutePromotionImplemented") is not False
        or objective_action.get("originalStoryFlagRuntimeImplemented") is not False
        or completion_before.get("completed") is not True
        or completion_before.get("id") != "map1_02b->map1_01a"
        or completion_before.get("sourceMap") != "map1_02b"
        or completion_before.get("targetMap") != "map1_01a"
        or completion_before.get("completedCount") != 1
        or completion_before.get("count") != 1
        or completion_before.get("transitionReviewState") != "confirmed"
        or completion_before.get("source") != "prototype-map-transition"
        or completion_before.get("originalSceneRecordBacked") is not True
        or completion_before.get("originalRoutePromotionImplemented") is not False
        or completion_before.get("originalStoryFlagRuntimeImplemented") is not False
        or notice.get("blockId") != "transition-complete:map1_01a"
        or notice.get("map") != "map1_01a"
        or notice.get("originalSceneRecordBacked") is not True
        or notice.get("originalRoutePromotionImplemented") is not False
        or notice.get("originalStoryFlagRuntimeImplemented") is not False
        or notice_completion.get("id") != "map1_02b->map1_01a"
        or notice_completion.get("completed") is not True
        or notice_feedback.get("source") != "map-transition-completion-feedback"
        or notice_feedback.get("text") != "맵 전환 완료 map1_01a"
        or len(feedback_log) < 1
        or len(feedback_render_log) < 1
        or feedback.get("source") != "map-transition-completion-feedback"
        or feedback.get("text") != "맵 전환 완료 map1_01a"
        or feedback.get("map") != "map1_01a"
        or feedback.get("blockId") != "transition-complete:map1_01a"
        or feedback.get("sourceMap") != "map1_02b"
        or feedback.get("targetMap") != "map1_01a"
        or feedback.get("transitionKind") != "event-transition"
        or feedback.get("transitionReviewState") != "confirmed"
        or feedback.get("completedCount") != 1
        or feedback.get("eventCount") != 1
        or feedback.get("mapTransitionCompletionSound") != "menuConfirm"
        or not str(feedback.get("mapTransitionCompletionSoundSrc") or "").endswith("/extract_wlk/04.wav")
        or feedback.get("mapTransitionCompletionSoundPlayed") is not True
        or feedback.get("durationMs") != 1100
        or feedback.get("browserMapTransitionCompletionFeedbackImplemented") is not True
        or feedback.get("prototypeMapTransitionCompletionImplemented") is not True
        or feedback.get("originalRoutePromotionImplemented") is not False
        or feedback.get("originalStoryFlagRuntimeImplemented") is not False
        or feedback_render.get("active") is not True
        or feedback_render.get("source") != "map-transition-completion-feedback"
        or feedback_render.get("text") != "맵 전환 완료 map1_01a"
        or feedback_render.get("map") != "map1_01a"
        or feedback_render.get("blockId") != "transition-complete:map1_01a"
        or feedback_render.get("sourceMap") != "map1_02b"
        or feedback_render.get("targetMap") != "map1_01a"
        or feedback_render.get("transitionKind") != "event-transition"
        or feedback_render.get("transitionReviewState") != "confirmed"
        or feedback_render.get("completedCount") != 1
        or feedback_render.get("eventCount") != 1
        or feedback_render.get("mapTransitionCompletionSound") != "menuConfirm"
        or not str(feedback_render.get("mapTransitionCompletionSoundSrc") or "").endswith("/extract_wlk/04.wav")
        or feedback_render.get("mapTransitionCompletionSoundPlayed") is not True
        or feedback_render.get("durationMs") != 1100
        or feedback_render.get("browserMapTransitionCompletionFeedbackImplemented") is not True
        or feedback_render.get("prototypeMapTransitionCompletionImplemented") is not True
        or feedback_render.get("originalRoutePromotionImplemented") is not False
        or feedback_render.get("originalStoryFlagRuntimeImplemented") is not False
        or published_completion.get("id") != "map1_02b->map1_01a"
        or published_completion.get("completed") is not True
        or "맵 전환 완료 1/1" not in lines
        or "map1_02b -> map1_01a" not in lines
        or "review confirmed" not in lines
        or "원본 story flag/route promotion 실행 증명은 아직 아닙니다." not in lines
        or "맵 전환 완료 1/1" not in notice_lines
        or state.get("trialTransitions") not in ("", None)
        or state.get("routeAssistActive") is not False
    ):
        raise WebDriverError(f"candidate transition completion objective is incomplete: {state!r}")


def write_report(report: dict) -> None:
    out_dir = ROOT / "out"
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "candidate_transition_progress_browser_smoke.json").write_text(
        json.dumps(report, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    lines = [
        "# Confirmed Event Transition Progress Browser Smoke",
        "",
        f"- status: `{report.get('status')}`",
        f"- confirmed transition: `{report.get('confirmedTransition')}`",
        f"- confirmed transition objective: `{report.get('confirmedTransitionObjective')}`",
        f"- restored transition: `{report.get('restoredTransition')}`",
        f"- title restored transition: `{report.get('titleRestoredTransition')}`",
        f"- title restored transition objective: `{report.get('titleRestoredTransitionObjective')}`",
        f"- movement transition: `{report.get('movementTransition')}`",
        "",
    ]


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" / "candidate_transition_progress_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,
            )

            start_url = load_map(
                base,
                port,
                session_id,
                {
                    "map": "map1_02b",
                    "startTile": "11,12",
                    "moveMs": "80",
                },
            )
            execute_js(port, session_id, start_transition_progress_script(), timeout=3)
            progress_state = wait_for_transition_progress_state(port, session_id)
            verify_transition_progress_state(progress_state)
            progress_objective_state = execute_js(port, session_id, transition_completion_objective_capture_script(), timeout=3)
            verify_transition_completion_objective(progress_objective_state)

            title_url = urljoin(base.rstrip("/") + "/", "/web/game.html?game=1")
            request_json(port, "POST", f"/session/{session_id}/url", {"url": title_url}, timeout=30)
            wait_for_page(port, session_id)
            title_continue_state = wait_for_transition_title_continue_ready(port, session_id)
            title_continue_click = execute_js(port, session_id, click_title_continue_script(), timeout=3)
            title_restore_state = wait_for_title_transition_restore_state(port, session_id)
            verify_title_transition_continue_state(title_continue_state, title_continue_click, title_restore_state)
            title_objective_state = execute_js(port, session_id, transition_completion_objective_capture_script(), timeout=3)
            verify_transition_completion_objective(title_objective_state)

            restore_url = load_map(
                base,
                port,
                session_id,
                {
                    "map": "map1_02b",
                    "startTile": "11,12",
                },
            )
            execute_js(port, session_id, restore_transition_progress_script(), timeout=3)
            restore_state = wait_for_transition_restore_state(port, session_id)
            verify_transition_progress_state(restore_state, restored=True)

            movement_url = load_map(
                base,
                port,
                session_id,
                {
                    "map": "map1_02b",
                    "startTile": "11,12",
                    "moveMs": "80",
                },
            )
            execute_js(port, session_id, start_transition_movement_progress_script(), timeout=3)
            movement_state = wait_for_transition_movement_state(port, session_id)
            verify_transition_movement_state(movement_state)

            progress_effect = transition_effect_summary(progress_state)
            movement_effect = transition_effect_summary(movement_state)
            progress_completion_feedback = transition_completion_feedback_summary(progress_objective_state)
            title_completion_feedback = transition_completion_feedback_summary(title_objective_state)
            report = {
                "status": "passed",
                "base": base,
                "startUrl": start_url,
                "titleUrl": title_url,
                "restoreUrl": restore_url,
                "movementUrl": movement_url,
                "confirmedTransition": (
                    "map1_02b->map1_01a "
                    f"map={progress_state.get('map')} "
                    f"payloadMap={progress_state.get('payloadMap')} "
                    f"mapTransitionCount={(progress_state.get('progress') or {}).get('counts', {}).get('map-transition')} "
                    f"autoSaved={(progress_state.get('autoSave') or {}).get('saved')} "
                    f"autoSource={(progress_state.get('autoSave') or {}).get('source')} "
                    f"prompt={(progress_state.get('promptBefore') or {}).get('text')} "
                    "trigger=action "
                    "canvasPointer=True "
                    "transitionReviewState=confirmed "
                    f"{progress_effect} "
                    "originalSceneRecordBacked=True "
                    "routeAssistActive=False "
                    "originalStoryFlagRuntimeImplemented=False"
                ),
                "confirmedTransitionObjective": (
                    f"objective={(progress_objective_state.get('objectiveBefore') or {}).get('title')} "
                    f"objectiveNext={(progress_objective_state.get('objectiveBefore') or {}).get('nextAction')} "
                    f"objectiveAction={(progress_objective_state.get('objectiveAction') or {}).get('action')} "
                    f"objectiveActiveId={(progress_objective_state.get('objectiveAction') or {}).get('activeId')} "
                    "route=map1_02b->map1_01a "
                    "transitionReviewState=confirmed "
                    f"{progress_completion_feedback} "
                    "originalSceneRecordBacked=True "
                    "originalRoutePromotionImplemented=False "
                    "originalStoryFlagRuntimeImplemented=False"
                ),
                "restoredTransition": (
                    "map1_02b->map1_01a "
                    f"map={restore_state.get('map')} "
                    f"progressRestored={restore_state.get('loaded')} "
                    f"mapTransitionCount={(restore_state.get('progress') or {}).get('counts', {}).get('map-transition')} "
                    "label=맵 전환 "
                    f"sourcePrompt={(restore_state.get('sourcePrompt') or {}).get('text')} "
                    "routeAssistActive=False "
                    "originalStoryFlagRuntimeImplemented=False"
                ),
                "titleRestoredTransition": (
                    "map1_02b->map1_01a "
                    "titleContinue=True "
                    f"map={title_restore_state.get('map')} "
                    "progressRestored=True "
                    f"mapTransitionCount={(title_restore_state.get('progress') or {}).get('counts', {}).get('map-transition')} "
                    f"label={next((label for label in (title_continue_state.get('titleMenuLabels') or []) if '이어하기 map1_01a' in str(label)), '')} "
                    f"quickLoadText={title_restore_state.get('quickLoadText')} "
                    "routeAssistActive=False "
                    "originalStoryFlagRuntimeImplemented=False"
                ),
                "titleRestoredTransitionObjective": (
                    f"objective={(title_objective_state.get('objectiveBefore') or {}).get('title')} "
                    f"objectiveNext={(title_objective_state.get('objectiveBefore') or {}).get('nextAction')} "
                    f"objectiveAction={(title_objective_state.get('objectiveAction') or {}).get('action')} "
                    f"objectiveActiveId={(title_objective_state.get('objectiveAction') or {}).get('activeId')} "
                    "titleContinue=True "
                    "route=map1_02b->map1_01a "
                    "transitionReviewState=confirmed "
                    f"{title_completion_feedback} "
                    "originalSceneRecordBacked=True "
                    "originalRoutePromotionImplemented=False "
                    "originalStoryFlagRuntimeImplemented=False"
                ),
                "movementTransition": (
                    "map1_02b->map1_01a "
                    f"map={movement_state.get('map')} "
                    f"payloadMap={movement_state.get('payloadMap')} "
                    f"mapTransitionCount={(movement_state.get('progress') or {}).get('counts', {}).get('map-transition')} "
                    f"autoSaved={(movement_state.get('autoSave') or {}).get('saved')} "
                    f"autoSource={(movement_state.get('autoSave') or {}).get('source')} "
                    f"prompt={(movement_state.get('promptBefore') or {}).get('text')} "
                    "trigger=movement "
                    f"direction={','.join(str(item) for item in ((movement_state.get('before') or {}).get('direction') or []))} "
                    "transitionReviewState=confirmed "
                    f"{movement_effect} "
                    "originalSceneRecordBacked=True "
                    "routeAssistActive=False "
                    "originalStoryFlagRuntimeImplemented=False"
                ),
                "snapshots": {
                    "record": progress_state,
                    "recordObjective": progress_objective_state,
                    "title": title_continue_state,
                    "titleClick": title_continue_click,
                    "titleRestore": title_restore_state,
                    "titleObjective": title_objective_state,
                    "restore": restore_state,
                    "movement": movement_state,
                },
            }
            write_report(report)
            print(
                "ok candidate transition progress browser "
                f"confirmed={report['confirmedTransition']} restored={report['restoredTransition']} "
                f"confirmedObjective={report['confirmedTransitionObjective']} "
                f"titleRestored={report['titleRestoredTransition']} "
                f"titleObjective={report['titleRestoredTransitionObjective']} "
                f"movement={report['movementTransition']}"
            )
        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()
                proc.wait(timeout=5)


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()
