#!/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 title_state_script() -> str:
    return """
const titleMenu = typeof titleMenuItems === 'function' ? titleMenuItems() : [];
return {
  readyState: document.readyState,
  href: location.href,
  search: location.search,
  scene: typeof scene === 'undefined' ? '' : scene,
  quickLoadHidden: document.getElementById('quickLoadButton')?.hidden ?? null,
  quickLoadText: document.getElementById('quickLoadButton')?.textContent || '',
  titleMenuKeys: titleMenu.map((item) => item.key),
  titleMenuLabels: titleMenu.map((item) => item.label),
};
"""


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,
  titleMenuKeys: items.map((item) => item.key),
  titleMenuLabels: items.map((item) => item.label),
  selectedTitleMenuIndex,
};
"""


def status_menu_start_script() -> str:
    return """
window.__hwanseStatusMenuStart = null;
const resetMenuReviewFeedback = () => {
  window.HWANSE_MENU_REVIEW_FEEDBACK_LOG = [];
  window.HWANSE_MENU_REVIEW_FEEDBACK_RENDER = [];
  window.HWANSE_LAST_MENU_REVIEW_FEEDBACK = null;
  window.HWANSE_LAST_MENU_REVIEW_FEEDBACK_RENDER = null;
  if (typeof activeMenuReviewFeedbacks !== 'undefined') activeMenuReviewFeedbacks = [];
};
const captureMenuReviewFeedback = () => ({
  log: Array.isArray(window.HWANSE_MENU_REVIEW_FEEDBACK_LOG)
    ? [...window.HWANSE_MENU_REVIEW_FEEDBACK_LOG]
    : [],
  render: Array.isArray(window.HWANSE_MENU_REVIEW_FEEDBACK_RENDER)
    ? [...window.HWANSE_MENU_REVIEW_FEEDBACK_RENDER]
    : [],
  last: window.HWANSE_LAST_MENU_REVIEW_FEEDBACK || null,
  lastRender: window.HWANSE_LAST_MENU_REVIEW_FEEDBACK_RENDER || null,
});
const finish = () => {
  const items = menuItems();
  const index = items.findIndex((item) => item.command === 'showStatusMenu');
  const labels = items.map((item) => menuItemLabel(item));
  if (index < 0) {
    window.__hwanseStatusMenuStart = { ok: false, reason: 'missing status menu command', labels };
    return;
  }
  selectedMenuItemIndex = index;
  resetMenuReviewFeedback();
  const commandResult = useSelectedMenuItem();
  window.setTimeout(() => {
    const activeId = activeDialogue?.block?.blockId || '';
    const lines = activeDialogue?.lines || [];
    const completion = publishPrototypeCompletionState();
    if (typeof render === 'function') render();
    const menuReviewFeedback = captureMenuReviewFeedback();
    resetMenuReviewFeedback();
    window.HWANSE_LAST_OBJECTIVE_ACTION = null;
    window.HWANSE_LAST_MENU_REVIEW_COMPLETION_NOTICE = null;
    const objectiveBefore = prototypeObjectiveState();
    const objectiveResult = activatePrototypeObjectiveAction();
    const objectiveAction = window.HWANSE_LAST_OBJECTIVE_ACTION || null;
    const objectiveCompletionNotice = window.HWANSE_LAST_MENU_REVIEW_COMPLETION_NOTICE || null;
    const objectiveActiveDialogueBlock = activeDialogue?.block || null;
    if (typeof render === 'function') render();
    const objectiveMenuReviewFeedback = captureMenuReviewFeedback();
    window.__hwanseStatusMenuStart = {
      ok: true,
      index,
      labels,
      commandResult,
      notice: menuNotice,
      activeId,
      lines,
      statusMenu: window.HWANSE_LAST_STATUS_MENU || null,
      statusProgress: window.HWANSE_LAST_STATUS_MENU_PROGRESS || null,
      progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
      completion,
      objectiveBefore,
      objectiveResult,
      objectiveAction,
      objectiveCompletionNotice,
      objectiveActiveDialogueBlock,
      menuReviewFeedbackLog: menuReviewFeedback.log,
      menuReviewFeedbackRender: menuReviewFeedback.render,
      menuReviewFeedbackLast: menuReviewFeedback.last,
      menuReviewFeedbackLastRender: menuReviewFeedback.lastRender,
      objectiveMenuReviewFeedbackLog: objectiveMenuReviewFeedback.log,
      objectiveMenuReviewFeedbackRender: objectiveMenuReviewFeedback.render,
      objectiveMenuReviewFeedbackLast: objectiveMenuReviewFeedback.last,
      objectiveMenuReviewFeedbackLastRender: objectiveMenuReviewFeedback.lastRender,
    };
  }, 0);
};
if (runtimeState && loadedSaveSummary) {
  finish();
} else {
  window.setTimeout(() => {
    if (runtimeState && loadedSaveSummary) finish();
    else window.__hwanseStatusMenuStart = {
      ok: false,
      reason: 'savedat not loaded',
      labels: menuItems().map((item) => menuItemLabel(item)),
      loadedSaveSummary: loadedSaveSummary || null,
      runtimeState: runtimeState || null,
    };
  }, 300);
}
return true;
"""


def status_menu_state_script() -> str:
    return "return window.__hwanseStatusMenuStart || null;"


def status_member_menu_start_script() -> str:
    return """
window.__hwanseStatusMemberMenu = null;
const resetMenuReviewFeedback = () => {
  window.HWANSE_MENU_REVIEW_FEEDBACK_LOG = [];
  window.HWANSE_MENU_REVIEW_FEEDBACK_RENDER = [];
  window.HWANSE_LAST_MENU_REVIEW_FEEDBACK = null;
  window.HWANSE_LAST_MENU_REVIEW_FEEDBACK_RENDER = null;
  if (typeof activeMenuReviewFeedbacks !== 'undefined') activeMenuReviewFeedbacks = [];
};
const captureMenuReviewFeedback = () => ({
  log: Array.isArray(window.HWANSE_MENU_REVIEW_FEEDBACK_LOG)
    ? [...window.HWANSE_MENU_REVIEW_FEEDBACK_LOG]
    : [],
  render: Array.isArray(window.HWANSE_MENU_REVIEW_FEEDBACK_RENDER)
    ? [...window.HWANSE_MENU_REVIEW_FEEDBACK_RENDER]
    : [],
  last: window.HWANSE_LAST_MENU_REVIEW_FEEDBACK || null,
  lastRender: window.HWANSE_LAST_MENU_REVIEW_FEEDBACK_RENDER || null,
});
const fail = (reason, extra = {}) => {
  window.__hwanseStatusMemberMenu = {
    ok: false,
    reason,
    labels: menuItems().map((item) => menuItemLabel(item)),
    menuMode,
    map: map?.name || '',
    loadedSaveSummary: loadedSaveSummary || null,
    runtimeState: runtimeState || null,
    ...extra,
  };
};
const finish = () => {
  activeDialogue = null;
  menuMode = 'main';
  menuOpen = true;
  selectedMenuItemIndex = 0;
  prototypeProgress = createPrototypeProgressState();
  publishPrototypeProgress();
  window.HWANSE_LAST_STATUS_MEMBER_SELECT_MENU = null;
  window.HWANSE_LAST_STATUS_MEMBER_MENU_SELECTION = null;
  const beforeItems = menuItems();
  const commandIndex = beforeItems.findIndex((item) => item.command === 'openStatusMemberMenu');
  const beforeLabels = beforeItems.map((item) => menuItemLabel(item));
  if (commandIndex < 0) {
    fail('missing status member menu command', { beforeLabels });
    return;
  }
  selectedMenuItemIndex = commandIndex;
  const commandResult = useSelectedMenuItem();
  const afterOpenMenuMode = menuMode;
  const afterOpenMarker = window.HWANSE_LAST_STATUS_MEMBER_SELECT_MENU || null;
  const afterOpenItems = menuItems();
  const afterOpenLabels = afterOpenItems.map((item) => menuItemLabel(item));
  const selectIndex = afterOpenItems.findIndex(
    (item) => item.command === 'selectStatusMember' && item.memberName === 'Rinshan'
  );
  if (selectIndex < 0) {
    fail('missing Rinshan status member menu item', {
      beforeLabels,
      commandResult,
      afterOpenMenuMode,
      afterOpenLabels,
      afterOpenMarker,
    });
    return;
  }
  selectedMenuItemIndex = selectIndex;
  resetMenuReviewFeedback();
  const selectResult = useSelectedMenuItem();
  window.setTimeout(() => {
    if (typeof render === 'function') render();
    const activeId = activeDialogue?.block?.blockId || '';
    const lines = activeDialogue?.lines || [];
    const selection = window.HWANSE_LAST_STATUS_MEMBER_MENU_SELECTION || null;
    const progress = window.HWANSE_LAST_PROTOTYPE_PROGRESS || null;
    const menuReviewFeedback = captureMenuReviewFeedback();
    const completion = publishPrototypeCompletionState();
    resetMenuReviewFeedback();
    window.HWANSE_LAST_OBJECTIVE_ACTION = null;
    window.HWANSE_LAST_MENU_REVIEW_COMPLETION_NOTICE = null;
    activeDialogue = null;
    const objectiveBefore = prototypeObjectiveState();
    const objectiveResult = activatePrototypeObjectiveAction();
    const objectiveAction = window.HWANSE_LAST_OBJECTIVE_ACTION || null;
    const objectiveCompletionNotice = window.HWANSE_LAST_MENU_REVIEW_COMPLETION_NOTICE || null;
    const objectiveActiveDialogueBlock = activeDialogue?.block || null;
    if (typeof render === 'function') render();
    const objectiveMenuReviewFeedback = captureMenuReviewFeedback();
    window.__hwanseStatusMemberMenu = {
      ok: true,
      beforeLabels,
      commandIndex,
      commandResult,
      afterOpenMenuMode,
      afterOpenLabels,
      afterOpenMarker,
      selectIndex,
      selectResult,
      afterMenuMode: menuMode,
      activeId,
      lines,
      selection,
      progress,
      completion,
      objectiveBefore,
      objectiveResult,
      objectiveAction,
      objectiveCompletionNotice,
      objectiveActiveDialogueBlock,
      menuReviewFeedbackLog: menuReviewFeedback.log,
      menuReviewFeedbackRender: menuReviewFeedback.render,
      menuReviewFeedbackLast: menuReviewFeedback.last,
      menuReviewFeedbackLastRender: menuReviewFeedback.lastRender,
      objectiveMenuReviewFeedbackLog: objectiveMenuReviewFeedback.log,
      objectiveMenuReviewFeedbackRender: objectiveMenuReviewFeedback.render,
      objectiveMenuReviewFeedbackLast: objectiveMenuReviewFeedback.last,
      objectiveMenuReviewFeedbackLastRender: objectiveMenuReviewFeedback.lastRender,
    };
  }, 0);
};
if (runtimeState && loadedSaveSummary) {
  finish();
} else {
  window.setTimeout(() => {
    if (runtimeState && loadedSaveSummary) finish();
    else fail('savedat not loaded');
  }, 300);
}
return true;
"""


def status_member_menu_state_script() -> str:
    return "return window.__hwanseStatusMemberMenu || null;"


def new_game_status_menu_start_script() -> str:
    return """
window.__hwanseNewGameStatusMenu = null;
const resetMenuReviewFeedback = () => {
  window.HWANSE_MENU_REVIEW_FEEDBACK_LOG = [];
  window.HWANSE_MENU_REVIEW_FEEDBACK_RENDER = [];
  window.HWANSE_LAST_MENU_REVIEW_FEEDBACK = null;
  window.HWANSE_LAST_MENU_REVIEW_FEEDBACK_RENDER = null;
  if (typeof activeMenuReviewFeedbacks !== 'undefined') activeMenuReviewFeedbacks = [];
};
const captureMenuReviewFeedback = () => ({
  log: Array.isArray(window.HWANSE_MENU_REVIEW_FEEDBACK_LOG)
    ? [...window.HWANSE_MENU_REVIEW_FEEDBACK_LOG]
    : [],
  render: Array.isArray(window.HWANSE_MENU_REVIEW_FEEDBACK_RENDER)
    ? [...window.HWANSE_MENU_REVIEW_FEEDBACK_RENDER]
    : [],
  last: window.HWANSE_LAST_MENU_REVIEW_FEEDBACK || null,
  lastRender: window.HWANSE_LAST_MENU_REVIEW_FEEDBACK_RENDER || null,
});
const fail = (reason, extra = {}) => {
  window.__hwanseNewGameStatusMenu = {
    ok: false,
    reason,
    labels: menuItems().map((item) => menuItemLabel(item)),
    map: map?.name || '',
    runtimeState: runtimeState || null,
    loadedSaveSummary: loadedSaveSummary || null,
    ...extra,
  };
};
if (!loadedSaveSummary && typeof ensurePrototypeRuntimeState === 'function') ensurePrototypeRuntimeState();
if (!runtimeState || loadedSaveSummary) {
  fail('expected prototype runtime without loaded savedat');
  return true;
}
menuMode = 'main';
menuOpen = true;
const items = menuItems();
const index = items.findIndex((item) => item.command === 'showStatusMenu');
const labels = items.map((item) => menuItemLabel(item));
if (index < 0) {
  fail('missing new-game status menu command', { labels });
  return true;
}
selectedMenuItemIndex = index;
resetMenuReviewFeedback();
const commandResult = useSelectedMenuItem();
window.setTimeout(() => {
  const activeId = activeDialogue?.block?.blockId || '';
  const lines = activeDialogue?.lines || [];
  const completion = publishPrototypeCompletionState();
  if (typeof render === 'function') render();
  const menuReviewFeedback = captureMenuReviewFeedback();
  resetMenuReviewFeedback();
  window.HWANSE_LAST_OBJECTIVE_ACTION = null;
  window.HWANSE_LAST_MENU_REVIEW_COMPLETION_NOTICE = null;
  const objectiveBefore = prototypeObjectiveState();
  const objectiveResult = activatePrototypeObjectiveAction();
  const objectiveAction = window.HWANSE_LAST_OBJECTIVE_ACTION || null;
  const objectiveCompletionNotice = window.HWANSE_LAST_MENU_REVIEW_COMPLETION_NOTICE || null;
  const objectiveActiveDialogueBlock = activeDialogue?.block || null;
  if (typeof render === 'function') render();
  const objectiveMenuReviewFeedback = captureMenuReviewFeedback();
  window.__hwanseNewGameStatusMenu = {
    ok: true,
    index,
    labels,
    commandResult,
    notice: menuNotice,
    activeId,
    lines,
    statusMenu: window.HWANSE_LAST_STATUS_MENU || null,
    statusProgress: window.HWANSE_LAST_STATUS_MENU_PROGRESS || null,
    progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
    completion,
    objectiveBefore,
    objectiveResult,
    objectiveAction,
    objectiveCompletionNotice,
    objectiveActiveDialogueBlock,
    menuReviewFeedbackLog: menuReviewFeedback.log,
    menuReviewFeedbackRender: menuReviewFeedback.render,
    menuReviewFeedbackLast: menuReviewFeedback.last,
    menuReviewFeedbackLastRender: menuReviewFeedback.lastRender,
    objectiveMenuReviewFeedbackLog: objectiveMenuReviewFeedback.log,
    objectiveMenuReviewFeedbackRender: objectiveMenuReviewFeedback.render,
    objectiveMenuReviewFeedbackLast: objectiveMenuReviewFeedback.last,
    objectiveMenuReviewFeedbackLastRender: objectiveMenuReviewFeedback.lastRender,
    loadedSaveSummary: loadedSaveSummary || null,
  };
}, 0);
return true;
"""


def new_game_status_menu_state_script() -> str:
    return "return window.__hwanseNewGameStatusMenu || null;"


def capture_status_title_continue_script() -> str:
    return """
window.__hwanseStatusMenuTitleContinue = null;
const resetMenuReviewFeedback = () => {
  window.HWANSE_MENU_REVIEW_FEEDBACK_LOG = [];
  window.HWANSE_MENU_REVIEW_FEEDBACK_RENDER = [];
  window.HWANSE_LAST_MENU_REVIEW_FEEDBACK = null;
  window.HWANSE_LAST_MENU_REVIEW_FEEDBACK_RENDER = null;
  if (typeof activeMenuReviewFeedbacks !== 'undefined') activeMenuReviewFeedbacks = [];
};
const captureMenuReviewFeedback = () => ({
  log: Array.isArray(window.HWANSE_MENU_REVIEW_FEEDBACK_LOG)
    ? [...window.HWANSE_MENU_REVIEW_FEEDBACK_LOG]
    : [],
  render: Array.isArray(window.HWANSE_MENU_REVIEW_FEEDBACK_RENDER)
    ? [...window.HWANSE_MENU_REVIEW_FEEDBACK_RENDER]
    : [],
  last: window.HWANSE_LAST_MENU_REVIEW_FEEDBACK || null,
  lastRender: window.HWANSE_LAST_MENU_REVIEW_FEEDBACK_RENDER || null,
});
Promise.resolve()
  .then(() => {
    const completion = publishPrototypeCompletionState();
    resetMenuReviewFeedback();
    window.HWANSE_LAST_OBJECTIVE_ACTION = null;
    window.HWANSE_LAST_MENU_REVIEW_COMPLETION_NOTICE = null;
    const objectiveBefore = prototypeObjectiveState();
    const objectiveResult = activatePrototypeObjectiveAction();
    const objectiveAction = window.HWANSE_LAST_OBJECTIVE_ACTION || null;
    const objectiveCompletionNotice = window.HWANSE_LAST_MENU_REVIEW_COMPLETION_NOTICE || null;
    const objectiveActiveDialogueBlock = activeDialogue?.block || null;
    if (typeof render === 'function') render();
    const objectiveMenuReviewFeedback = captureMenuReviewFeedback();
    window.__hwanseStatusMenuTitleContinue = {
      titleContinue: true,
      scene,
      map: map?.name || '',
      search: location.search,
      progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
      completion,
      objectiveBefore,
      objectiveResult,
      objectiveAction,
      objectiveCompletionNotice,
      objectiveActiveDialogueBlock,
      objectiveMenuReviewFeedbackLog: objectiveMenuReviewFeedback.log,
      objectiveMenuReviewFeedbackRender: objectiveMenuReviewFeedback.render,
      objectiveMenuReviewFeedbackLast: objectiveMenuReviewFeedback.last,
      objectiveMenuReviewFeedbackLastRender: objectiveMenuReviewFeedback.lastRender,
      quickLoadHidden: document.getElementById('quickLoadButton')?.hidden ?? null,
      quickLoadText: document.getElementById('quickLoadButton')?.textContent || '',
      loadedSaveSummary: loadedSaveSummary || null,
    };
  })
  .catch((error) => {
    window.__hwanseStatusMenuTitleContinue = { error: String(error && error.message || error) };
  });
return true;
"""


def status_title_continue_state_script() -> str:
    return "return window.__hwanseStatusMenuTitleContinue || null;"


def wait_for_status_menu_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, status_menu_state_script(), timeout=3)
        state = value if isinstance(value, dict) else {}
        if state and state.get("ok") is True and state.get("activeId", "").startswith("status-menu:"):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"candidate status menu did not become ready: {state!r}")


def wait_for_status_member_menu_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, status_member_menu_state_script(), timeout=3)
        state = value if isinstance(value, dict) else {}
        if (
            state
            and state.get("ok") is True
            and state.get("activeId", "").startswith("status-member:")
            and ((state.get("progress") or {}).get("counts") or {}).get("status-member-menu-prototype") == 1
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"status member menu did not become ready: {state!r}")


def wait_for_new_game_status_menu_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, new_game_status_menu_state_script(), timeout=3)
        state = value if isinstance(value, dict) else {}
        if state and state.get("ok") is True and state.get("activeId", "").startswith("status-menu:"):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"new-game status menu did not become ready: {state!r}")


def wait_for_status_title_ready(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, 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("scene") == "title"
            and state.get("quickLoadHidden") is False
            and state.get("quickLoadText") == "이어하기"
            and any("이어하기 map1_02b 11,12" in label for label in labels)
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"status title continue did not become ready: {state!r}")


def wait_for_status_title_continue_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, status_title_continue_state_script(), timeout=3)
        state = value if isinstance(value, dict) else {}
        if state and state.get("titleContinue") is True:
            return state
        time.sleep(0.2)
    raise WebDriverError(f"status title continue state did not become ready: {state!r}")


def menu_review_feedback_entries(state: dict, key: str) -> list[dict]:
    return [entry for entry in (state.get(key) or []) if isinstance(entry, dict)]


def menu_review_feedback_summary(state: dict, key: str) -> str:
    return ",".join(
        f"{entry.get('source')}:{entry.get('kind')}:{entry.get('id')}:{entry.get('text')}"
        for entry in menu_review_feedback_entries(state, key)
    )


def menu_review_feedback_rendered(state: dict, key: str) -> bool:
    return any(
        entry.get("browserMenuReviewFeedbackImplemented") is True
        for entry in menu_review_feedback_entries(state, key)
    )


def verify_status_menu_review_feedback(state: dict, key: str, *, menu_id: str, character_count: int, menu_source: str, rendered: bool = False) -> None:
    entries = menu_review_feedback_entries(state, key)
    for entry in entries:
        if (
            entry.get("source") == "status-menu-review-feedback"
            and entry.get("kind") == "status-menu-prototype"
            and entry.get("id") == menu_id
            and entry.get("label") == "상태"
            and entry.get("text") == f"상태 확인 {character_count}명"
            and entry.get("characterCount") == character_count
            and entry.get("menuSource") == menu_source
            and entry.get("durationMs") == 1000
            and entry.get("active") is True
            and entry.get("menuReviewSound") == "menuConfirm"
            and str(entry.get("menuReviewSoundSrc") or "").endswith("/extract_wlk/04.wav")
            and entry.get("menuReviewSoundPlayed") is True
            and entry.get("browserMenuReviewFeedbackImplemented") is True
            and entry.get("originalEquipmentOffsetsMapped") is False
            and entry.get("originalStoryFlagRuntimeImplemented") is False
            and (not rendered or (entry.get("x", 0) > 0 and entry.get("y", 0) > 0 and entry.get("alpha", 0) > 0))
        ):
            return
    raise WebDriverError(f"missing status menu review feedback {key}: {entries!r}")


def verify_status_member_menu_review_feedback(state: dict, key: str, *, rendered: bool = False) -> None:
    entries = menu_review_feedback_entries(state, key)
    for entry in entries:
        if (
            entry.get("source") == "status-member-menu-review-feedback"
            and entry.get("kind") == "status-member-menu-prototype"
            and entry.get("id") == "status-member:map2_02d:Rinshan"
            and entry.get("label") == "상태 선택 Rinshan"
            and entry.get("text") == "상태 선택 Rinshan"
            and entry.get("characterCount") == 1
            and entry.get("menuSource") == "savedat-character-stat-fields"
            and entry.get("durationMs") == 1000
            and entry.get("active") is True
            and entry.get("menuReviewSound") == "menuConfirm"
            and str(entry.get("menuReviewSoundSrc") or "").endswith("/extract_wlk/04.wav")
            and entry.get("menuReviewSoundPlayed") is True
            and entry.get("browserMenuReviewFeedbackImplemented") is True
            and entry.get("originalEquipmentOffsetsMapped") is False
            and entry.get("originalStoryFlagRuntimeImplemented") is False
            and (not rendered or (entry.get("x", 0) > 0 and entry.get("y", 0) > 0 and entry.get("alpha", 0) > 0))
        ):
            return
    raise WebDriverError(f"missing status member menu review feedback {key}: {entries!r}")


def verify_status_completion_notice_feedback(
    state: dict,
    *,
    menu_id: str,
    character_count: int,
    menu_source: str,
) -> None:
    feedback = state.get("objectiveMenuReviewFeedbackLast") or {}
    rendered = state.get("objectiveMenuReviewFeedbackLastRender") or {}
    log = menu_review_feedback_entries(state, "objectiveMenuReviewFeedbackLog")
    render_log = menu_review_feedback_entries(state, "objectiveMenuReviewFeedbackRender")
    if (
        len(log) < 1
        or len(render_log) < 1
        or feedback.get("source") != "status-menu-completion-notice-feedback"
        or feedback.get("kind") != "status-menu-prototype"
        or feedback.get("id") != menu_id
        or feedback.get("label") != "상태"
        or feedback.get("text") != "상태 확인 완료 1/1"
        or feedback.get("characterCount") != character_count
        or feedback.get("menuSource") != menu_source
        or feedback.get("durationMs") != 1000
        or feedback.get("menuReviewSound") != "menuConfirm"
        or not str(feedback.get("menuReviewSoundSrc") or "").endswith("/extract_wlk/04.wav")
        or feedback.get("menuReviewSoundPlayed") is not True
        or feedback.get("browserMenuReviewFeedbackImplemented") is not True
        or feedback.get("originalEquipmentOffsetsMapped") is not False
        or feedback.get("originalStoryFlagRuntimeImplemented") is not False
        or rendered.get("active") is not True
        or rendered.get("source") != "status-menu-completion-notice-feedback"
        or rendered.get("kind") != "status-menu-prototype"
        or rendered.get("id") != menu_id
        or rendered.get("label") != "상태"
        or rendered.get("text") != "상태 확인 완료 1/1"
        or rendered.get("characterCount") != character_count
        or rendered.get("menuSource") != menu_source
        or rendered.get("durationMs") != 1000
        or rendered.get("menuReviewSound") != "menuConfirm"
        or not str(rendered.get("menuReviewSoundSrc") or "").endswith("/extract_wlk/04.wav")
        or rendered.get("menuReviewSoundPlayed") is not True
        or rendered.get("browserMenuReviewFeedbackImplemented") is not True
        or rendered.get("originalEquipmentOffsetsMapped") is not False
        or rendered.get("originalStoryFlagRuntimeImplemented") is not False
    ):
        raise WebDriverError(f"status completion notice feedback is incomplete: {state!r}")


def verify_status_member_completion_notice_feedback(state: dict) -> None:
    feedback = state.get("objectiveMenuReviewFeedbackLast") or {}
    rendered = state.get("objectiveMenuReviewFeedbackLastRender") or {}
    log = menu_review_feedback_entries(state, "objectiveMenuReviewFeedbackLog")
    render_log = menu_review_feedback_entries(state, "objectiveMenuReviewFeedbackRender")
    if (
        len(log) < 1
        or len(render_log) < 1
        or feedback.get("source") != "status-member-menu-completion-notice-feedback"
        or feedback.get("kind") != "status-member-menu-prototype"
        or feedback.get("id") != "status-member:map2_02d:Rinshan"
        or feedback.get("label") != "상태 선택"
        or feedback.get("text") != "상태 선택 확인 완료 1/1"
        or feedback.get("characterCount") != 1
        or feedback.get("selectedMemberName") != "Rinshan"
        or feedback.get("menuSource") != "savedat-character-stat-fields"
        or feedback.get("durationMs") != 1000
        or feedback.get("menuReviewSound") != "menuConfirm"
        or not str(feedback.get("menuReviewSoundSrc") or "").endswith("/extract_wlk/04.wav")
        or feedback.get("menuReviewSoundPlayed") is not True
        or feedback.get("browserMenuReviewFeedbackImplemented") is not True
        or feedback.get("originalEquipmentOffsetsMapped") is not False
        or feedback.get("originalStoryFlagRuntimeImplemented") is not False
        or rendered.get("active") is not True
        or rendered.get("source") != "status-member-menu-completion-notice-feedback"
        or rendered.get("kind") != "status-member-menu-prototype"
        or rendered.get("id") != "status-member:map2_02d:Rinshan"
        or rendered.get("label") != "상태 선택"
        or rendered.get("text") != "상태 선택 확인 완료 1/1"
        or rendered.get("characterCount") != 1
        or rendered.get("selectedMemberName") != "Rinshan"
        or rendered.get("menuSource") != "savedat-character-stat-fields"
        or rendered.get("durationMs") != 1000
        or rendered.get("menuReviewSound") != "menuConfirm"
        or not str(rendered.get("menuReviewSoundSrc") or "").endswith("/extract_wlk/04.wav")
        or rendered.get("menuReviewSoundPlayed") is not True
        or rendered.get("browserMenuReviewFeedbackImplemented") is not True
        or rendered.get("originalEquipmentOffsetsMapped") is not False
        or rendered.get("originalStoryFlagRuntimeImplemented") is not False
    ):
        raise WebDriverError(f"status member completion notice feedback is incomplete: {state!r}")


def status_completion_notice_feedback_summary(state: dict) -> str:
    feedback = state.get("objectiveMenuReviewFeedbackLast") or {}
    rendered = state.get("objectiveMenuReviewFeedbackLastRender") or {}
    return (
        f"statusNoticeFeedback={feedback.get('source')}:{feedback.get('text')} "
        f"statusNoticeFeedbackRender={rendered.get('active')} "
        f"statusNoticeSound={feedback.get('menuReviewSound')}:"
        f"{feedback.get('menuReviewSoundSrc')}:{feedback.get('menuReviewSoundPlayed')}"
    )


def verify_status_menu_state(state: dict) -> None:
    labels = state.get("labels") or []
    lines = "\n".join(state.get("lines") or [])
    status_menu = state.get("statusMenu") or {}
    progress = state.get("progress") or {}
    counts = progress.get("counts") or {}
    status_progress = state.get("statusProgress") or {}
    status_auto_save = status_progress.get("autoSave") or {}
    completion = state.get("completion") or {}
    menu_review = completion.get("menuReview") or {}
    objective_before = state.get("objectiveBefore") or {}
    objective_action = state.get("objectiveAction") or {}
    objective_notice = state.get("objectiveCompletionNotice") or {}
    objective_dialogue = state.get("objectiveActiveDialogueBlock") or {}
    objective_notice_lines = "\n".join(objective_notice.get("lines") or [])
    menu_feedback = status_progress.get("menuReviewFeedback") or {}
    rows = status_menu.get("rows") or []
    names = [row.get("name") for row in rows]
    verify_status_menu_review_feedback(
        state,
        "menuReviewFeedbackLog",
        menu_id="status-menu:map2_02d",
        character_count=3,
        menu_source="savedat-character-stat-fields",
    )
    verify_status_menu_review_feedback(
        state,
        "menuReviewFeedbackRender",
        menu_id="status-menu:map2_02d",
        character_count=3,
        menu_source="savedat-character-stat-fields",
        rendered=True,
    )
    verify_status_completion_notice_feedback(
        state,
        menu_id="status-menu:map2_02d",
        character_count=3,
        menu_source="savedat-character-stat-fields",
    )
    if (
        "상태 3" not in labels
        or state.get("commandResult") is not True
        or state.get("activeId") != "status-menu:map2_02d"
        or status_menu.get("source") != "savedat-character-stat-fields"
        or status_menu.get("characterCount") != 3
        or status_menu.get("originalEquipmentOffsetsMapped") is not False
        or status_menu.get("originalStoryFlagRuntimeImplemented") is not False
        or names != ["Ataho", "Rinshan", "Smashu"]
        or "상태 savedat: 3명" not in lines
        or "Ataho Lv" not in lines
        or "Rinshan Lv" not in lines
        or "Smashu Lv" not in lines
        or "ATK" not in lines
        or "DEF" not in lines
        or "장비 offset" not in lines
        or counts.get("status-menu-prototype") != 1
        or status_progress.get("id") != "status-menu:map2_02d"
        or status_progress.get("duplicateProgress") is not False
        or menu_feedback.get("text") != "상태 확인 3명"
        or menu_feedback.get("menuReviewSound") != "menuConfirm"
        or not str(menu_feedback.get("menuReviewSoundSrc") or "").endswith("/extract_wlk/04.wav")
        or menu_feedback.get("menuReviewSoundPlayed") is not True
        or status_auto_save.get("saved") is not True
        or status_auto_save.get("source") != "status-menu-prototype"
        or status_auto_save.get("payloadMap") != "map2_02d"
        or menu_review.get("id") != "status-menu:map2_02d"
        or menu_review.get("kind") != "status-menu-prototype"
        or menu_review.get("label") != "상태"
        or menu_review.get("completed") is not True
        or menu_review.get("characterCount") != 3
        or objective_before.get("title") != "후보 상태 완료 map2_02d"
        or objective_before.get("nextAction") != "완료 알림 확인"
        or objective_before.get("source") != "prototype-menu-review-completion"
        or state.get("objectiveResult") is not True
        or objective_action.get("action") != "menu-review-completion-notice"
        or objective_action.get("activeId") != "status-review-complete:map2_02d"
        or objective_notice.get("blockId") != "status-review-complete:map2_02d"
        or "상태 확인 완료 1/1" not in objective_notice_lines
        or "인원 3" not in objective_notice_lines
        or "source savedat-character-stat-fields" not in objective_notice_lines
        or objective_dialogue.get("blockId") != "status-review-complete:map2_02d"
    ):
        raise WebDriverError(f"candidate status menu state is incomplete: {state!r}")


def verify_status_member_menu_state(state: dict) -> None:
    before_labels = state.get("beforeLabels") or []
    after_open_labels = state.get("afterOpenLabels") or []
    lines = "\n".join(state.get("lines") or [])
    marker = state.get("afterOpenMarker") or {}
    marker_choices = marker.get("choices") or []
    selection = state.get("selection") or {}
    progress = state.get("progress") or {}
    counts = progress.get("counts") or {}
    progress_event = selection.get("progressEvent") or {}
    auto_save = selection.get("autoSave") or {}
    menu_feedback = selection.get("menuReviewFeedback") or {}
    completion = state.get("completion") or {}
    menu_review = completion.get("menuReview") or {}
    objective_before = state.get("objectiveBefore") or {}
    objective_action = state.get("objectiveAction") or {}
    objective_notice = state.get("objectiveCompletionNotice") or {}
    objective_dialogue = state.get("objectiveActiveDialogueBlock") or {}
    objective_notice_lines = "\n".join(objective_notice.get("lines") or [])
    verify_status_member_menu_review_feedback(state, "menuReviewFeedbackLog")
    verify_status_member_menu_review_feedback(state, "menuReviewFeedbackRender", rendered=True)
    verify_status_member_completion_notice_feedback(state)
    if (
        "상태 선택 3" not in before_labels
        or "상태 3" not in before_labels
        or state.get("commandResult") is not True
        or state.get("selectResult") is not True
        or state.get("afterOpenMenuMode") != "status-member"
        or state.get("afterMenuMode") != "main"
        or state.get("activeId") != "status-member:map2_02d:Rinshan"
        or marker.get("source") != "prototype-status-member-menu"
        or marker.get("map") != "map2_02d"
        or marker.get("count") != 3
        or marker.get("menuSource") != "savedat-character-stat-fields"
        or [choice.get("memberName") for choice in marker_choices] != ["Ataho", "Rinshan", "Smashu"]
        or "상태 1/3 Ataho" not in after_open_labels
        or "상태 2/3 Rinshan" not in after_open_labels
        or "상태 3/3 Smashu" not in after_open_labels
        or "상태 선택 닫기" not in after_open_labels
        or selection.get("source") != "prototype-status-member-menu"
        or selection.get("map") != "map2_02d"
        or selection.get("memberName") != "Rinshan"
        or selection.get("menuCandidateIndex") != 1
        or selection.get("menuCandidateCount") != 3
        or selection.get("menuSource") != "savedat-character-stat-fields"
        or selection.get("started") is not True
        or selection.get("activeId") != "status-member:map2_02d:Rinshan"
        or selection.get("lineCount", 0) < 6
        or "상태 Rinshan" not in lines
        or "savedat 캐릭터 stat/skill 필드 선택 확인입니다." not in lines
        or "Rinshan Lv" not in lines
        or "HP" not in lines
        or "MP" not in lines
        or "ATK" not in lines
        or "DEF" not in lines
        or "장비 offset" not in lines
        or counts.get("status-member-menu-prototype") != 1
        or selection.get("progressCount") != 1
        or progress_event.get("kind") != "status-member-menu-prototype"
        or progress_event.get("id") != "status-member:map2_02d:Rinshan"
        or (progress_event.get("detail") or {}).get("selectedMemberName") != "Rinshan"
        or selection.get("autoSaved") is not True
        or selection.get("autoSource") != "status-member-menu-prototype"
        or auto_save.get("saved") is not True
        or auto_save.get("source") != "status-member-menu-prototype"
        or auto_save.get("payloadMap") != "map2_02d"
        or menu_feedback.get("source") != "status-member-menu-review-feedback"
        or menu_feedback.get("kind") != "status-member-menu-prototype"
        or menu_feedback.get("id") != "status-member:map2_02d:Rinshan"
        or menu_feedback.get("label") != "상태 선택 Rinshan"
        or menu_feedback.get("text") != "상태 선택 Rinshan"
        or menu_feedback.get("menuReviewSound") != "menuConfirm"
        or not str(menu_feedback.get("menuReviewSoundSrc") or "").endswith("/extract_wlk/04.wav")
        or menu_feedback.get("menuReviewSoundPlayed") is not True
        or menu_review.get("id") != "status-member:map2_02d:Rinshan"
        or menu_review.get("kind") != "status-member-menu-prototype"
        or menu_review.get("label") != "상태 선택"
        or menu_review.get("completed") is not True
        or menu_review.get("selectedMemberName") != "Rinshan"
        or menu_review.get("characterCount") != 1
        or objective_before.get("title") != "후보 상태 선택 완료 map2_02d"
        or objective_before.get("nextAction") != "완료 알림 확인"
        or objective_before.get("detail") != "상태 Rinshan"
        or objective_before.get("source") != "prototype-menu-review-completion"
        or state.get("objectiveResult") is not True
        or objective_action.get("action") != "menu-review-completion-notice"
        or objective_action.get("activeId") != "status-member-review-complete:map2_02d"
        or objective_notice.get("blockId") != "status-member-review-complete:map2_02d"
        or "상태 선택 확인 완료 1/1" not in objective_notice_lines
        or "선택 Rinshan" not in objective_notice_lines
        or "member Rinshan" not in objective_notice_lines
        or "source savedat-character-stat-fields" not in objective_notice_lines
        or objective_dialogue.get("blockId") != "status-member-review-complete:map2_02d"
        or marker.get("originalEquipmentOffsetsMapped") is not False
        or marker.get("originalStoryFlagRuntimeImplemented") is not False
        or selection.get("originalEquipmentOffsetsMapped") is not False
        or selection.get("originalStoryFlagRuntimeImplemented") is not False
    ):
        raise WebDriverError(f"status member menu state is incomplete: {state!r}")


def verify_new_game_status_menu_state(state: dict) -> None:
    labels = state.get("labels") or []
    lines = "\n".join(state.get("lines") or [])
    status_menu = state.get("statusMenu") or {}
    progress = state.get("progress") or {}
    counts = progress.get("counts") or {}
    status_progress = state.get("statusProgress") or {}
    status_auto_save = status_progress.get("autoSave") or {}
    completion = state.get("completion") or {}
    menu_review = completion.get("menuReview") or {}
    objective_before = state.get("objectiveBefore") or {}
    objective_action = state.get("objectiveAction") or {}
    objective_notice = state.get("objectiveCompletionNotice") or {}
    objective_dialogue = state.get("objectiveActiveDialogueBlock") or {}
    objective_notice_lines = "\n".join(objective_notice.get("lines") or [])
    menu_feedback = status_progress.get("menuReviewFeedback") or {}
    rows = status_menu.get("rows") or []
    row = rows[0] if rows else {}
    verify_status_menu_review_feedback(
        state,
        "menuReviewFeedbackLog",
        menu_id="status-menu:map1_02b",
        character_count=1,
        menu_source="prototype-party-defaults",
    )
    verify_status_menu_review_feedback(
        state,
        "menuReviewFeedbackRender",
        menu_id="status-menu:map1_02b",
        character_count=1,
        menu_source="prototype-party-defaults",
        rendered=True,
    )
    verify_status_completion_notice_feedback(
        state,
        menu_id="status-menu:map1_02b",
        character_count=1,
        menu_source="prototype-party-defaults",
    )
    if (
        "상태 1" not in labels
        or state.get("commandResult") is not True
        or state.get("activeId") != "status-menu:map1_02b"
        or state.get("loadedSaveSummary") is not None
        or status_menu.get("source") != "prototype-party-defaults"
        or status_menu.get("characterCount") != 1
        or len(rows) != 1
        or row.get("name") != "Ataho"
        or row.get("level") != 1
        or row.get("hp") != 36
        or row.get("hpMax") != 36
        or row.get("mp") != 8
        or row.get("mpMax") != 8
        or row.get("atk") != 0
        or row.get("def") != 0
        or row.get("skillLearned") != 0
        or row.get("skillTotal") != 0
        or status_menu.get("originalEquipmentOffsetsMapped") is not False
        or status_menu.get("originalStoryFlagRuntimeImplemented") is not False
        or "상태 prototype: 1명" not in lines
        or "savedat 없이 현재 웹 프로토타입 동료 구성을 표시합니다." not in lines
        or "Ataho Lv1 HP 36/36 MP 8/8" not in lines
        or "EXP 0/100 상태 정상 기술 0/0" not in lines
        or "ATK 0 DEF 0 TECH 0 QUICK 0 LUCK 0" not in lines
        or "장비 offset" not in lines
        or counts.get("status-menu-prototype") != 1
        or status_progress.get("id") != "status-menu:map1_02b"
        or status_progress.get("duplicateProgress") is not False
        or menu_feedback.get("text") != "상태 확인 1명"
        or menu_feedback.get("menuReviewSound") != "menuConfirm"
        or not str(menu_feedback.get("menuReviewSoundSrc") or "").endswith("/extract_wlk/04.wav")
        or menu_feedback.get("menuReviewSoundPlayed") is not True
        or status_auto_save.get("saved") is not True
        or status_auto_save.get("source") != "status-menu-prototype"
        or status_auto_save.get("payloadMap") != "map1_02b"
        or menu_review.get("id") != "status-menu:map1_02b"
        or menu_review.get("kind") != "status-menu-prototype"
        or menu_review.get("label") != "상태"
        or menu_review.get("completed") is not True
        or menu_review.get("characterCount") != 1
        or objective_before.get("title") != "후보 상태 완료 map1_02b"
        or objective_before.get("nextAction") != "완료 알림 확인"
        or objective_before.get("source") != "prototype-menu-review-completion"
        or state.get("objectiveResult") is not True
        or objective_action.get("action") != "menu-review-completion-notice"
        or objective_action.get("activeId") != "status-review-complete:map1_02b"
        or objective_notice.get("blockId") != "status-review-complete:map1_02b"
        or "상태 확인 완료 1/1" not in objective_notice_lines
        or "인원 1" not in objective_notice_lines
        or "source prototype-party-defaults" not in objective_notice_lines
        or objective_dialogue.get("blockId") != "status-review-complete:map1_02b"
    ):
        raise WebDriverError(f"new-game status menu state is incomplete: {state!r}")


def verify_status_title_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 {}
    completion = restored_state.get("completion") or {}
    menu_review = completion.get("menuReview") or {}
    objective_before = restored_state.get("objectiveBefore") or {}
    objective_action = restored_state.get("objectiveAction") or {}
    objective_notice = restored_state.get("objectiveCompletionNotice") or {}
    objective_dialogue = restored_state.get("objectiveActiveDialogueBlock") or {}
    objective_notice_lines = "\n".join(objective_notice.get("lines") or [])
    verify_status_completion_notice_feedback(
        restored_state,
        menu_id="status-menu:map1_02b",
        character_count=1,
        menu_source="prototype-party-defaults",
    )
    if (
        title_state.get("scene") != "title"
        or title_state.get("quickLoadHidden") is not False
        or title_state.get("quickLoadText") != "이어하기"
        or not any("이어하기 map1_02b 11,12" in label for label in title_labels)
        or click_state.get("ok") is not True
        or click_state.get("text") != "이어하기"
        or not any("이어하기 map1_02b 11,12" in label for label in click_labels)
        or restored_state.get("titleContinue") is not True
        or restored_state.get("scene") != "map"
        or restored_state.get("map") != "map1_02b"
        or restored_state.get("quickLoadText") != "임시 불러오기"
        or restored_state.get("loadedSaveSummary") is not None
        or counts.get("status-menu-prototype") != 1
        or menu_review.get("id") != "status-menu:map1_02b"
        or menu_review.get("completed") is not True
        or menu_review.get("characterCount") != 1
        or objective_before.get("title") != "후보 상태 완료 map1_02b"
        or objective_before.get("nextAction") != "완료 알림 확인"
        or objective_before.get("source") != "prototype-menu-review-completion"
        or restored_state.get("objectiveResult") is not True
        or objective_action.get("action") != "menu-review-completion-notice"
        or objective_action.get("activeId") != "status-review-complete:map1_02b"
        or objective_notice.get("blockId") != "status-review-complete:map1_02b"
        or "상태 확인 완료 1/1" not in objective_notice_lines
        or "인원 1" not in objective_notice_lines
        or objective_dialogue.get("blockId") != "status-review-complete:map1_02b"
    ):
        raise WebDriverError(
            "new-game status menu title continue did not restore completion objective: "
            f"title={title_state!r} click={click_state!r} restored={restored_state!r}"
        )


def write_report(report: dict) -> None:
    out_dir = ROOT / "out"
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "candidate_status_menu_browser_smoke.json").write_text(
        json.dumps(report, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )
    lines = [
        "# Candidate Status Menu Browser Smoke",
        "",
        f"- status: `{report.get('status')}`",
        f"- savedat status menu: `{report.get('statusMenu')}`",
        f"- status member menu: `{report.get('statusMemberMenu')}`",
        f"- new game status menu: `{report.get('newGameStatusMenu')}`",
        f"- new game status title continue: `{report.get('newGameStatusTitleContinue')}`",
    ]


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_status_menu_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,
            )

            url = load_map(
                base,
                port,
                session_id,
                {
                    "map": "map2_02d",
                    "startTile": "11,12",
                    "publicSave": "flack3r-savedat2",
                },
            )
            execute_js(port, session_id, status_menu_start_script(), timeout=3)
            state = wait_for_status_menu_state(port, session_id)
            verify_status_menu_state(state)
            status_menu = state.get("statusMenu") or {}
            rows = status_menu.get("rows") or []
            stats = ";".join(
                f"{row.get('name')}:{row.get('level')}/{row.get('hp')}/{row.get('mp')}/ATK{row.get('atk')}/DEF{row.get('def')}"
                for row in rows
            )
            execute_js(port, session_id, status_member_menu_start_script(), timeout=3)
            status_member_state = wait_for_status_member_menu_state(port, session_id)
            verify_status_member_menu_state(status_member_state)
            url_new_game_status = load_map(
                base,
                port,
                session_id,
                {
                    "map": "map1_02b",
                    "startTile": "11,12",
                },
            )
            execute_js(port, session_id, new_game_status_menu_start_script(), timeout=3)
            new_game_status_state = wait_for_new_game_status_menu_state(port, session_id)
            verify_new_game_status_menu_state(new_game_status_state)
            new_game_status_menu = new_game_status_state.get("statusMenu") or {}
            new_game_rows = new_game_status_menu.get("rows") or []
            new_game_stats = ";".join(
                f"{row.get('name')}:{row.get('level')}/{row.get('hp')}/{row.get('mp')}/ATK{row.get('atk')}/DEF{row.get('def')}"
                for row in new_game_rows
            )
            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_state = wait_for_status_title_ready(port, session_id)
            title_click = execute_js(port, session_id, click_title_continue_script(), timeout=3)
            if not isinstance(title_click, dict) or title_click.get("ok") is not True:
                raise WebDriverError(f"status menu title continue button was not usable: {title_click!r}")
            wait_for_map_runtime(port, session_id, "map1_02b")
            execute_js(port, session_id, capture_status_title_continue_script(), timeout=3)
            title_continue_state = wait_for_status_title_continue_state(port, session_id)
            verify_status_title_continue_state(title_state, title_click, title_continue_state)
            report = {
                "status": "passed",
                "url": url,
                "newGameStatusUrl": url_new_game_status,
                "statusMenu": (
                    f"label=상태 3 active=status-menu:map2_02d "
                    f"source={status_menu.get('source')} stats={stats} "
                    f"statusFeedback={menu_review_feedback_summary(state, 'menuReviewFeedbackLog')} "
                    f"statusFeedbackRender={menu_review_feedback_rendered(state, 'menuReviewFeedbackRender')} "
                    f"statusSound={((state.get('menuReviewFeedbackLast') or {}).get('menuReviewSound') or '')} "
                    f"statusSoundSrc={((state.get('menuReviewFeedbackLast') or {}).get('menuReviewSoundSrc') or '')} "
                    f"statusSoundPlayed={((state.get('menuReviewFeedbackLast') or {}).get('menuReviewSoundPlayed'))} "
                    f"{status_completion_notice_feedback_summary(state)} "
                    "originalEquipmentOffsetsMapped=False "
                    "originalStoryFlagRuntimeImplemented=False"
                ),
                "statusMemberMenu": (
                    "menuCount=3 selected=Rinshan "
                    "selectionSource=prototype-status-member-menu "
                    f"commandResult={status_member_state.get('commandResult')} "
                    f"selectResult={status_member_state.get('selectResult')} "
                    f"afterOpenMenuMode={status_member_state.get('afterOpenMenuMode')} "
                    f"afterMenuMode={status_member_state.get('afterMenuMode')} "
                    f"active={status_member_state.get('activeId')} "
                    f"progressCount={((status_member_state.get('progress') or {}).get('counts') or {}).get('status-member-menu-prototype')} "
                    f"autoSaved={(status_member_state.get('selection') or {}).get('autoSaved')} "
                    f"autoSource={(status_member_state.get('selection') or {}).get('autoSource')} "
                    f"statusFeedback={menu_review_feedback_summary(status_member_state, 'menuReviewFeedbackLog')} "
                    f"statusFeedbackRender={menu_review_feedback_rendered(status_member_state, 'menuReviewFeedbackRender')} "
                    f"statusSound={((status_member_state.get('menuReviewFeedbackLast') or {}).get('menuReviewSound') or '')} "
                    f"statusSoundSrc={((status_member_state.get('menuReviewFeedbackLast') or {}).get('menuReviewSoundSrc') or '')} "
                    f"statusSoundPlayed={((status_member_state.get('menuReviewFeedbackLast') or {}).get('menuReviewSoundPlayed'))} "
                    f"objective={(status_member_state.get('objectiveBefore') or {}).get('title')} "
                    f"objectiveAction={(status_member_state.get('objectiveAction') or {}).get('action')} "
                    f"objectiveActiveId={(status_member_state.get('objectiveAction') or {}).get('activeId')} "
                    f"{status_completion_notice_feedback_summary(status_member_state)} "
                    "originalEquipmentOffsetsMapped=False "
                    "originalStoryFlagRuntimeImplemented=False"
                ),
                "newGameStatusMenu": (
                    "label=상태 1 active=status-menu:map1_02b "
                    f"source={new_game_status_menu.get('source')} stats={new_game_stats} "
                    f"autoSource={(new_game_status_state.get('statusProgress') or {}).get('autoSave', {}).get('source')} "
                    f"objective={(new_game_status_state.get('objectiveBefore') or {}).get('title')} "
                    f"objectiveAction={(new_game_status_state.get('objectiveAction') or {}).get('action')} "
                    f"objectiveActiveId={(new_game_status_state.get('objectiveAction') or {}).get('activeId')} "
                    f"statusFeedback={menu_review_feedback_summary(new_game_status_state, 'menuReviewFeedbackLog')} "
                    f"statusFeedbackRender={menu_review_feedback_rendered(new_game_status_state, 'menuReviewFeedbackRender')} "
                    f"statusSound={((new_game_status_state.get('menuReviewFeedbackLast') or {}).get('menuReviewSound') or '')} "
                    f"statusSoundSrc={((new_game_status_state.get('menuReviewFeedbackLast') or {}).get('menuReviewSoundSrc') or '')} "
                    f"statusSoundPlayed={((new_game_status_state.get('menuReviewFeedbackLast') or {}).get('menuReviewSoundPlayed'))} "
                    f"{status_completion_notice_feedback_summary(new_game_status_state)} "
                    "loadedSaveSummary=False "
                    "originalEquipmentOffsetsMapped=False "
                    "originalStoryFlagRuntimeImplemented=False"
                ),
                "newGameStatusTitleContinue": (
                    f"titleContinue={title_continue_state.get('titleContinue')} "
                    f"label={next((label for label in (title_state.get('titleMenuLabels') or []) if '이어하기 map1_02b 11,12' in str(label)), '')} "
                    f"quickLoadText={title_continue_state.get('quickLoadText')} "
                    f"statusProgressCount={(title_continue_state.get('progress') or {}).get('counts', {}).get('status-menu-prototype')} "
                    f"objective={(title_continue_state.get('objectiveBefore') or {}).get('title')} "
                    f"objectiveAction={(title_continue_state.get('objectiveAction') or {}).get('action')} "
                    f"objectiveActiveId={(title_continue_state.get('objectiveAction') or {}).get('activeId')} "
                    f"{status_completion_notice_feedback_summary(title_continue_state)} "
                    "loadedSaveSummary=False"
                ),
                "snapshots": {
                    "statusMenu": state,
                    "statusMemberMenu": status_member_state,
                    "newGameStatusMenu": new_game_status_state,
                    "newGameStatusTitle": title_state,
                    "newGameStatusTitleClick": title_click,
                    "newGameStatusTitleContinue": title_continue_state,
                },
            }
            write_report(report)
            print(f"ok candidate status menu browser {report['statusMenu']}")
        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()
