#!/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_progress_record_script() -> str:
    return """
window.__hwanseCandidateProgressRecord = null;
localStorage.removeItem(RUNTIME_SAVE_KEY);
Promise.all([ensureSceneEvents(), ensureEventDialogueBlocks()])
  .then(() => {
    updateObjectButton();
    render();
    sessionStorage.removeItem('HWANSE_LAST_QUICK_OBJECTIVE_ACTION');
    const objectiveButton = document.getElementById('objectiveActionButton');
    const beforeObjectiveButton = {
      hidden: objectiveButton?.hidden ?? null,
      disabled: objectiveButton?.disabled ?? null,
      text: objectiveButton?.textContent || '',
      title: objectiveButton?.title || '',
    };
    if (!objectiveButton) {
      throw new Error('missing objectiveActionButton');
    }
    const clickEvent = new MouseEvent('click', {
      bubbles: true,
      cancelable: true,
      detail: 1,
    });
    const dispatched = objectiveButton.dispatchEvent(clickEvent);
    const objectiveAction = window.HWANSE_LAST_OBJECTIVE_ACTION || null;
    let quickObjectiveAction = null;
    try {
      quickObjectiveAction = JSON.parse(sessionStorage.getItem('HWANSE_LAST_QUICK_OBJECTIVE_ACTION') || 'null');
    } catch (error) {
      quickObjectiveAction = null;
    }
    return {
      activated: objectiveAction?.handled === true,
      objectiveAction,
      quickObjectiveAction,
      beforeObjectiveButton,
      objectiveButtonDispatched: dispatched,
      objectiveButtonDefaultPrevented: clickEvent.defaultPrevented,
    };
  })
  .then((objectiveRecord) => {
    const openedId = activeDialogue?.block?.blockId || '';
    const openedLines = activeDialogue?.lines?.slice(0, 8) || [];
    const assets = (window.HWANSE_LAST_EVENT_OBJECT_INSPECT?.objectAssets || []);
    return Promise.all(assets.map((asset) => ensureImage(asset))).then(() => {
      activeDialogue = null;
      updateObjectButton();
      render();
      const autoSave = window.HWANSE_LAST_EVENT_OBJECT_INSPECT_AUTO_SAVE || window.HWANSE_LAST_EVENT_OBJECT_INSPECT?.autoSave || null;
      const raw = localStorage.getItem(RUNTIME_SAVE_KEY) || '{}';
      const payload = JSON.parse(raw);
      const button = document.getElementById('objectButton');
      const completedHotspotPrompt = window.HWANSE_LAST_ACTION_PROMPT || null;
      window.HWANSE_LAST_EVENT_OBJECT_COMPLETION_NOTICE = null;
      const completedHotspotActionResult = activateHotspotAtFoot();
      const completedHotspotNotice = window.HWANSE_LAST_EVENT_OBJECT_COMPLETION_NOTICE || null;
      const completedHotspotActiveDialogueBlock = activeDialogue ? {
        blockId: activeDialogue.block?.blockId || '',
        line: activeDialogue.lines?.[activeDialogue.index] || '',
        lineCount: activeDialogue.lines?.length || 0,
      } : null;
      activeDialogue = null;
      render();
      window.__hwanseCandidateProgressRecord = {
        activated: objectiveRecord.activated,
        saved: autoSave?.saved === true,
        autoSave,
        scene,
        map: map?.name || '',
        activeId: openedId,
        activeLines: openedLines,
        inspect: window.HWANSE_LAST_EVENT_OBJECT_INSPECT || null,
        completion: window.HWANSE_LAST_PROTOTYPE_COMPLETION || null,
        objectButtonText: button?.textContent || '',
        objectButtonTitle: button?.title || '',
        actionPrompt: window.HWANSE_LAST_ACTION_PROMPT || null,
        completedHotspotPrompt,
        completedHotspotActionResult,
        completedHotspotNotice,
        completedHotspotActiveDialogueBlock,
        objectRender: window.HWANSE_LAST_EVENT_OBJECT_RENDER || null,
        progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
        storyFlags: window.HWANSE_LAST_PROTOTYPE_STORY_FLAGS || null,
        progressRecord: window.HWANSE_LAST_PROGRESS_RECORD || null,
        objectiveAction: objectiveRecord.objectiveAction,
        quickObjectiveAction: objectiveRecord.quickObjectiveAction,
        beforeObjectiveButton: objectiveRecord.beforeObjectiveButton,
        objectiveButtonDispatched: objectiveRecord.objectiveButtonDispatched,
        objectiveButtonDefaultPrevented: objectiveRecord.objectiveButtonDefaultPrevented,
        savedProgress: payload.prototypeProgress || null,
        savedStoryFlags: payload.prototypeStoryFlags || null,
      };
    });
  })
  .catch((error) => {
    window.__hwanseCandidateProgressRecord = { error: String(error && error.message || error) };
  });
return true;
"""


def progress_record_state_script() -> str:
    return "return window.__hwanseCandidateProgressRecord || null;"


def start_progress_load_script() -> str:
    return """
window.__hwanseCandidateProgressLoad = null;
function resetEventObjectInspectFeedback() {
  window.HWANSE_EVENT_OBJECT_INSPECT_FEEDBACK_LOG = [];
  window.HWANSE_EVENT_OBJECT_INSPECT_FEEDBACK_RENDER = [];
  window.HWANSE_LAST_EVENT_OBJECT_INSPECT_FEEDBACK = null;
  window.HWANSE_LAST_EVENT_OBJECT_INSPECT_FEEDBACK_RENDER = null;
  if (typeof activeEventObjectInspectFeedbacks !== 'undefined') activeEventObjectInspectFeedbacks = [];
}
quickLoadRuntime()
  .then((loaded) => {
    return ensureSceneEvents().then(() => {
      const assets = eventObjectCandidatesForMap().map((candidate) => candidate.key);
      return Promise.all(assets.map((asset) => ensureImage(asset))).then(() => {
        updateObjectButton();
        activeDialogue = null;
        render();
        const button = document.getElementById('objectButton');
        const completion = window.HWANSE_LAST_PROTOTYPE_COMPLETION || null;
        const actionPrompt = window.HWANSE_LAST_ACTION_PROMPT || null;
        const objectRender = window.HWANSE_LAST_EVENT_OBJECT_RENDER || null;
        const completedActionResult = activateEventObjectCandidateAtFoot();
        const completionNotice = window.HWANSE_LAST_EVENT_OBJECT_COMPLETION_NOTICE || null;
        const completionNoticeBlock = activeDialogue ? {
          blockId: activeDialogue.block?.blockId || '',
          line: activeDialogue.lines?.[activeDialogue.index] || '',
          lineCount: activeDialogue.lines?.length || 0,
        } : null;
        const progressAfterCompletedAction = window.HWANSE_LAST_PROTOTYPE_PROGRESS || null;
        activeDialogue = null;
        return activateObjectButton().then((buttonCompletedActionResult) => {
          const buttonCompletionNotice = window.HWANSE_LAST_EVENT_OBJECT_COMPLETION_NOTICE || null;
          const buttonCompletionNoticeBlock = activeDialogue ? {
            blockId: activeDialogue.block?.blockId || '',
            line: activeDialogue.lines?.[activeDialogue.index] || '',
            lineCount: activeDialogue.lines?.length || 0,
          } : null;
          const progressAfterButtonAction = window.HWANSE_LAST_PROTOTYPE_PROGRESS || null;
          activeDialogue = null;
          const objectiveBefore = prototypeObjectiveState();
          window.HWANSE_LAST_OBJECTIVE_ACTION = null;
          window.HWANSE_LAST_EVENT_OBJECT_COMPLETION_NOTICE = null;
          resetEventObjectInspectFeedback();
          const objectiveActionResult = activatePrototypeObjectiveAction();
          render();
          const objectiveAction = window.HWANSE_LAST_OBJECTIVE_ACTION || null;
          const objectiveCompletionNotice = window.HWANSE_LAST_EVENT_OBJECT_COMPLETION_NOTICE || null;
          const objectiveInspectFeedbackLog = window.HWANSE_EVENT_OBJECT_INSPECT_FEEDBACK_LOG || [];
          const objectiveInspectFeedbackRender = window.HWANSE_EVENT_OBJECT_INSPECT_FEEDBACK_RENDER || [];
          const objectiveInspectFeedbackLast = window.HWANSE_LAST_EVENT_OBJECT_INSPECT_FEEDBACK || null;
          const objectiveInspectFeedbackLastRender = window.HWANSE_LAST_EVENT_OBJECT_INSPECT_FEEDBACK_RENDER || null;
          const objectiveActiveDialogueBlock = activeDialogue ? {
            blockId: activeDialogue.block?.blockId || '',
            line: activeDialogue.lines?.[activeDialogue.index] || '',
            lineCount: activeDialogue.lines?.length || 0,
          } : null;
          activeDialogue = null;
          const progressReviewResult = startPrototypeProgressReview();
          const progressReviewBlock = activeDialogue ? {
            blockId: activeDialogue.block?.blockId || '',
            line: activeDialogue.lines?.[activeDialogue.index] || '',
            lineCount: activeDialogue.lines?.length || 0,
            lines: activeDialogue.lines?.slice(0, 12) || [],
          } : null;
          activeDialogue = null;
          const storyFlagReviewResult = startPrototypeStoryFlagReview();
          const storyFlagReviewBlock = activeDialogue ? {
            blockId: activeDialogue.block?.blockId || '',
            line: activeDialogue.lines?.[activeDialogue.index] || '',
            lineCount: activeDialogue.lines?.length || 0,
            lines: activeDialogue.lines?.slice(0, 12) || [],
          } : null;
          const storyFlagReview = window.HWANSE_LAST_PROTOTYPE_STORY_FLAG_REVIEW || null;
          activeDialogue = null;
          let savedPayload = {};
          try {
            savedPayload = JSON.parse(localStorage.getItem(RUNTIME_SAVE_KEY) || '{}');
          } catch (error) {
            savedPayload = {};
          }
          window.__hwanseCandidateProgressLoad = {
            loaded,
            scene,
            map: map?.name || '',
            activeId: progressReviewBlock?.blockId || '',
            activeLines: progressReviewBlock?.lines || [],
            progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
            storyFlags: window.HWANSE_LAST_PROTOTYPE_STORY_FLAGS || null,
            savedStoryFlags: savedPayload.prototypeStoryFlags || null,
            completion,
            objectButtonText: button?.textContent || '',
            objectButtonTitle: button?.title || '',
            actionPrompt,
            objectRender,
            completedActionResult,
            completionNotice,
            completionNoticeBlock,
            progressAfterCompletedAction,
            buttonCompletedActionResult,
            buttonCompletionNotice,
            buttonCompletionNoticeBlock,
            progressAfterButtonAction,
            objectiveBefore,
            objectiveActionResult,
            objectiveAction,
            objectiveCompletionNotice,
            objectiveInspectFeedbackLog,
            objectiveInspectFeedbackRender,
            objectiveInspectFeedbackLast,
            objectiveInspectFeedbackLastRender,
            objectiveActiveDialogueBlock,
            progressReviewResult,
            progressReviewBlock,
            storyFlagReviewResult,
            storyFlagReviewBlock,
            storyFlagReview,
            menuCommands: commandMenuItems().map((item) => ({
              key: item.key,
              name: item.name,
              command: item.command,
              usable: item.usable,
            })),
          };
        });
      });
    });
  })
  .catch((error) => {
    window.__hwanseCandidateProgressLoad = { error: String(error && error.message || error) };
  });
return true;
"""

def progress_load_state_script() -> str:
    return "return window.__hwanseCandidateProgressLoad || null;"


def wait_for_record_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, progress_record_state_script(), timeout=3)
        state = value if isinstance(value, dict) else {}
        progress = state.get("progress") or {}
        saved_progress = state.get("savedProgress") or {}
        completion = state.get("completion") or {}
        object_completion = completion.get("objectInspect") or {}
        if (
            state
            and state.get("error") is None
            and progress.get("total") == 1
            and saved_progress.get("events")
            and object_completion.get("completed") is True
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"candidate progress record did not become ready: {state!r}")


def wait_for_load_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, progress_load_state_script(), timeout=3)
        state = value if isinstance(value, dict) else {}
        progress = state.get("progress") or {}
        completion = state.get("completion") or {}
        object_completion = completion.get("objectInspect") or {}
        if (
            state
            and state.get("error") is None
            and state.get("activeId") == "prototype-progress:map2_03l"
            and progress.get("total") == 1
            and object_completion.get("completed") is True
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"candidate progress load did not become ready: {state!r}")


def verify_object_inspect_story_flags(state: dict, *, require_saved: bool) -> None:
    expected_key = "story:object-inspect:map2_03l:event-object-candidates:map2_03l"
    story_flags = state.get("storyFlags") or {}
    progress_story_flags = ((state.get("progress") or {}).get("storyFlags") or {})
    flags = story_flags.get("flags") or {}
    record = flags.get(expected_key) or {}
    if (
        story_flags.get("source") != "prototype-progress-derived-story-flags"
        or story_flags.get("flagCount") != 1
        or story_flags.get("browserPrototypeStoryFlagImplemented") is not True
        or story_flags.get("originalStoryFlagRuntimeImplemented") is not False
        or progress_story_flags.get("flagCount") != 1
        or progress_story_flags.get("browserPrototypeStoryFlagImplemented") is not True
        or progress_story_flags.get("originalStoryFlagRuntimeImplemented") is not False
        or record.get("kind") != "object-inspect"
        or record.get("id") != "event-object-candidates:map2_03l"
        or record.get("map") != "map2_03l"
        or record.get("browserPrototypeStoryFlagImplemented") is not True
        or record.get("originalStoryFlagRuntimeImplemented") is not False
    ):
        raise WebDriverError(f"candidate progress story flags are incomplete: {state!r}")
    if require_saved:
        saved_story_flags = state.get("savedStoryFlags") or {}
        saved_flags = saved_story_flags.get("flags") or {}
        saved_record = saved_flags.get(expected_key) or {}
        if (
            saved_story_flags.get("source") != "prototype-progress-derived-story-flags"
            or saved_story_flags.get("flagCount") != 1
            or saved_story_flags.get("browserPrototypeStoryFlagImplemented") is not True
            or saved_story_flags.get("originalStoryFlagRuntimeImplemented") is not False
            or saved_record.get("kind") != "object-inspect"
            or saved_record.get("id") != "event-object-candidates:map2_03l"
            or saved_record.get("map") != "map2_03l"
        ):
            raise WebDriverError(f"candidate progress saved story flags are incomplete: {state!r}")


def story_flags_summary(state: dict) -> str:
    story_flags = state.get("storyFlags") or {}
    progress_story_flags = ((state.get("progress") or {}).get("storyFlags") or {})
    saved_story_flags = state.get("savedStoryFlags") or {}
    flag_keys = ",".join(sorted((story_flags.get("flags") or {}).keys())) or "-"
    saved_count = saved_story_flags.get("flagCount") if saved_story_flags else "-"
    return (
        f"storyFlags={story_flags.get('flagCount')} "
        f"progressStoryFlags={progress_story_flags.get('flagCount')} "
        f"savedStoryFlags={saved_count} "
        f"storyFlagKeys={flag_keys} "
        f"storyFlagSource={story_flags.get('source')} "
        f"browserPrototypeStoryFlagImplemented={story_flags.get('browserPrototypeStoryFlagImplemented')} "
        f"originalStoryFlagRuntimeImplemented={story_flags.get('originalStoryFlagRuntimeImplemented')}"
    )


def verify_record_state(state: dict) -> None:
    inspect = state.get("inspect") or {}
    completion = state.get("completion") or {}
    object_completion = completion.get("objectInspect") or {}
    action_prompt = state.get("actionPrompt") or {}
    completed_hotspot_prompt = state.get("completedHotspotPrompt") or {}
    completed_hotspot_notice = state.get("completedHotspotNotice") or {}
    completed_hotspot_block = state.get("completedHotspotActiveDialogueBlock") or {}
    object_render = state.get("objectRender") or {}
    rendered_objects = object_render.get("objects") or []
    progress = state.get("progress") or {}
    objective = progress.get("objective") or {}
    record = state.get("progressRecord") or {}
    objective_action = state.get("objectiveAction") or {}
    quick_objective_action = state.get("quickObjectiveAction") or {}
    quick_objective_sound = quick_objective_action.get("sound") or {}
    before_objective_button = state.get("beforeObjectiveButton") or {}
    auto_save = state.get("autoSave") or {}
    auto_save_progress = auto_save.get("progress") or {}
    auto_save_tile = auto_save.get("payloadTile") or {}
    saved_progress = state.get("savedProgress") or {}
    saved_events = saved_progress.get("events") or []
    saved_event = saved_events[0] if saved_events else {}
    lines = "\n".join(state.get("activeLines") or [])
    if (
        state.get("activated") is not True
        or state.get("saved") is not True
        or auto_save.get("saved") is not True
        or auto_save.get("source") != "object-inspect"
        or auto_save.get("id") != "event-object-candidates:map2_03l"
        or auto_save.get("payloadMap") != "map2_03l"
        or auto_save_tile.get("x") != 11
        or auto_save_tile.get("y") != 11
        or (auto_save_progress.get("counts") or {}).get("object-inspect") != 1
        or auto_save.get("count") != 3
        or auto_save.get("linkedDialogueCount") != 3
        or auto_save.get("originalLinkedDialogueRuntimeImplemented") is not False
        or auto_save.get("originalEventObjectRuntimeImplemented") is not False
        or auto_save.get("originalStoryFlagRuntimeImplemented") is not False
        or state.get("scene") != "map"
        or state.get("map") != "map2_03l"
        or state.get("activeId") != "event-object-candidates:map2_03l"
        or before_objective_button.get("hidden") is not False
        or before_objective_button.get("disabled") is not False
        or before_objective_button.get("text") != "목표"
        or "목표 실행: 탐험" not in str(before_objective_button.get("title") or "")
        or state.get("objectiveButtonDefaultPrevented") is not True
        or quick_objective_action.get("source") != "objective-action-button"
        or quick_objective_action.get("action") != "hotspot-action"
        or quick_objective_action.get("handled") is not True
        or quick_objective_sound.get("source") != "quick-objective-sound"
        or quick_objective_sound.get("inputSource") != "objective-action-button"
        or quick_objective_sound.get("action") != "hotspot-action"
        or quick_objective_sound.get("activeId") != "event-object-candidates:map2_03l"
        or quick_objective_sound.get("objectiveTitle") != "탐험"
        or quick_objective_sound.get("soundKey") != "menuConfirm"
        or not str(quick_objective_sound.get("soundSrc") or "").endswith("/extract_wlk/04.wav")
        or quick_objective_sound.get("soundPlayed") is not True
        or quick_objective_sound.get("browserQuickObjectiveSoundImplemented") is not True
        or quick_objective_sound.get("originalDirectSoundTimingImplemented") is not False
        or objective_action.get("action") != "hotspot-action"
        or objective_action.get("handled") is not True
        or objective_action.get("activeId") != "event-object-candidates:map2_03l"
        or (objective_action.get("objective") or {}).get("title") != "탐험"
        or objective_action.get("originalEventVmRuntimeImplemented") is not False
        or objective_action.get("originalStoryFlagRuntimeImplemented") is not False
        or inspect.get("count") != 3
        or inspect.get("originalEventObjectRuntimeImplemented") is not False
        or object_completion.get("id") != "event-object-candidates:map2_03l"
        or object_completion.get("count") != 3
        or object_completion.get("completedCount") != 3
        or object_completion.get("remainingCount") != 0
        or object_completion.get("completed") is not True
        or completion.get("originalStoryFlagRuntimeImplemented") is not False
        or state.get("objectButtonText") != "조사 완료 3"
        or "조사 완료" not in str(state.get("objectButtonTitle") or "")
        or action_prompt.get("kind") != "object-inspect"
        or action_prompt.get("text") != "Enter -> 조사 완료 zg_kni 3개"
        or action_prompt.get("completed") is not True
        or action_prompt.get("originalStoryFlagRuntimeImplemented") is not False
        or completed_hotspot_prompt.get("kind") != "object-inspect"
        or completed_hotspot_prompt.get("text") != "Enter -> 조사 완료 zg_kni 3개"
        or completed_hotspot_prompt.get("completed") is not True
        or state.get("completedHotspotActionResult") is not True
        or completed_hotspot_notice.get("blockId") != "event-object-complete:map2_03l"
        or completed_hotspot_notice.get("map") != "map2_03l"
        or "조사 완료 3" not in " ".join(completed_hotspot_notice.get("lines") or [])
        or completed_hotspot_block.get("blockId") != "event-object-complete:map2_03l"
        or completed_hotspot_block.get("line") != "조사 완료 3"
        or completed_hotspot_block.get("lineCount", 0) < 3
        or (object_render.get("completion") or {}).get("completed") is not True
        or object_render.get("renderedCount", 0) < 1
        or not rendered_objects
        or not all(row.get("completed") is True for row in rendered_objects)
        or not all(" done" in str(row.get("label") or "") for row in rendered_objects)
        or progress.get("total") != 1
        or objective.get("title") != "후보 조사 완료 map2_03l"
        or objective.get("nextAction") != "완료 알림 확인"
        or objective.get("detail") != "조사 완료 3/3"
        or objective.get("source") != "prototype-object-completion"
        or objective.get("originalStoryFlagRuntimeImplemented") is not False
        or progress.get("originalStoryFlagRuntimeImplemented") is not False
        or (progress.get("counts") or {}).get("object-inspect") != 1
        or record.get("kind") != "object-inspect"
        or record.get("id") != "event-object-candidates:map2_03l"
        or record.get("originalStoryFlagRuntimeImplemented") is not False
        or (record.get("detail") or {}).get("originalEventObjectRuntimeImplemented") is not False
        or saved_progress.get("originalStoryFlagRuntimeImplemented") is not False
        or saved_event.get("kind") != "object-inspect"
        or saved_event.get("id") != "event-object-candidates:map2_03l"
        or saved_event.get("originalStoryFlagRuntimeImplemented") is not False
        or "오브젝트 후보 map2_03l: 3개" not in lines
    ):
        raise WebDriverError(f"candidate progress record state is incomplete: {state!r}")
    verify_object_inspect_story_flags(state, require_saved=True)


def verify_object_completion_notice_feedback(state: dict) -> None:
    feedback_log = state.get("objectiveInspectFeedbackLog") or []
    feedback_render_log = state.get("objectiveInspectFeedbackRender") or []
    feedback = state.get("objectiveInspectFeedbackLast") or {}
    feedback_render = state.get("objectiveInspectFeedbackLastRender") or {}
    if (
        len(feedback_log) < 1
        or len(feedback_render_log) < 1
        or feedback.get("source") != "event-object-completion-notice-feedback"
        or feedback.get("inspectSource") != "object-completion-notice"
        or feedback.get("text") != "조사 완료 3"
        or feedback.get("map") != "map2_03l"
        or feedback.get("count") != 3
        or feedback.get("linkedDialogueCount") != 3
        or feedback.get("inspectSound") != "menuConfirm"
        or not str(feedback.get("inspectSoundSrc") or "").endswith("/extract_wlk/04.wav")
        or feedback.get("inspectSoundPlayed") is not True
        or feedback.get("durationMs") != 1100
        or feedback.get("browserEventObjectInspectFeedbackImplemented") is not True
        or feedback.get("prototypeEventObjectInspectImplemented") is not True
        or feedback.get("originalLinkedDialogueRuntimeImplemented") is not False
        or feedback.get("originalEventObjectRuntimeImplemented") is not False
        or feedback.get("originalStoryFlagRuntimeImplemented") is not False
        or feedback_render.get("active") is not True
        or feedback_render.get("source") != "event-object-completion-notice-feedback"
        or feedback_render.get("inspectSource") != "object-completion-notice"
        or feedback_render.get("text") != "조사 완료 3"
        or feedback_render.get("map") != "map2_03l"
        or feedback_render.get("count") != 3
        or feedback_render.get("linkedDialogueCount") != 3
        or feedback_render.get("inspectSound") != "menuConfirm"
        or not str(feedback_render.get("inspectSoundSrc") or "").endswith("/extract_wlk/04.wav")
        or feedback_render.get("inspectSoundPlayed") is not True
        or feedback_render.get("durationMs") != 1100
        or feedback_render.get("browserEventObjectInspectFeedbackImplemented") is not True
        or feedback_render.get("prototypeEventObjectInspectImplemented") is not True
        or feedback_render.get("originalLinkedDialogueRuntimeImplemented") is not False
        or feedback_render.get("originalEventObjectRuntimeImplemented") is not False
        or feedback_render.get("originalStoryFlagRuntimeImplemented") is not False
    ):
        raise WebDriverError(f"candidate progress object completion notice feedback is incomplete: {state!r}")


def object_completion_notice_feedback_summary(state: dict) -> str:
    feedback = state.get("objectiveInspectFeedbackLast") or {}
    rendered = state.get("objectiveInspectFeedbackLastRender") or {}
    return (
        f"objectNoticeFeedback={feedback.get('source')}:{feedback.get('text')} "
        f"objectNoticeFeedbackRender={rendered.get('active')} "
        f"objectNoticeSound={feedback.get('inspectSound')}:"
        f"{feedback.get('inspectSoundSrc')}:{feedback.get('inspectSoundPlayed')}"
    )


def verify_load_state(state: dict) -> None:
    progress = state.get("progress") or {}
    objective = progress.get("objective") or {}
    last_event = progress.get("lastEvent") or {}
    completion = state.get("completion") or {}
    object_completion = completion.get("objectInspect") or {}
    action_prompt = state.get("actionPrompt") or {}
    object_render = state.get("objectRender") or {}
    rendered_objects = object_render.get("objects") or []
    notice = state.get("completionNotice") or {}
    notice_block = state.get("completionNoticeBlock") or {}
    progress_after_action = state.get("progressAfterCompletedAction") or {}
    button_notice = state.get("buttonCompletionNotice") or {}
    button_notice_block = state.get("buttonCompletionNoticeBlock") or {}
    progress_after_button = state.get("progressAfterButtonAction") 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 {}
    progress_review_block = state.get("progressReviewBlock") or {}
    progress_review_lines = "\n".join(progress_review_block.get("lines") or [])
    story_flag_review = state.get("storyFlagReview") or {}
    story_flag_review_block = state.get("storyFlagReviewBlock") or {}
    story_flag_review_lines = "\n".join(story_flag_review_block.get("lines") or [])
    commands = state.get("menuCommands") or []
    command_names = {row.get("command"): row.get("name") for row in commands}
    lines = "\n".join(state.get("activeLines") or [])
    if (
        state.get("loaded") is not True
        or state.get("scene") != "map"
        or state.get("map") != "map2_03l"
        or state.get("activeId") != "prototype-progress:map2_03l"
        or progress.get("total") != 1
        or objective.get("title") != "후보 조사 완료 map2_03l"
        or objective.get("nextAction") != "완료 알림 확인"
        or objective.get("detail") != "조사 완료 3/3"
        or objective.get("source") != "prototype-object-completion"
        or objective.get("originalStoryFlagRuntimeImplemented") is not False
        or progress.get("originalStoryFlagRuntimeImplemented") is not False
        or (progress.get("counts") or {}).get("object-inspect") != 1
        or last_event.get("id") != "event-object-candidates:map2_03l"
        or object_completion.get("id") != "event-object-candidates:map2_03l"
        or object_completion.get("completed") is not True
        or object_completion.get("completedCount") != 3
        or object_completion.get("remainingCount") != 0
        or completion.get("originalStoryFlagRuntimeImplemented") is not False
        or state.get("objectButtonText") != "조사 완료 3"
        or action_prompt.get("text") != "Enter -> 조사 완료 zg_kni 3개"
        or action_prompt.get("completed") is not True
        or (object_render.get("completion") or {}).get("completed") is not True
        or object_render.get("renderedCount", 0) < 1
        or not rendered_objects
        or not all(row.get("completed") is True for row in rendered_objects)
        or not all(" done" in str(row.get("label") or "") for row in rendered_objects)
        or state.get("completedActionResult") is not True
        or notice.get("blockId") != "event-object-complete:map2_03l"
        or notice.get("map") != "map2_03l"
        or "조사 완료 3" not in " ".join(notice.get("lines") or [])
        or (notice.get("completion") or {}).get("completed") is not True
        or notice.get("originalEventObjectRuntimeImplemented") is not False
        or notice.get("originalStoryFlagRuntimeImplemented") is not False
        or notice_block.get("blockId") != "event-object-complete:map2_03l"
        or notice_block.get("line") != "조사 완료 3"
        or notice_block.get("lineCount", 0) < 3
        or state.get("buttonCompletedActionResult") is not True
        or button_notice.get("blockId") != "event-object-complete:map2_03l"
        or button_notice.get("map") != "map2_03l"
        or "조사 완료 3" not in " ".join(button_notice.get("lines") or [])
        or (button_notice.get("completion") or {}).get("completed") is not True
        or button_notice.get("originalEventObjectRuntimeImplemented") is not False
        or button_notice.get("originalStoryFlagRuntimeImplemented") is not False
        or button_notice_block.get("blockId") != "event-object-complete:map2_03l"
        or button_notice_block.get("line") != "조사 완료 3"
        or button_notice_block.get("lineCount", 0) < 3
        or progress_after_action.get("total") != 1
        or (progress_after_action.get("counts") or {}).get("object-inspect") != 1
        or progress_after_button.get("total") != 1
        or (progress_after_button.get("counts") or {}).get("object-inspect") != 1
        or objective_before.get("title") != "후보 조사 완료 map2_03l"
        or objective_before.get("nextAction") != "완료 알림 확인"
        or objective_before.get("detail") != "조사 완료 3/3"
        or objective_before.get("source") != "prototype-object-completion"
        or state.get("objectiveActionResult") is not True
        or objective_action.get("action") != "object-completion-notice"
        or objective_action.get("handled") is not True
        or objective_action.get("activeId") != "event-object-complete:map2_03l"
        or (objective_action.get("objective") or {}).get("title") != "후보 조사 완료 map2_03l"
        or objective_action.get("originalEventObjectRuntimeImplemented") is not False
        or objective_action.get("originalStoryFlagRuntimeImplemented") is not False
        or objective_notice.get("blockId") != "event-object-complete:map2_03l"
        or objective_notice.get("map") != "map2_03l"
        or "조사 완료 3" not in " ".join(objective_notice.get("lines") or [])
        or (objective_notice.get("completion") or {}).get("completed") is not True
        or objective_notice.get("originalEventObjectRuntimeImplemented") is not False
        or objective_notice.get("originalStoryFlagRuntimeImplemented") is not False
        or objective_dialogue.get("blockId") != "event-object-complete:map2_03l"
        or objective_dialogue.get("line") != "조사 완료 3"
        or objective_dialogue.get("lineCount", 0) < 3
        or state.get("progressReviewResult") is not True
        or progress_review_block.get("blockId") != "prototype-progress:map2_03l"
        or state.get("storyFlagReviewResult") is not True
        or story_flag_review_block.get("blockId") != "prototype-story-flags:map2_03l"
        or story_flag_review_block.get("line") != "스토리 상태 map2_03l: 1개"
        or story_flag_review_block.get("lineCount", 0) < 4
        or "스토리 상태 map2_03l: 1개" not in story_flag_review_lines
        or "종류 조사 1" not in story_flag_review_lines
        or "#1 조사 map2_03l event-object-candidates:map2_03l" not in story_flag_review_lines
        or "원본 story flag 실행 증명이 아니라 웹 프로토타입 진행 기록입니다." not in story_flag_review_lines
        or story_flag_review.get("blockId") != "prototype-story-flags:map2_03l"
        or story_flag_review.get("storyFlagCount") != 1
        or (story_flag_review.get("storyFlagCounts") or {}).get("object-inspect") != 1
        or story_flag_review.get("storyFlagKeys") != ["story:object-inspect:map2_03l:event-object-candidates:map2_03l"]
        or story_flag_review.get("source") != "prototype-progress-derived-story-flags"
        or story_flag_review.get("browserPrototypeStoryFlagReviewImplemented") is not True
        or story_flag_review.get("originalStoryFlagRuntimeImplemented") is not False
        or command_names.get("openObjectCandidateMenu") != "조사 선택 3"
        or command_names.get("activatePrototypeObjective") != "목표 실행"
        or "진행 기록 map2_03l: 1개" not in lines
        or "목표 후보 조사 완료 map2_03l" not in lines
        or "다음 완료 알림 확인" not in lines
        or "상태 조사 완료 3/3" not in lines
        or "스토리 상태 1개" not in lines
        or "최근 스토리 조사 map2_03l event-object-candidates:map2_03l" not in lines
        or "스토리 상태 1개" not in progress_review_lines
        or "최근 스토리 조사 map2_03l event-object-candidates:map2_03l" not in progress_review_lines
        or "원본 story flag 실행 증명이 아니라 웹 프로토타입 진행 기록입니다." not in lines
        or "조사 event-object-candidates:map2_03l" not in lines
        or command_names.get("showPrototypeProgress") != "진행 목표 1"
        or command_names.get("showPrototypeStoryFlags") != "스토리 상태 1"
    ):
        raise WebDriverError(f"candidate progress load state is incomplete: {state!r}")
    verify_object_inspect_story_flags(state, require_saved=True)
    verify_object_completion_notice_feedback(state)


def write_report(report: dict) -> None:
    out_dir = ROOT / "out"
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "candidate_progress_browser_smoke.json").write_text(
        json.dumps(report, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    lines = [
        "# Candidate Progress Browser Smoke",
        "",
        f"- status: `{report.get('status')}`",
        f"- record: `{report.get('record')}`",
        f"- restore: `{report.get('restore')}`",
        "",
    ]


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

            record_url = load_map(base, port, session_id, {"map": "map2_03l", "startTile": "11,11"})
            execute_js(port, session_id, start_progress_record_script(), timeout=3)
            record_state = wait_for_record_state(port, session_id)
            verify_record_state(record_state)

            restore_url = load_map(base, port, session_id, {"map": "map2_03l", "startTile": "11,11"})
            execute_js(port, session_id, start_progress_load_script(), timeout=3)
            load_state = wait_for_load_state(port, session_id)
            verify_load_state(load_state)
            command_names = {
                row.get("command"): row.get("name")
                for row in (load_state.get("menuCommands") or [])
            }

            report = {
                "status": "passed",
                "base": base,
                "recordUrl": record_url,
                "restoreUrl": restore_url,
                "record": (
                    "event-object-candidates:map2_03l "
                    f"progressTotal={(record_state.get('progress') or {}).get('total')} "
                    f"objective={(record_state.get('progress') or {}).get('objective', {}).get('title')} "
                    f"next={(record_state.get('progress') or {}).get('objective', {}).get('nextAction')} "
                    f"savedProgressEvents={len((record_state.get('savedProgress') or {}).get('events') or [])} "
                    f"{story_flags_summary(record_state)} "
                    f"quickObjectiveSource={(record_state.get('quickObjectiveAction') or {}).get('source')} "
                    f"objectiveAction={(record_state.get('objectiveAction') or {}).get('action')} "
                    f"quickObjectiveSound={(((record_state.get('quickObjectiveAction') or {}).get('sound') or {}).get('source'))}:"
                    f"{(((record_state.get('quickObjectiveAction') or {}).get('sound') or {}).get('inputSource'))}:"
                    f"{(((record_state.get('quickObjectiveAction') or {}).get('sound') or {}).get('action'))} "
                    f"quickObjectiveSoundSrc={(((record_state.get('quickObjectiveAction') or {}).get('sound') or {}).get('soundSrc'))} "
                    f"quickObjectiveSoundPlayed={(((record_state.get('quickObjectiveAction') or {}).get('sound') or {}).get('soundPlayed'))} "
                    f"beforeObjectiveButtonHidden={(record_state.get('beforeObjectiveButton') or {}).get('hidden')} "
                    f"beforeObjectiveButtonText={(record_state.get('beforeObjectiveButton') or {}).get('text')} "
                    f"autoSaved={(record_state.get('autoSave') or {}).get('saved')} "
                    f"autoSource={(record_state.get('autoSave') or {}).get('source')} "
                    f"objectButton={record_state.get('objectButtonText')} "
                    f"completionState={(record_state.get('completion') or {}).get('objectInspect', {}).get('completed')} "
                    f"prompt={(record_state.get('actionPrompt') or {}).get('text')} "
                    f"completedHotspotResult={record_state.get('completedHotspotActionResult')} "
                    f"completedHotspotNotice={(record_state.get('completedHotspotNotice') or {}).get('blockId')} "
                    f"renderDone={all(row.get('completed') is True for row in ((record_state.get('objectRender') or {}).get('objects') or []))} "
                    "originalStoryFlagRuntimeImplemented=False"
                ),
                "restore": (
                    f"{load_state.get('activeId')} "
                    f"progressRestored={(load_state.get('progress') or {}).get('total') == 1} "
                    f"objective={(load_state.get('progress') or {}).get('objective', {}).get('title')} "
                    f"next={(load_state.get('progress') or {}).get('objective', {}).get('nextAction')} "
                    f"objectButton={load_state.get('objectButtonText')} "
                    f"completionState={(load_state.get('completion') or {}).get('objectInspect', {}).get('completed')} "
                    f"prompt={(load_state.get('actionPrompt') or {}).get('text')} "
                    f"notice={(load_state.get('completionNotice') or {}).get('blockId')} "
                    f"completedActionResult={load_state.get('completedActionResult')} "
                    f"buttonNotice={(load_state.get('buttonCompletionNotice') or {}).get('blockId')} "
                    f"buttonCompletedActionResult={load_state.get('buttonCompletedActionResult')} "
                    f"progressAfterAction={(load_state.get('progressAfterCompletedAction') or {}).get('total')} "
                    f"progressAfterButton={(load_state.get('progressAfterButtonAction') or {}).get('total')} "
                    f"objectiveAction={(load_state.get('objectiveAction') or {}).get('action')} "
                    f"objectiveActiveId={(load_state.get('objectiveAction') or {}).get('activeId')} "
                    f"{object_completion_notice_feedback_summary(load_state)} "
                    f"progressReview={(load_state.get('progressReviewBlock') or {}).get('blockId')} "
                    f"progressReviewStoryStatus={'스토리 상태 1개' in ' '.join((load_state.get('progressReviewBlock') or {}).get('lines') or [])} "
                    f"progressReviewStoryRecent={'최근 스토리 조사 map2_03l event-object-candidates:map2_03l' in ' '.join((load_state.get('progressReviewBlock') or {}).get('lines') or [])} "
                    f"storyFlagReview={(load_state.get('storyFlagReviewBlock') or {}).get('blockId')} "
                    f"storyFlagReviewStatus={'스토리 상태 map2_03l: 1개' in ' '.join((load_state.get('storyFlagReviewBlock') or {}).get('lines') or [])} "
                    f"storyFlagReviewCounts={'종류 조사 1' in ' '.join((load_state.get('storyFlagReviewBlock') or {}).get('lines') or [])} "
                    f"storyFlagReviewMarker={(load_state.get('storyFlagReview') or {}).get('browserPrototypeStoryFlagReviewImplemented')} "
                    f"{story_flags_summary(load_state)} "
                    f"renderDone={all(row.get('completed') is True for row in ((load_state.get('objectRender') or {}).get('objects') or []))} "
                    f"objectMenu={command_names.get('openObjectCandidateMenu')} "
                    f"objectiveMenu={command_names.get('activatePrototypeObjective')} "
                    "menuCommand=showPrototypeProgress "
                    "storyFlagMenuCommand=showPrototypeStoryFlags "
                    "originalStoryFlagRuntimeImplemented=False"
                ),
                "snapshots": {
                    "record": record_state,
                    "restore": load_state,
                },
            }
            write_report(report)
            print(f"ok candidate progress browser record={report['record']} restore={report['restore']}")
        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()
