#!/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]

EXPECTED_ITEM_TEXT = {
    "itemNameSource": "exe-text-table-items",
    "itemTextTableKey": "items",
    "itemTextTableIndex": 0,
    "itemTextTableRefVaHex": "0x0048b984",
    "itemTextTableTextVaHex": "0x0048ba7c",
}


def verify_item_text_provenance(row: dict, *, prefix: str = "item") -> None:
    expected = {
        f"{prefix}NameSource": EXPECTED_ITEM_TEXT["itemNameSource"],
        f"{prefix}TextTableKey": EXPECTED_ITEM_TEXT["itemTextTableKey"],
        f"{prefix}TextTableIndex": EXPECTED_ITEM_TEXT["itemTextTableIndex"],
        f"{prefix}TextTableRefVaHex": EXPECTED_ITEM_TEXT["itemTextTableRefVaHex"],
        f"{prefix}TextTableTextVaHex": EXPECTED_ITEM_TEXT["itemTextTableTextVaHex"],
    }
    for key, value in expected.items():
        if row.get(key) != value:
            raise WebDriverError(f"item text provenance mismatch for {key}: {row!r}")


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,
  hasScreen: !!document.getElementById('screen'),
  scene: typeof scene === 'undefined' ? '' : scene,
  mapName: typeof map === 'undefined' || !map ? '' : map.name,
  titleLoaded: !!images?.title?.complete && (images.title.naturalWidth || 0) > 0,
  quickLoadHidden: document.getElementById('quickLoadButton')?.hidden ?? null,
  quickLoadText: document.getElementById('quickLoadButton')?.textContent || '',
  titleMenuMode: typeof titleMenuMode === 'undefined' ? '' : titleMenuMode,
  titleMenuKeys: titleMenu.map((item) => item.key),
  titleMenuLabels: titleMenu.map((item) => item.label),
  selectedTitleMenuIndex: typeof selectedTitleMenuIndex === 'undefined' ? null : selectedTitleMenuIndex,
  bootError: window.HWANSE_BOOT_ERROR || null,
};
"""


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


def shop_menu_start_script() -> str:
    return """
window.__hwanseShopMenuStart = null;
const items = menuItems();
const index = items.findIndex((item) => item.command === 'openShopBuyMenu');
const mainLabels = items.map((item) => menuItemLabel(item));
const directShopReviewRows = items.filter((item) => item.command === 'showShopCandidates').length;
if (index < 0) {
  window.__hwanseShopMenuStart = {
    ok: false,
    reason: 'missing shop buy command',
    mainLabels,
    directShopReviewRows,
  };
  return true;
}
selectedMenuItemIndex = index;
const openResult = useSelectedMenuItem();
const afterOpenMenuMode = menuMode;
const submenuItems = menuItems();
const labels = submenuItems.map((item) => menuItemLabel(item));
const reviewIndex = submenuItems.findIndex((item) => item.command === 'showShopCandidates');
if (reviewIndex < 0) {
  window.__hwanseShopMenuStart = {
    ok: false,
    reason: 'missing shop review command',
    index,
    mainLabels,
    labels,
    directShopReviewRows,
    openResult,
    afterOpenMenuMode,
  };
  return true;
}
selectedMenuItemIndex = reviewIndex;
const commandResult = useSelectedMenuItem();
window.__hwanseShopMenuStart = {
  ok: true,
  index,
  reviewIndex,
  mainLabels,
  labels,
  directShopReviewRows,
  openResult,
  commandResult,
  afterOpenMenuMode,
};
return true;
"""


def shop_menu_state_script() -> str:
    return """
const base = window.__hwanseShopMenuStart || {};
return {
  ...base,
  currentLabels: menuItems().map((item) => menuItemLabel(item)),
  notice: menuNotice,
  activeId: activeDialogue?.block?.blockId || '',
  lines: activeDialogue?.lines || [],
  shopMenu: window.HWANSE_LAST_SHOP_CANDIDATE_MENU || null,
};
"""


def shop_candidate_menu_selection_script() -> str:
    return """
window.__hwanseShopCandidateMenuSelection = null;
activeDialogue = null;
menuOpen = true;
menuMode = 'main';
selectedMenuItemIndex = 0;
ensureEventDialogueBlocks()
  .then(() => {
    runtimeState = createPrototypeRuntimeState(runtimeState);
    const labels = menuItems().map((item) => menuItemLabel(item));
    const openIndex = menuItems().findIndex((item) => item.command === 'openShopCandidateMenu');
    selectedMenuItemIndex = openIndex;
    const commandResult = useSelectedMenuItem();
    const afterOpenMenuMode = menuMode;
    const candidateItems = menuItems();
    const targetIndex = candidateItems.findIndex(
      (item) => item.command === 'selectShopCandidate' && item.shopCandidateBlock?.blockId === 'event-dialogue-block-032',
    );
    const targetName = candidateItems[targetIndex]?.name || '';
    selectedMenuItemIndex = targetIndex;
    const selectResult = useSelectedMenuItem();
    if (typeof render === 'function') render();
    window.__hwanseShopCandidateMenuSelection = {
      commandResult,
      selectResult,
      before: {
        labels,
        openIndex,
        afterOpenMenuMode,
        candidateLabels: candidateItems.map((item) => menuItemLabel(item)),
        targetIndex,
        targetName,
      },
      marker: window.HWANSE_LAST_SHOP_CANDIDATE_SELECT_MENU || null,
      selection: window.HWANSE_LAST_SHOP_CANDIDATE_MENU_SELECTION || null,
      action: window.HWANSE_LAST_SHOP_ACTION || null,
      buyMenu: window.HWANSE_LAST_SHOP_BUY_MENU || null,
      menuMode,
      menuOpen,
      selectedMenuItemIndex,
      labelsAfter: menuItems().map((item) => menuItemLabel(item)),
      notice: menuNotice,
      scene,
      map: map?.name || '',
      originalShopRuntimeImplemented: false,
    };
  })
  .catch((error) => {
    window.__hwanseShopCandidateMenuSelection = { error: String(error && error.message || error) };
  });
return true;
"""


def shop_candidate_menu_selection_state_script() -> str:
    return "return window.__hwanseShopCandidateMenuSelection || null;"


def shop_purchase_start_script() -> str:
    return """
window.__hwanseShopPurchaseStart = null;
const fail = (reason, extra = {}) => {
  window.__hwanseShopPurchaseStart = {
    ok: false,
    reason,
    labels: menuItems().map((item) => menuItemLabel(item)),
    map: map?.name || '',
    runtimeState: runtimeState || null,
    ...extra,
  };
};
const resetShopTransactionFeedback = () => {
  window.HWANSE_SHOP_TRANSACTION_FEEDBACK_LOG = [];
  window.HWANSE_SHOP_TRANSACTION_FEEDBACK_RENDER = [];
  window.HWANSE_LAST_SHOP_TRANSACTION_FEEDBACK = null;
  window.HWANSE_LAST_SHOP_TRANSACTION_FEEDBACK_RENDER = null;
  if (typeof activeShopTransactionFeedbacks !== 'undefined') activeShopTransactionFeedbacks = [];
};
const resetSoundCapture = () => {
  window.HWANSE_SOUND_COUNTS = {};
  window.HWANSE_SOUND_LOG = [];
  window.HWANSE_LAST_SOUND = null;
};
const captureShopTransactionFeedback = () => ({
  log: Array.isArray(window.HWANSE_SHOP_TRANSACTION_FEEDBACK_LOG)
    ? [...window.HWANSE_SHOP_TRANSACTION_FEEDBACK_LOG]
    : [],
  render: Array.isArray(window.HWANSE_SHOP_TRANSACTION_FEEDBACK_RENDER)
    ? [...window.HWANSE_SHOP_TRANSACTION_FEEDBACK_RENDER]
    : [],
  last: window.HWANSE_LAST_SHOP_TRANSACTION_FEEDBACK || null,
  lastRender: window.HWANSE_LAST_SHOP_TRANSACTION_FEEDBACK_RENDER || null,
});
const captureSoundState = () => ({
  counts: { ...(window.HWANSE_SOUND_COUNTS || {}) },
  last: window.HWANSE_LAST_SOUND || null,
  log: Array.isArray(window.HWANSE_SOUND_LOG) ? [...window.HWANSE_SOUND_LOG] : [],
});
const runPurchase = () => {
  if (!runtimeState) {
    fail('runtimeState missing');
    return;
  }
  Promise.all([ensureSceneEvents(), ensureEventDialogueBlocks(), ensureSavePointCandidates()])
    .then(() => {
	  menuMode = 'main';
	  menuOpen = false;
	  drawActionPrompt();
	  const dialogueGate = {
	    beforePrompt: window.HWANSE_LAST_ACTION_PROMPT || null,
	    completedIds: [],
	    completedVmReplays: [],
	  };
	  let dialogueGuard = 0;
	  while (dialogueGuard < 8) {
	    const prompt = window.HWANSE_LAST_ACTION_PROMPT || null;
	    if (prompt?.kind !== 'dialogue-candidate' || prompt?.completed) break;
	    const startedDialogue = activateDialogueCandidateAtFoot();
	    const openedId = activeDialogue?.block?.blockId || '';
	    if (!startedDialogue || !openedId) {
	      fail('dialogue gate did not open before shop action', { dialogueGate, prompt, startedDialogue, openedId });
	      return;
	    }
	    let lineGuard = 0;
	    while (activeDialogue && lineGuard < 128) {
	      advanceDialogue();
	      lineGuard += 1;
	    }
	    if (activeDialogue) {
	      fail('dialogue gate did not complete before shop action', { dialogueGate, openedId, lineGuard });
	      return;
	    }
	    const vmReplay = typeof eventVmReplaySummaryForBlock === 'function'
	      ? eventVmReplaySummaryForBlock(openedId)
	      : ((window.HWANSE_LAST_EVENT_VM_REPLAY || {}).blockId === openedId ? window.HWANSE_LAST_EVENT_VM_REPLAY : null);
	    dialogueGate.completedIds.push(openedId);
	    dialogueGate.completedVmReplays.push(vmReplay);
	    updateDialogueButton();
	    render();
	    drawActionPrompt();
	    dialogueGuard += 1;
	  }
	  dialogueGate.afterPrompt = window.HWANSE_LAST_ACTION_PROMPT || null;
	  dialogueGate.dialogueCompleteCount = (window.HWANSE_LAST_PROTOTYPE_PROGRESS?.counts || {})['dialogue-complete'] || 0;
	  const actionPrompt = dialogueGate.afterPrompt;
	  const promptRect = actionPromptRect();
	  const canvas = document.getElementById('screen');
  const canvasRect = canvas.getBoundingClientRect();
  const openViaPointer = () => {
    if (!promptRect) return false;
    canvas.dispatchEvent(new PointerEvent('pointerdown', {
      bubbles: true,
      cancelable: true,
      pointerId: 67,
      pointerType: 'mouse',
      isPrimary: true,
      clientX: canvasRect.left + (promptRect.x + promptRect.width / 2) / (canvas.width / canvasRect.width),
      clientY: canvasRect.top + (promptRect.y + promptRect.height / 2) / (canvas.height / canvasRect.height),
    }));
    return true;
  };
  const pointerDispatched = openViaPointer();
  const openResult = pointerDispatched && menuMode === 'shop-buy';
  const shopAction = window.HWANSE_LAST_SHOP_ACTION || null;
  const actionLabels = menuItems().map((item) => menuItemLabel(item));
  if (openResult !== true || actionPrompt?.kind !== 'shop-candidate' || menuMode !== 'shop-buy') {
    fail('shop action did not open buy menu', {
      openResult,
      pointerDispatched,
      promptRect,
      actionPrompt,
      shopAction,
      actionLabels,
      mode: menuMode,
    });
    return;
  }
  const shopItems = menuItems();
  const buyIndex = shopItems.findIndex((item) => item.command === 'buyShopItem' && item.shopItem?.key === 'herb');
  if (buyIndex < 0) {
    fail('missing herb buy command', { actionPrompt, shopAction, shopItems: shopItems.map((item) => menuItemLabel(item)) });
    return;
  }
  const herb = runtimeState.items.find((item) => item.key === 'herb');
  const before = {
    money: runtimeState.money,
    herbCount: herb?.count,
	    labels: shopItems.map((item) => menuItemLabel(item)),
	    mode: menuMode,
	    actionPrompt,
	    dialogueGate,
	    promptRect,
	    canvasPointer: true,
	    shopAction,
  };
  const runBuyFailure = ({ money, count }) => {
    runtimeState.money = money;
    if (herb) herb.count = count;
    resetShopTransactionFeedback();
    resetSoundCapture();
    selectedMenuItemIndex = buyIndex;
    const result = useSelectedMenuItem();
    if (typeof render === 'function') render();
    const failureFeedback = captureShopTransactionFeedback();
    const failureSoundState = captureSoundState();
    const failureHerb = runtimeState.items.find((item) => item.key === 'herb');
    return {
      result,
      notice: menuNotice,
      money: runtimeState.money,
      herbCount: failureHerb?.count,
      purchaseFailure: window.HWANSE_LAST_SHOP_PURCHASE_FAILURE || null,
      autoSave: window.HWANSE_LAST_SHOP_PURCHASE_AUTO_SAVE || null,
      progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
      buyFeedbackLog: failureFeedback.log,
      buyFeedbackRender: failureFeedback.render,
      buyFeedbackLast: failureFeedback.last,
      buyFeedbackLastRender: failureFeedback.lastRender,
      soundState: failureSoundState,
    };
  };
  const buyInsufficientFailure = runBuyFailure({ money: 5, count: before.herbCount });
  const buyFullFailure = runBuyFailure({ money: before.money, count: 99 });
  runtimeState.money = before.money;
  if (herb) herb.count = before.herbCount;
  resetShopTransactionFeedback();
  resetSoundCapture();
  selectedMenuItemIndex = buyIndex;
  const buyResult = useSelectedMenuItem();
  if (typeof render === 'function') render();
  const buyFeedback = captureShopTransactionFeedback();
  const buySoundState = captureSoundState();
  const afterHerb = runtimeState.items.find((item) => item.key === 'herb');
  const after = {
    money: runtimeState.money,
    herbCount: afterHerb?.count,
    notice: menuNotice,
    purchase: window.HWANSE_LAST_SHOP_PURCHASE || null,
    autoSave: window.HWANSE_LAST_SHOP_PURCHASE_AUTO_SAVE || window.HWANSE_LAST_SHOP_PURCHASE?.autoSave || null,
    progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
    labels: menuItems().map((item) => menuItemLabel(item)),
    mode: menuMode,
    buyFeedbackLog: buyFeedback.log,
    buyFeedbackRender: buyFeedback.render,
    buyFeedbackLast: buyFeedback.last,
    buyFeedbackLastRender: buyFeedback.lastRender,
    soundState: buySoundState,
  };
  const autoSave = after.autoSave;
  runtimeState.money = 1;
  if (afterHerb) afterHerb.count = 0;
  quickLoadRuntime()
    .then((loaded) => {
      const restoredHerb = runtimeState?.items?.find((item) => item.key === 'herb') || null;
      const restored = {
        map: map?.name || '',
        money: runtimeState?.money,
        herbCount: restoredHerb?.count,
        progressCount: (prototypeProgress?.events || []).filter((event) => event.kind === 'shop-buy-prototype').length,
        progressReview: prototypeProgressReviewBlock(),
        purchase: window.HWANSE_LAST_SHOP_PURCHASE || null,
        loadedSummaryMoney: loadedSaveSummary?.money,
        loadedSummaryHerbCount: (loadedSaveSummary?.items || []).find((item) => item.key === 'herb')?.count,
      };
      menuMode = 'main';
      menuOpen = true;
      selectedMenuItemIndex = menuItems().findIndex((item) => item.command === 'openShopSellMenu');
      if (selectedMenuItemIndex < 0) {
        fail('missing openShopSellMenu command', { before, after, restored });
        return;
      }
      const openSellResult = useSelectedMenuItem();
      const sellItems = menuItems();
      const sellIndex = sellItems.findIndex((item) => item.command === 'sellShopItem' && item.shopItem?.key === 'herb');
      if (sellIndex < 0) {
        fail('missing herb sell command', { before, after, restored, sellItems: sellItems.map((item) => menuItemLabel(item)) });
        return;
      }
      const beforeSellHerb = runtimeState.items.find((item) => item.key === 'herb');
	      const beforeSell = {
	        money: runtimeState.money,
	        herbCount: beforeSellHerb?.count,
	        labels: sellItems.map((item) => menuItemLabel(item)),
	        mode: menuMode,
	      };
	      const runSellFailure = ({ money, count }) => {
	        runtimeState.money = money;
	        const failureHerbSource = runtimeState.items.find((item) => item.key === 'herb');
	        if (failureHerbSource) failureHerbSource.count = count;
	        resetShopTransactionFeedback();
	        resetSoundCapture();
	        selectedMenuItemIndex = sellIndex;
	        const result = useSelectedMenuItem();
	        if (typeof render === 'function') render();
	        const failureFeedback = captureShopTransactionFeedback();
	        const failureSoundState = captureSoundState();
	        const failureHerb = runtimeState.items.find((item) => item.key === 'herb');
	        return {
	          result,
	          notice: menuNotice,
	          money: runtimeState.money,
	          herbCount: failureHerb?.count,
	          saleFailure: window.HWANSE_LAST_SHOP_SALE_FAILURE || null,
	          autoSave: window.HWANSE_LAST_SHOP_SALE_AUTO_SAVE || null,
	          progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
	          sellFeedbackLog: failureFeedback.log,
	          sellFeedbackRender: failureFeedback.render,
	          sellFeedbackLast: failureFeedback.last,
	          sellFeedbackLastRender: failureFeedback.lastRender,
	          soundState: failureSoundState,
	        };
	      };
	      const sellEmptyFailure = runSellFailure({ money: beforeSell.money, count: 0 });
	      runtimeState.money = beforeSell.money;
	      if (beforeSellHerb) beforeSellHerb.count = beforeSell.herbCount;
	      resetShopTransactionFeedback();
	      resetSoundCapture();
	      selectedMenuItemIndex = sellIndex;
	      const sellResult = useSelectedMenuItem();
      if (typeof render === 'function') render();
      const sellFeedback = captureShopTransactionFeedback();
      const sellSoundState = captureSoundState();
      const afterSellHerb = runtimeState.items.find((item) => item.key === 'herb');
      const afterSell = {
        money: runtimeState.money,
        herbCount: afterSellHerb?.count,
        notice: menuNotice,
        sale: window.HWANSE_LAST_SHOP_SALE || null,
        autoSave: window.HWANSE_LAST_SHOP_SALE_AUTO_SAVE || window.HWANSE_LAST_SHOP_SALE?.autoSave || null,
        progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
        labels: menuItems().map((item) => menuItemLabel(item)),
        mode: menuMode,
        sellFeedbackLog: sellFeedback.log,
        sellFeedbackRender: sellFeedback.render,
        sellFeedbackLast: sellFeedback.last,
        sellFeedbackLastRender: sellFeedback.lastRender,
        soundState: sellSoundState,
      };
      const autoSaveAfterSell = afterSell.autoSave;
      runtimeState.money = 1;
      if (afterSellHerb) afterSellHerb.count = 0;
      quickLoadRuntime()
        .then((loadedAfterSell) => {
          const restoredSellHerb = runtimeState?.items?.find((item) => item.key === 'herb') || null;
          window.__hwanseShopPurchaseStart = {
            ok: true,
            openResult,
            buyResult,
            saved: autoSave?.saved === true,
            autoSave,
            loaded,
            before,
            buyInsufficientFailure,
            buyFullFailure,
            after,
            buyFeedbackLog: buyFeedback.log,
            buyFeedbackRender: buyFeedback.render,
            buyFeedbackLast: buyFeedback.last,
            buyFeedbackLastRender: buyFeedback.lastRender,
            buySoundState,
            restored,
	            openSellResult,
	            sellEmptyFailure,
	            sellResult,
            savedAfterSell: autoSaveAfterSell?.saved === true,
            autoSaveAfterSell,
            loadedAfterSell,
            beforeSell,
            afterSell,
            sellFeedbackLog: sellFeedback.log,
            sellFeedbackRender: sellFeedback.render,
            sellFeedbackLast: sellFeedback.last,
            sellFeedbackLastRender: sellFeedback.lastRender,
            sellSoundState,
            restoredAfterSell: {
              map: map?.name || '',
              money: runtimeState?.money,
              herbCount: restoredSellHerb?.count,
              buyProgressCount: (prototypeProgress?.events || []).filter((event) => event.kind === 'shop-buy-prototype').length,
              sellProgressCount: (prototypeProgress?.events || []).filter((event) => event.kind === 'shop-sell-prototype').length,
              progressReview: prototypeProgressReviewBlock(),
              sale: window.HWANSE_LAST_SHOP_SALE || null,
              loadedSummaryMoney: loadedSaveSummary?.money,
              loadedSummaryHerbCount: (loadedSaveSummary?.items || []).find((item) => item.key === 'herb')?.count,
            },
          };
        })
        .catch((error) => fail(error.message || String(error), { before, after, restored, beforeSell, afterSell }));
    })
    .catch((error) => fail(error.message || String(error), { before, after }));
    })
    .catch((error) => fail(error.message || String(error)));
};
if (map?.name === 'map4_08n') {
  runPurchase();
} else {
  setMap('map4_08n', { spawnTile: { x: 18, y: 42 } })
    .then(runPurchase)
    .catch((error) => fail(error.message || String(error)));
}
return true;
"""


def shop_purchase_state_script() -> str:
    return "return window.__hwanseShopPurchaseStart || null;"


def new_game_shop_start_script() -> str:
    return """
window.__hwanseNewGameShop = null;
const fail = (reason, extra = {}) => {
  window.__hwanseNewGameShop = {
    ok: false,
    reason,
    map: map?.name || '',
    runtimeState: runtimeState || null,
    labels: menuItems().map((item) => menuItemLabel(item)),
    ...extra,
  };
};
const resetShopTransactionFeedback = () => {
  window.HWANSE_SHOP_TRANSACTION_FEEDBACK_LOG = [];
  window.HWANSE_SHOP_TRANSACTION_FEEDBACK_RENDER = [];
  window.HWANSE_LAST_SHOP_TRANSACTION_FEEDBACK = null;
  window.HWANSE_LAST_SHOP_TRANSACTION_FEEDBACK_RENDER = null;
  if (typeof activeShopTransactionFeedbacks !== 'undefined') activeShopTransactionFeedbacks = [];
};
const resetSoundCapture = () => {
  window.HWANSE_SOUND_COUNTS = {};
  window.HWANSE_SOUND_LOG = [];
  window.HWANSE_LAST_SOUND = null;
};
const captureShopTransactionFeedback = () => ({
  log: Array.isArray(window.HWANSE_SHOP_TRANSACTION_FEEDBACK_LOG)
    ? [...window.HWANSE_SHOP_TRANSACTION_FEEDBACK_LOG]
    : [],
  render: Array.isArray(window.HWANSE_SHOP_TRANSACTION_FEEDBACK_RENDER)
    ? [...window.HWANSE_SHOP_TRANSACTION_FEEDBACK_RENDER]
    : [],
  last: window.HWANSE_LAST_SHOP_TRANSACTION_FEEDBACK || null,
  lastRender: window.HWANSE_LAST_SHOP_TRANSACTION_FEEDBACK_RENDER || null,
});
const captureSoundState = () => ({
  counts: { ...(window.HWANSE_SOUND_COUNTS || {}) },
  last: window.HWANSE_LAST_SOUND || null,
  log: Array.isArray(window.HWANSE_SOUND_LOG) ? [...window.HWANSE_SOUND_LOG] : [],
});
const run = () => {
  if (!runtimeState || loadedSaveSummary) {
    fail('expected prototype runtime without loaded savedat', { loadedSaveSummary });
    return;
  }
  Promise.all([ensureSceneEvents(), ensureEventDialogueBlocks()])
    .then(() => {
      localStorage.removeItem(RUNTIME_SAVE_KEY);
	      menuMode = 'main';
	      menuOpen = false;
	      drawActionPrompt();
	      const dialogueGate = {
	        beforePrompt: window.HWANSE_LAST_ACTION_PROMPT || null,
	        completedIds: [],
	        completedVmReplays: [],
	      };
	      let dialogueGuard = 0;
	      while (dialogueGuard < 8) {
	        const prompt = window.HWANSE_LAST_ACTION_PROMPT || null;
	        if (prompt?.kind !== 'dialogue-candidate' || prompt?.completed) break;
	        const startedDialogue = activateDialogueCandidateAtFoot();
	        const openedId = activeDialogue?.block?.blockId || '';
	        if (!startedDialogue || !openedId) {
	          fail('new-game dialogue gate did not open before shop action', { dialogueGate, prompt, startedDialogue, openedId });
	          return;
	        }
	        let lineGuard = 0;
	        while (activeDialogue && lineGuard < 128) {
	          advanceDialogue();
	          lineGuard += 1;
	        }
	        if (activeDialogue) {
	          fail('new-game dialogue gate did not complete before shop action', { dialogueGate, openedId, lineGuard });
	          return;
	        }
	        const vmReplay = typeof eventVmReplaySummaryForBlock === 'function'
	          ? eventVmReplaySummaryForBlock(openedId)
	          : ((window.HWANSE_LAST_EVENT_VM_REPLAY || {}).blockId === openedId ? window.HWANSE_LAST_EVENT_VM_REPLAY : null);
	        dialogueGate.completedIds.push(openedId);
	        dialogueGate.completedVmReplays.push(vmReplay);
	        updateDialogueButton();
	        render();
	        drawActionPrompt();
	        dialogueGuard += 1;
	      }
	      dialogueGate.afterPrompt = window.HWANSE_LAST_ACTION_PROMPT || null;
	      dialogueGate.dialogueCompleteCount = (window.HWANSE_LAST_PROTOTYPE_PROGRESS?.counts || {})['dialogue-complete'] || 0;
	      const actionPrompt = dialogueGate.afterPrompt;
	      const promptRect = actionPromptRect();
      const canvas = document.getElementById('screen');
      const canvasRect = canvas.getBoundingClientRect();
      const pointerDispatched = Boolean(promptRect);
      if (pointerDispatched) {
        canvas.dispatchEvent(new PointerEvent('pointerdown', {
          bubbles: true,
          cancelable: true,
          pointerId: 91,
          pointerType: 'mouse',
          isPrimary: true,
          clientX: canvasRect.left + (promptRect.x + promptRect.width / 2) / (canvas.width / canvasRect.width),
          clientY: canvasRect.top + (promptRect.y + promptRect.height / 2) / (canvas.height / canvasRect.height),
        }));
      }
      const openResult = pointerDispatched && menuMode === 'shop-buy';
      const shopAction = window.HWANSE_LAST_SHOP_ACTION || null;
      if (!openResult || actionPrompt?.kind !== 'shop-candidate') {
        fail('new-game shop action did not open buy menu', { actionPrompt, promptRect, shopAction, openResult });
        return;
      }
      const buyItems = menuItems();
      const buyIndex = buyItems.findIndex((item) => item.command === 'buyShopItem' && item.shopItem?.key === 'herb');
      if (buyIndex < 0) {
        fail('missing new-game herb buy command', { buyLabels: buyItems.map((item) => menuItemLabel(item)) });
        return;
      }
      const herbBefore = runtimeState.items.find((item) => item.key === 'herb') || {};
      const before = {
        money: runtimeState.money,
        herbCount: herbBefore.count,
	        labels: buyItems.map((item) => menuItemLabel(item)),
	        actionPrompt,
	        dialogueGate,
	        promptRect,
        canvasPointer: true,
        shopAction,
        loadedSaveSummary: loadedSaveSummary || null,
      };
      resetShopTransactionFeedback();
      resetSoundCapture();
      selectedMenuItemIndex = buyIndex;
      const buyResult = useSelectedMenuItem();
      if (typeof render === 'function') render();
      const buyFeedback = captureShopTransactionFeedback();
      const buySoundState = captureSoundState();
      const herbAfter = runtimeState.items.find((item) => item.key === 'herb') || {};
      const after = {
        money: runtimeState.money,
        herbCount: herbAfter.count,
        notice: menuNotice,
        purchase: window.HWANSE_LAST_SHOP_PURCHASE || null,
        autoSave: window.HWANSE_LAST_SHOP_PURCHASE_AUTO_SAVE || window.HWANSE_LAST_SHOP_PURCHASE?.autoSave || null,
        progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
        labels: menuItems().map((item) => menuItemLabel(item)),
        loadedSaveSummary: loadedSaveSummary || null,
        buyFeedbackLog: buyFeedback.log,
        buyFeedbackRender: buyFeedback.render,
        buyFeedbackLast: buyFeedback.last,
        buyFeedbackLastRender: buyFeedback.lastRender,
        soundState: buySoundState,
      };
      const autoSave = after.autoSave;
      runtimeState.money = 1;
      herbAfter.count = 0;
      quickLoadRuntime()
        .then((loaded) => {
          const restoredHerb = runtimeState?.items?.find((item) => item.key === 'herb') || {};
          const restored = {
            loaded,
            map: map?.name || '',
            money: runtimeState?.money,
            herbCount: restoredHerb.count,
            buyProgressCount: (prototypeProgress?.events || []).filter((event) => event.kind === 'shop-buy-prototype').length,
            progressReview: prototypeProgressReviewBlock(),
            loadedSaveSummary: loadedSaveSummary || null,
          };
          menuMode = 'main';
          menuOpen = true;
          selectedMenuItemIndex = menuItems().findIndex((item) => item.command === 'openShopSellMenu');
          if (selectedMenuItemIndex < 0) {
            fail('missing new-game openShopSellMenu command', { before, after, restored });
            return;
          }
          const openSellResult = useSelectedMenuItem();
          const sellItems = menuItems();
          const sellIndex = sellItems.findIndex((item) => item.command === 'sellShopItem' && item.shopItem?.key === 'herb');
          if (sellIndex < 0) {
            fail('missing new-game herb sell command', { before, after, restored, sellLabels: sellItems.map((item) => menuItemLabel(item)) });
            return;
          }
          const herbBeforeSell = runtimeState.items.find((item) => item.key === 'herb') || {};
          const beforeSell = {
            money: runtimeState.money,
            herbCount: herbBeforeSell.count,
            labels: sellItems.map((item) => menuItemLabel(item)),
          };
          resetShopTransactionFeedback();
          resetSoundCapture();
          selectedMenuItemIndex = sellIndex;
          const sellResult = useSelectedMenuItem();
          if (typeof render === 'function') render();
          const sellFeedback = captureShopTransactionFeedback();
          const sellSoundState = captureSoundState();
          const herbAfterSell = runtimeState.items.find((item) => item.key === 'herb') || {};
          const afterSell = {
            money: runtimeState.money,
            herbCount: herbAfterSell.count,
            notice: menuNotice,
            sale: window.HWANSE_LAST_SHOP_SALE || null,
            autoSave: window.HWANSE_LAST_SHOP_SALE_AUTO_SAVE || window.HWANSE_LAST_SHOP_SALE?.autoSave || null,
            progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
            labels: menuItems().map((item) => menuItemLabel(item)),
            sellFeedbackLog: sellFeedback.log,
            sellFeedbackRender: sellFeedback.render,
            sellFeedbackLast: sellFeedback.last,
            sellFeedbackLastRender: sellFeedback.lastRender,
            soundState: sellSoundState,
          };
          const autoSaveAfterSell = afterSell.autoSave;
          runtimeState.money = 1;
          herbAfterSell.count = 0;
          quickLoadRuntime()
            .then((loadedAfterSell) => {
              const restoredSellHerb = runtimeState?.items?.find((item) => item.key === 'herb') || {};
              window.__hwanseNewGameShop = {
                ok: true,
                openResult,
                buyResult,
                saved: autoSave?.saved === true,
                autoSave,
                loaded,
                before,
                after,
                buyFeedbackLog: buyFeedback.log,
                buyFeedbackRender: buyFeedback.render,
                buyFeedbackLast: buyFeedback.last,
                buyFeedbackLastRender: buyFeedback.lastRender,
                buySoundState,
                restored,
                openSellResult,
                sellResult,
                savedAfterSell: autoSaveAfterSell?.saved === true,
                autoSaveAfterSell,
                loadedAfterSell,
                beforeSell,
                afterSell,
                sellFeedbackLog: sellFeedback.log,
                sellFeedbackRender: sellFeedback.render,
                sellFeedbackLast: sellFeedback.last,
                sellFeedbackLastRender: sellFeedback.lastRender,
                sellSoundState,
                restoredAfterSell: {
                  map: map?.name || '',
                  money: runtimeState?.money,
                  herbCount: restoredSellHerb.count,
                  buyProgressCount: (prototypeProgress?.events || []).filter((event) => event.kind === 'shop-buy-prototype').length,
                  sellProgressCount: (prototypeProgress?.events || []).filter((event) => event.kind === 'shop-sell-prototype').length,
                  progressReview: prototypeProgressReviewBlock(),
                  loadedSaveSummary: loadedSaveSummary || null,
                },
              };
            })
            .catch((error) => fail(error.message || String(error), { before, after, restored, beforeSell, afterSell }));
        })
        .catch((error) => fail(error.message || String(error), { before, after }));
    })
    .catch((error) => fail(error.message || String(error)));
};
if (map?.name === 'map4_08n') run();
else setMap('map4_08n', { spawnTile: { x: 18, y: 42 } }).then(run).catch((error) => fail(error.message || String(error)));
return true;
"""


def new_game_shop_state_script() -> str:
    return "return window.__hwanseNewGameShop || null;"


def capture_new_game_shop_after_title_continue_script() -> str:
    return """
window.__hwanseNewGameShopTitleRestore = null;
Promise.resolve()
  .then(() => {
    const herb = runtimeState?.items?.find((item) => item.key === 'herb') || null;
    const text = localStorage.getItem(RUNTIME_SAVE_KEY) || '';
    let savedPayload = null;
    try {
      savedPayload = text ? JSON.parse(text) : null;
    } catch (error) {
      savedPayload = { error: String(error && error.message || error) };
    }
    window.__hwanseNewGameShopTitleRestore = {
      ok: true,
      loaded: true,
      titleContinue: true,
      scene,
      map: map?.name || '',
      search: window.location.search,
      money: runtimeState?.money,
      herbCount: herb?.count,
      buyProgressCount: (prototypeProgress?.events || []).filter((event) => event.kind === 'shop-buy-prototype').length,
      sellProgressCount: (prototypeProgress?.events || []).filter((event) => event.kind === 'shop-sell-prototype').length,
      progressReview: prototypeProgressReviewBlock(),
      loadedSaveSummary: loadedSaveSummary || null,
      savedPayload,
      quickLoadHidden: document.getElementById('quickLoadButton')?.hidden ?? null,
      quickLoadText: document.getElementById('quickLoadButton')?.textContent || '',
    };
  })
  .catch((error) => {
    window.__hwanseNewGameShopTitleRestore = { ok: false, error: String(error && error.message || error) };
  });
return true;
"""


def new_game_shop_title_restore_state_script() -> str:
    return "return window.__hwanseNewGameShopTitleRestore || null;"


def shop_completion_objective_capture_script() -> str:
    return """
const resetShopTransactionFeedback = () => {
  window.HWANSE_SHOP_TRANSACTION_FEEDBACK_LOG = [];
  window.HWANSE_SHOP_TRANSACTION_FEEDBACK_RENDER = [];
  window.HWANSE_LAST_SHOP_TRANSACTION_FEEDBACK = null;
  window.HWANSE_LAST_SHOP_TRANSACTION_FEEDBACK_RENDER = null;
  if (typeof activeShopTransactionFeedbacks !== 'undefined') activeShopTransactionFeedbacks = [];
};
const resetSoundCapture = () => {
  window.HWANSE_SOUND_COUNTS = {};
  window.HWANSE_SOUND_LOG = [];
  window.HWANSE_LAST_SOUND = null;
};
const captureShopTransactionFeedback = () => ({
  log: Array.isArray(window.HWANSE_SHOP_TRANSACTION_FEEDBACK_LOG)
    ? [...window.HWANSE_SHOP_TRANSACTION_FEEDBACK_LOG]
    : [],
  render: Array.isArray(window.HWANSE_SHOP_TRANSACTION_FEEDBACK_RENDER)
    ? [...window.HWANSE_SHOP_TRANSACTION_FEEDBACK_RENDER]
    : [],
  last: window.HWANSE_LAST_SHOP_TRANSACTION_FEEDBACK || null,
  lastRender: window.HWANSE_LAST_SHOP_TRANSACTION_FEEDBACK_RENDER || null,
});
const captureSoundState = () => ({
  counts: { ...(window.HWANSE_SOUND_COUNTS || {}) },
  last: window.HWANSE_LAST_SOUND || null,
  log: Array.isArray(window.HWANSE_SOUND_LOG) ? [...window.HWANSE_SOUND_LOG] : [],
});
if (typeof activeDialogue !== 'undefined') activeDialogue = null;
if (typeof menuOpen !== 'undefined') menuOpen = false;
window.HWANSE_LAST_OBJECTIVE_ACTION = null;
window.HWANSE_LAST_SHOP_COMPLETION_NOTICE = null;
resetShopTransactionFeedback();
resetSoundCapture();
const objectiveBefore = typeof prototypeObjectiveState === 'function' ? prototypeObjectiveState() : null;
const objectiveResult = typeof activatePrototypeObjectiveAction === 'function'
  ? activatePrototypeObjectiveAction()
  : false;
if (typeof render === 'function') render();
const objectiveAction = window.HWANSE_LAST_OBJECTIVE_ACTION || null;
const objectiveCompletionNotice = window.HWANSE_LAST_SHOP_COMPLETION_NOTICE || null;
const objectiveShopFeedback = captureShopTransactionFeedback();
const objectiveSoundState = captureSoundState();
const objectiveActiveDialogueBlock = activeDialogue
  ? {
      blockId: activeDialogue.block?.blockId || '',
      line: activeDialogue.lines?.[activeDialogue.index || 0] || '',
      lineCount: activeDialogue.lines?.length || 0,
    }
  : null;
if (typeof activeDialogue !== 'undefined') activeDialogue = null;
return {
  scene,
  mapName: map?.name || '',
  foot: typeof footTile === 'function' ? footTile() : null,
  objectiveBefore,
  objectiveResult,
  objectiveAction,
  objectiveCompletionNotice,
  objectiveShopFeedbackLog: objectiveShopFeedback.log,
  objectiveShopFeedbackRender: objectiveShopFeedback.render,
  objectiveShopFeedbackLast: objectiveShopFeedback.last,
  objectiveShopFeedbackLastRender: objectiveShopFeedback.lastRender,
  objectiveSoundState,
  objectiveActiveDialogueBlock,
  progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
  completion: window.HWANSE_LAST_PROTOTYPE_COMPLETION || null,
  runtimeItems: (runtimeState?.items || []).map((item) => `${item.key}:${item.count}`),
  money: runtimeState?.money,
  originalShopRuntimeImplemented: false,
};
"""


def wait_for_shop_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, shop_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("shop-candidates:"):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"candidate shop menu did not become ready: {state!r}")


def wait_for_shop_candidate_menu_selection(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, shop_candidate_menu_selection_state_script(), timeout=3)
        state = value if isinstance(value, dict) else {}
        if state and state.get("error") is None and state.get("selectResult") is True and state.get("menuMode") == "shop-buy":
            return state
        time.sleep(0.2)
    raise WebDriverError(f"candidate shop selection menu did not finish: {state!r}")


def wait_for_shop_purchase_state(port: int, session_id: str, timeout: float = 10) -> dict:
    deadline = time.monotonic() + timeout
    state: dict = {}
    while time.monotonic() < deadline:
        value = execute_js(port, session_id, shop_purchase_state_script(), timeout=3)
        state = value if isinstance(value, dict) else {}
        if state and state.get("ok") is True:
            return state
        time.sleep(0.2)
    raise WebDriverError(f"candidate shop purchase did not become ready: {state!r}")


def wait_for_new_game_shop_state(port: int, session_id: str, timeout: float = 10) -> dict:
    deadline = time.monotonic() + timeout
    state: dict = {}
    while time.monotonic() < deadline:
        value = execute_js(port, session_id, new_game_shop_state_script(), timeout=3)
        state = value if isinstance(value, dict) else {}
        if state and state.get("ok") is True:
            return state
        time.sleep(0.2)
    raise WebDriverError(f"new-game shop state did not become ready: {state!r}")


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


def wait_for_new_game_shop_title_restore_state(port: int, session_id: str, timeout: float = 10) -> dict:
    deadline = time.monotonic() + timeout
    state: dict = {}
    while time.monotonic() < deadline:
        value = execute_js(port, session_id, new_game_shop_title_restore_state_script(), timeout=3)
        state = value if isinstance(value, dict) else {}
        if (
            state
            and state.get("ok") is True
            and state.get("titleContinue") is True
            and state.get("map") == "map4_08n"
            and state.get("money") == 54
            and state.get("herbCount") == 1
            and state.get("buyProgressCount") == 1
            and state.get("sellProgressCount") == 1
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"new-game shop title continue restore did not become ready: {state!r}")


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


def shop_transaction_feedback_summary(state: dict, key: str) -> str:
    return ",".join(
        f"{entry.get('source')}:{entry.get('transaction')}:{entry.get('itemKey')}:{entry.get('text')}"
        for entry in shop_transaction_feedback_entries(state, key)
    )


def shop_transaction_feedback_rendered(state: dict, key: str) -> bool:
    return any(
        entry.get("browserShopTransactionFeedbackImplemented") is True
        for entry in shop_transaction_feedback_entries(state, key)
    )


def verify_shop_completion_notice_feedback(state: dict) -> None:
    feedback_log = shop_transaction_feedback_entries(state, "objectiveShopFeedbackLog")
    feedback_render_log = shop_transaction_feedback_entries(state, "objectiveShopFeedbackRender")
    feedback = state.get("objectiveShopFeedbackLast") or {}
    feedback_render = state.get("objectiveShopFeedbackLastRender") or {}
    sound_state = state.get("objectiveSoundState") or {}
    sound_counts = sound_state.get("counts") or {}
    if (
        len(feedback_log) < 1
        or len(feedback_render_log) < 1
        or int(sound_counts.get("menuConfirm") or 0) < 1
        or feedback.get("source") != "shop-completion-notice-feedback"
        or feedback.get("text") != "상점 거래 완료 2/2"
        or feedback.get("map") != "map4_08n"
        or feedback.get("transaction") != "buy"
        or feedback.get("itemName") != "상점"
        or feedback.get("moneyBefore") != 60
        or feedback.get("moneyAfter") != 54
        or feedback.get("countBefore") != 1
        or feedback.get("countAfter") != 1
        or feedback.get("shopTransactionSound") != "menuConfirm"
        or not str(feedback.get("shopTransactionSoundSrc") or "").endswith("/extract_wlk/04.wav")
        or feedback.get("shopTransactionSoundPlayed") is not True
        or feedback.get("durationMs") != 1100
        or feedback.get("browserShopTransactionFeedbackImplemented") is not True
        or feedback.get("originalShopRuntimeImplemented") is not False
        or feedback.get("originalShopPriceTableMapped") is not False
        or feedback.get("originalShopInventoryMapped") is not False
        or feedback.get("originalMoneyMutationImplemented") is not False
        or feedback.get("originalStoryFlagRuntimeImplemented") is not False
        or feedback_render.get("active") is not True
        or feedback_render.get("source") != "shop-completion-notice-feedback"
        or feedback_render.get("text") != "상점 거래 완료 2/2"
        or feedback_render.get("map") != "map4_08n"
        or feedback_render.get("transaction") != "buy"
        or feedback_render.get("itemName") != "상점"
        or feedback_render.get("moneyBefore") != 60
        or feedback_render.get("moneyAfter") != 54
        or feedback_render.get("countBefore") != 1
        or feedback_render.get("countAfter") != 1
        or feedback_render.get("shopTransactionSound") != "menuConfirm"
        or not str(feedback_render.get("shopTransactionSoundSrc") or "").endswith("/extract_wlk/04.wav")
        or feedback_render.get("shopTransactionSoundPlayed") is not True
        or feedback_render.get("durationMs") != 1100
        or feedback_render.get("browserShopTransactionFeedbackImplemented") is not True
        or feedback_render.get("originalShopRuntimeImplemented") is not False
        or feedback_render.get("originalShopPriceTableMapped") is not False
        or feedback_render.get("originalShopInventoryMapped") is not False
        or feedback_render.get("originalMoneyMutationImplemented") is not False
        or feedback_render.get("originalStoryFlagRuntimeImplemented") is not False
    ):
        raise WebDriverError(f"shop completion notice feedback is incomplete: {state!r}")


def shop_completion_notice_feedback_summary(state: dict) -> str:
    feedback = state.get("objectiveShopFeedbackLast") or {}
    rendered = state.get("objectiveShopFeedbackLastRender") or {}
    return (
        f"shopNoticeFeedback={feedback.get('source')}:{feedback.get('text')} "
        f"shopNoticeFeedbackRender={rendered.get('active')} "
        f"shopNoticeSound={feedback.get('shopTransactionSound')}:"
        f"{feedback.get('shopTransactionSoundSrc')}:{feedback.get('shopTransactionSoundPlayed')}"
    )


def verify_shop_sound_state(state: dict, container_key: str, top_key: str, label: str) -> None:
    sound_state = (state.get(container_key) or {}).get("soundState") or state.get(top_key) or {}
    counts = sound_state.get("counts") or {}
    log = sound_state.get("log") or []
    last = sound_state.get("last") or {}
    if (
        int(counts.get("item") or 0) < 1
        or last.get("key") != "item"
        or not str(last.get("src") or "").endswith("/extract_wlk/11.wav")
        or not any(isinstance(entry, dict) and entry.get("key") == "item" for entry in log)
    ):
        raise WebDriverError(f"missing {label} WLK item sound state: {sound_state!r}")


def verify_shop_transaction_feedback(
    state: dict,
    key: str,
    *,
    source: str,
    transaction: str,
    item_key: str,
    item_name: str,
    price: int,
    money_before: int,
    money_after: int,
    count_before: int,
    count_after: int,
    text: str,
    rendered: bool = False,
    failed: bool = False,
    failure_reason: str = "",
) -> None:
    entries = shop_transaction_feedback_entries(state, key)
    for entry in entries:
        if (
            entry.get("source") == source
            and entry.get("transaction") == transaction
            and entry.get("failed") is failed
            and entry.get("failureReason") == failure_reason
            and entry.get("itemKey") == item_key
            and entry.get("itemName") == item_name
            and entry.get("itemNameSource") == EXPECTED_ITEM_TEXT["itemNameSource"]
            and entry.get("itemTextTableKey") == EXPECTED_ITEM_TEXT["itemTextTableKey"]
            and entry.get("itemTextTableIndex") == EXPECTED_ITEM_TEXT["itemTextTableIndex"]
            and entry.get("itemTextTableRefVaHex") == EXPECTED_ITEM_TEXT["itemTextTableRefVaHex"]
            and entry.get("itemTextTableTextVaHex") == EXPECTED_ITEM_TEXT["itemTextTableTextVaHex"]
            and entry.get("price") == price
            and entry.get("moneyBefore") == money_before
            and entry.get("moneyAfter") == money_after
            and entry.get("countBefore") == count_before
            and entry.get("countAfter") == count_after
            and entry.get("text") == text
            and entry.get("durationMs") == 1100
            and entry.get("active") is True
            and (
                (
                    failed
                    and entry.get("shopTransactionSound", "") == ""
                    and entry.get("shopTransactionSoundSrc", "") == ""
                    and entry.get("shopTransactionSoundPlayed") is False
                )
                or (
                    not failed
                    and entry.get("shopTransactionSound") == "item"
                    and str(entry.get("shopTransactionSoundSrc") or "").endswith("/extract_wlk/11.wav")
                    and entry.get("shopTransactionSoundPlayed") is True
                )
            )
            and entry.get("browserShopTransactionFeedbackImplemented") is True
            and entry.get("originalShopRuntimeImplemented") is False
            and entry.get("originalShopPriceTableMapped") is False
            and entry.get("originalShopInventoryMapped") is False
            and entry.get("originalMoneyMutationImplemented") 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 shop transaction feedback {key}: {entries!r}")


def verify_shop_failure_state(
    state: dict,
    *,
    label: str,
    failure_reason: str,
    notice: str,
    money: int,
    count: int,
    text: str,
) -> None:
    failure = state.get("purchaseFailure") or {}
    feedback = failure.get("transactionFeedback") or {}
    progress_counts = ((state.get("progress") or {}).get("counts") or {})
    sound_counts = ((state.get("soundState") or {}).get("counts") or {})
    verify_item_text_provenance(failure)
    verify_item_text_provenance(feedback)
    verify_shop_transaction_feedback(
        state,
        "buyFeedbackLog",
        source="shop-buy-failed-feedback",
        transaction="buy",
        item_key="herb",
        item_name="약초",
        price=12,
        money_before=money,
        money_after=money,
        count_before=count,
        count_after=count,
        text=text,
        failed=True,
        failure_reason=failure_reason,
    )
    verify_shop_transaction_feedback(
        state,
        "buyFeedbackRender",
        source="shop-buy-failed-feedback",
        transaction="buy",
        item_key="herb",
        item_name="약초",
        price=12,
        money_before=money,
        money_after=money,
        count_before=count,
        count_after=count,
        text=text,
        rendered=True,
        failed=True,
        failure_reason=failure_reason,
    )
    if (
        state.get("result") is not True
        or state.get("notice") != notice
        or state.get("money") != money
        or state.get("herbCount") != count
        or failure.get("failed") is not True
        or failure.get("failureReason") != failure_reason
        or failure.get("itemKey") != "herb"
        or failure.get("itemName") != "약초"
        or failure.get("price") != 12
        or failure.get("moneyBefore") != money
        or failure.get("moneyAfter") != money
        or failure.get("countBefore") != count
        or failure.get("countAfter") != count
        or failure.get("source") != "prototype-shop-prices"
        or failure.get("originalShopRuntimeImplemented") is not False
        or failure.get("originalShopPriceTableMapped") is not False
        or failure.get("originalShopInventoryMapped") is not False
        or failure.get("originalMoneyMutationImplemented") is not False
        or failure.get("originalStoryFlagRuntimeImplemented") is not False
        or feedback.get("source") != "shop-buy-failed-feedback"
        or feedback.get("text") != text
        or feedback.get("failed") is not True
        or feedback.get("failureReason") != failure_reason
        or int(sound_counts.get("item") or 0) != 0
        or int(progress_counts.get("shop-buy-prototype") or 0) != 0
        or state.get("autoSave") is not None
    ):
        raise WebDriverError(f"{label} shop failure state is incomplete: {state!r}")


def verify_shop_sale_failure_state(
    state: dict,
    *,
    label: str,
    failure_reason: str,
    notice: str,
    money: int,
    count: int,
    text: str,
) -> None:
    failure = state.get("saleFailure") or {}
    feedback = failure.get("transactionFeedback") or {}
    progress_counts = ((state.get("progress") or {}).get("counts") or {})
    sound_counts = ((state.get("soundState") or {}).get("counts") or {})
    verify_item_text_provenance(failure)
    verify_item_text_provenance(feedback)
    verify_shop_transaction_feedback(
        state,
        "sellFeedbackLog",
        source="shop-sell-failed-feedback",
        transaction="sell",
        item_key="herb",
        item_name="약초",
        price=6,
        money_before=money,
        money_after=money,
        count_before=count,
        count_after=count,
        text=text,
        failed=True,
        failure_reason=failure_reason,
    )
    verify_shop_transaction_feedback(
        state,
        "sellFeedbackRender",
        source="shop-sell-failed-feedback",
        transaction="sell",
        item_key="herb",
        item_name="약초",
        price=6,
        money_before=money,
        money_after=money,
        count_before=count,
        count_after=count,
        text=text,
        rendered=True,
        failed=True,
        failure_reason=failure_reason,
    )
    if (
        state.get("result") is not True
        or state.get("notice") != notice
        or state.get("money") != money
        or state.get("herbCount") != count
        or failure.get("failed") is not True
        or failure.get("failureReason") != failure_reason
        or failure.get("itemKey") != "herb"
        or failure.get("itemName") != "약초"
        or failure.get("price") != 6
        or failure.get("moneyBefore") != money
        or failure.get("moneyAfter") != money
        or failure.get("countBefore") != count
        or failure.get("countAfter") != count
        or failure.get("source") != "prototype-shop-prices"
        or failure.get("originalShopRuntimeImplemented") is not False
        or failure.get("originalShopPriceTableMapped") is not False
        or failure.get("originalShopInventoryMapped") is not False
        or failure.get("originalMoneyMutationImplemented") is not False
        or failure.get("originalStoryFlagRuntimeImplemented") is not False
        or feedback.get("source") != "shop-sell-failed-feedback"
        or feedback.get("text") != text
        or feedback.get("failed") is not True
        or feedback.get("failureReason") != failure_reason
        or int(sound_counts.get("item") or 0) != 0
        or int(progress_counts.get("shop-buy-prototype") or 0) != 1
        or int(progress_counts.get("shop-sell-prototype") or 0) != 0
        or state.get("autoSave") is not None
    ):
        raise WebDriverError(f"{label} shop sale failure state is incomplete: {state!r}")


def verify_shop_menu_state(state: dict) -> None:
    main_labels = state.get("mainLabels") or []
    labels = state.get("labels") or []
    lines = "\n".join(state.get("lines") or [])
    shop_menu = state.get("shopMenu") or {}
    rows = shop_menu.get("rows") or []
    first_row = next((row for row in rows if row.get("index") == 0), {})
    names = {row.get("name") for row in rows}
    context_ids = set(shop_menu.get("contextBlockIds") or [])
    ui_ids = set(shop_menu.get("uiBlockIds") or [])
    if (
        "상점 후보 6" in main_labels
        or "상점 구매 6" not in main_labels
        or "상점 후보 설명" not in labels
        or state.get("directShopReviewRows") != 0
        or state.get("openResult") is not True
        or state.get("commandResult") is not True
        or state.get("afterOpenMenuMode") != "shop-buy"
        or state.get("activeId") != "shop-candidates:map4_08n"
        or shop_menu.get("itemCount") != 6
        or shop_menu.get("scope") != "tileset-family"
        or shop_menu.get("source") != "exe-text-table-items-and-event-dialogue-shop-candidates"
        or shop_menu.get("itemTableStartVaHex") != "0x0048b984"
        or shop_menu.get("itemInboundRefCount") != 9
        or shop_menu.get("originalShopRuntimeImplemented") is not False
        or shop_menu.get("prototypeShopPurchaseImplemented") is not True
        or shop_menu.get("prototypeShopSellImplemented") is not True
        or shop_menu.get("originalShopPriceTableMapped") is not False
        or shop_menu.get("originalShopInventoryMapped") is not False
        or shop_menu.get("originalMoneyMutationImplemented") is not False
        or shop_menu.get("originalStoryFlagRuntimeImplemented") is not False
        or not {"약초", "해독초", "리프레시 워터", "마법의 물약", "고급한방약", "마수석"}.issubset(names)
        or not {"event-dialogue-block-032", "event-dialogue-block-038"}.issubset(context_ids)
        or "event-dialogue-block-028" not in ui_ids
        or "상점 후보 6개 (현재 타일셋 계열)" not in lines
        or "EXE item text table과 상점/구입 UI 텍스트 후보를 함께 표시합니다." not in lines
        or "가격표, 판매 목록, 구매/판매 실행은 아직 매핑하지 않았습니다." not in lines
        or "#0 약초 buy 12 sell 6 savedat 0x0015 0x0048b984->0x0048ba7c" not in lines
        or "#5 마수석 buy 90 sell 45 savedat 0x001f 0x0048b9ac->0x0048bad6" not in lines
        or "UI event-dialogue-block-028" not in lines
        or "소지수" not in lines
        or "구입수" not in lines
        or "번 돈" not in lines
        or "event-dialogue-block-032" not in lines
        or "이고 상점과 여관도 있지" not in lines
        or "event-dialogue-block-038" not in lines
        or "도장은 물론 상점 여관도 있지" not in lines
        or "originalShopRuntimeImplemented=False" not in lines
        or "prototypeShopPurchaseImplemented=True" not in lines
        or "prototypeShopSellImplemented=True" not in lines
        or "originalShopPriceTableMapped=False" not in lines
    ):
        raise WebDriverError(f"candidate shop menu state is incomplete: {state!r}")
    verify_item_text_provenance(first_row)


def verify_shop_candidate_menu_selection(state: dict) -> None:
    before = state.get("before") or {}
    marker = state.get("marker") or {}
    choices = marker.get("choices") or []
    selection = state.get("selection") or {}
    action = state.get("action") or {}
    buy_menu = state.get("buyMenu") or {}
    if (
        state.get("commandResult") is not True
        or state.get("selectResult") is not True
        or state.get("map") != "map4_08n"
        or state.get("menuMode") != "shop-buy"
        or state.get("menuOpen") is not True
        or before.get("openIndex", -1) < 0
        or before.get("afterOpenMenuMode") != "shop-candidate"
        or before.get("targetIndex", -1) < 0
        or "상점 선택" not in " ".join(str(label) for label in before.get("labels") or [])
        or "상점 1/2 event-dialogue-block-032" not in " ".join(str(label) for label in before.get("candidateLabels") or [])
        or "상점 2/2 event-dialogue-block-038" not in " ".join(str(label) for label in before.get("candidateLabels") or [])
        or before.get("targetName") != "상점 1/2 event-dialogue-block-032"
        or "구매 약초 12" not in (state.get("labelsAfter") or [])
        or marker.get("source") != "prototype-shop-candidate-menu"
        or marker.get("map") != "map4_08n"
        or marker.get("scope") != "tileset-family"
        or marker.get("count") != 2
        or marker.get("originalShopRuntimeImplemented") is not False
        or marker.get("originalShopPriceTableMapped") is not False
        or marker.get("originalShopInventoryMapped") is not False
        or marker.get("originalMoneyMutationImplemented") is not False
        or marker.get("originalStoryFlagRuntimeImplemented") is not False
        or len(choices) < 2
        or choices[0].get("blockId") != "event-dialogue-block-032"
        or choices[1].get("blockId") != "event-dialogue-block-038"
        or selection.get("source") != "prototype-shop-candidate-menu"
        or selection.get("map") != "map4_08n"
        or selection.get("scope") != "tileset-family"
        or selection.get("blockId") != "event-dialogue-block-032"
        or selection.get("index") != 0
        or selection.get("count") != 2
        or selection.get("itemCount") != 6
        or selection.get("contextCount") != 1
        or selection.get("opensMenuMode") != "shop-buy"
        or selection.get("originalShopRuntimeImplemented") is not False
        or action.get("selectionSource") != "prototype-shop-candidate-menu"
        or action.get("selectedContextBlockId") != "event-dialogue-block-032"
        or action.get("menuCandidateIndex") != 0
        or action.get("menuCandidateCount") != 2
        or action.get("opensMenuMode") != "shop-buy"
        or action.get("originalShopRuntimeImplemented") is not False
        or buy_menu.get("source") != "prototype-shop-prices"
        or buy_menu.get("shopCandidateSource") != "exe-text-table-items-and-event-dialogue-shop-candidates"
        or buy_menu.get("scope") != "tileset-family"
        or buy_menu.get("contextCount") != 1
        or buy_menu.get("contextBlockIds") != ["event-dialogue-block-032"]
        or buy_menu.get("selectionSource") != "prototype-shop-candidate-menu"
        or buy_menu.get("menuCandidateIndex") != 0
        or buy_menu.get("menuCandidateCount") != 2
        or buy_menu.get("itemCount") != 6
        or buy_menu.get("originalShopRuntimeImplemented") is not False
        or buy_menu.get("originalShopPriceTableMapped") is not False
        or buy_menu.get("originalShopInventoryMapped") is not False
        or buy_menu.get("originalMoneyMutationImplemented") is not False
        or buy_menu.get("originalStoryFlagRuntimeImplemented") is not False
    ):
        raise WebDriverError(f"candidate shop selection menu state is incomplete: {state!r}")


def dialogue_gate_vm(dialogue_gate: dict, block_id: str) -> dict:
    return next(
        (
            row
            for row in (dialogue_gate.get("completedVmReplays") or [])
            if isinstance(row, dict) and row.get("blockId") == block_id
        ),
        {},
    )


def verify_shop_dialogue_gate_vm(dialogue_gate: dict) -> None:
    block005 = dialogue_gate_vm(dialogue_gate, "event-dialogue-block-005")
    block026 = dialogue_gate_vm(dialogue_gate, "event-dialogue-block-026")
    if (
        len(dialogue_gate.get("completedVmReplays") or []) < 2
        or block005.get("browserEventVmPartialReplayImplemented") is not True
        or block005.get("browserEventVmFullImplementation") is not False
        or block005.get("renderEventCount") != 2
        or block005.get("vmDrivenLineCount") != 2
        or block005.get("literalTextEventCount") != 0
        or block005.get("firstRenderTextSourceValueHex") != "0x00030000"
        or block026.get("browserEventVmPartialReplayImplemented") is not True
        or block026.get("browserEventVmFullImplementation") is not False
        or block026.get("renderEventCount") != 1
        or block026.get("vmDrivenLineCount") != 1
        or block026.get("literalTextEventCount") != 8
        or block026.get("firstRenderTextSourceValueHex") != "0x00030000"
    ):
        raise WebDriverError(f"shop dialogue gate VM replay evidence is incomplete: {dialogue_gate!r}")


def shop_dialogue_gate_vm_report(dialogue_gate: dict) -> str:
    block005 = dialogue_gate_vm(dialogue_gate, "event-dialogue-block-005")
    block026 = dialogue_gate_vm(dialogue_gate, "event-dialogue-block-026")
    blocks = ",".join(
        (row or {}).get("blockId") or ""
        for row in (dialogue_gate.get("completedVmReplays") or [])
        if isinstance(row, dict)
    )
    return (
        f"vmBlocks={blocks} "
        f"vmPartial={block005.get('browserEventVmPartialReplayImplemented')},{block026.get('browserEventVmPartialReplayImplemented')} "
        f"vmRender={block005.get('renderEventCount')},{block026.get('renderEventCount')} "
        f"vmLiteral={block005.get('literalTextEventCount')},{block026.get('literalTextEventCount')} "
        f"vmSource={block005.get('firstRenderTextSourceValueHex')},{block026.get('firstRenderTextSourceValueHex')}"
    )


def verify_shop_purchase_state(state: dict) -> None:
    before = state.get("before") or {}
    after = state.get("after") or {}
    restored = state.get("restored") or {}
    before_sell = state.get("beforeSell") or {}
    after_sell = state.get("afterSell") or {}
    restored_after_sell = state.get("restoredAfterSell") or {}
    purchase = after.get("purchase") or {}
    sale = after_sell.get("sale") or {}
    buy_auto_save = state.get("autoSave") or after.get("autoSave") or purchase.get("autoSave") or {}
    buy_auto_state = buy_auto_save.get("runtimeState") or {}
    buy_auto_herb = next((item for item in buy_auto_state.get("items") or [] if item.get("key") == "herb"), {})
    buy_auto_progress = buy_auto_save.get("progress") or {}
    buy_auto_tile = buy_auto_save.get("payloadTile") or {}
    sell_auto_save = state.get("autoSaveAfterSell") or after_sell.get("autoSave") or sale.get("autoSave") or {}
    sell_auto_state = sell_auto_save.get("runtimeState") or {}
    sell_auto_herb = next((item for item in sell_auto_state.get("items") or [] if item.get("key") == "herb"), {})
    sell_auto_progress = sell_auto_save.get("progress") or {}
    sell_auto_tile = sell_auto_save.get("payloadTile") or {}
    progress = after.get("progress") or {}
    action_prompt = before.get("actionPrompt") or {}
    shop_action = before.get("shopAction") or {}
    dialogue_gate = before.get("dialogueGate") or {}
    dialogue_gate_before = dialogue_gate.get("beforePrompt") or {}
    dialogue_gate_after = dialogue_gate.get("afterPrompt") or {}
    dialogue_gate_ids = dialogue_gate.get("completedIds") or []
    verify_shop_dialogue_gate_vm(dialogue_gate)
    review_lines = "\n".join((restored_after_sell.get("progressReview") or {}).get("lines") or [])
    verify_item_text_provenance(purchase)
    verify_item_text_provenance(sale)
    verify_item_text_provenance(buy_auto_save)
    verify_item_text_provenance(sell_auto_save)
    verify_item_text_provenance((purchase.get("progressEvent") or {}).get("detail") or {})
    verify_item_text_provenance((sale.get("progressEvent") or {}).get("detail") or {})
    verify_shop_transaction_feedback(
        state,
        "buyFeedbackLog",
        source="shop-buy-feedback",
        transaction="buy",
        item_key="herb",
        item_name="약초",
        price=12,
        money_before=983062,
        money_after=983050,
        count_before=3,
        count_after=4,
        text="약초 +1 / -12",
    )
    verify_shop_transaction_feedback(
        state,
        "buyFeedbackRender",
        source="shop-buy-feedback",
        transaction="buy",
        item_key="herb",
        item_name="약초",
        price=12,
        money_before=983062,
        money_after=983050,
        count_before=3,
        count_after=4,
        text="약초 +1 / -12",
        rendered=True,
    )
    verify_shop_transaction_feedback(
        state,
        "sellFeedbackLog",
        source="shop-sell-feedback",
        transaction="sell",
        item_key="herb",
        item_name="약초",
        price=6,
        money_before=983050,
        money_after=983056,
        count_before=4,
        count_after=3,
        text="약초 -1 / +6",
    )
    verify_shop_transaction_feedback(
        state,
        "sellFeedbackRender",
        source="shop-sell-feedback",
        transaction="sell",
        item_key="herb",
        item_name="약초",
        price=6,
        money_before=983050,
        money_after=983056,
        count_before=4,
        count_after=3,
        text="약초 -1 / +6",
        rendered=True,
    )
    verify_shop_failure_state(
        state.get("buyInsufficientFailure") or {},
        label="insufficient-money",
        failure_reason="insufficient-money",
        notice="약초 구입에 12 필요",
        money=5,
        count=3,
        text="약초 돈 부족 5/12",
    )
    verify_shop_failure_state(
        state.get("buyFullFailure") or {},
        label="inventory-full",
        failure_reason="inventory-full",
        notice="약초 소지수가 가득 찼습니다.",
        money=983062,
        count=99,
        text="약초 소지수 가득",
    )
    verify_shop_sale_failure_state(
        state.get("sellEmptyFailure") or {},
        label="empty-stock",
        failure_reason="empty-stock",
        notice="약초 판매할 수량이 없습니다.",
        money=983050,
        count=0,
        text="약초 판매 없음",
    )
    verify_shop_sound_state(state, "after", "buySoundState", "shop buy")
    verify_shop_sound_state(state, "afterSell", "sellSoundState", "shop sell")
    if (
        state.get("openResult") is not True
        or state.get("buyResult") is not True
        or state.get("saved") is not True
        or buy_auto_save.get("saved") is not True
        or buy_auto_save.get("source") != "shop-buy-prototype"
        or buy_auto_save.get("payloadMap") != "map4_08n"
        or buy_auto_tile.get("x") != 18
        or buy_auto_tile.get("y") != 42
        or buy_auto_state.get("money") != 983050
        or buy_auto_herb.get("count") != 4
        or (buy_auto_progress.get("counts") or {}).get("shop-buy-prototype") != 1
        or buy_auto_save.get("originalShopRuntimeImplemented") is not False
        or buy_auto_save.get("originalShopPriceTableMapped") is not False
        or buy_auto_save.get("originalShopInventoryMapped") is not False
        or buy_auto_save.get("originalMoneyMutationImplemented") is not False
        or state.get("loaded") is not True
        or state.get("openSellResult") is not True
        or state.get("sellResult") is not True
        or state.get("savedAfterSell") is not True
        or sell_auto_save.get("saved") is not True
        or sell_auto_save.get("source") != "shop-sell-prototype"
        or sell_auto_save.get("payloadMap") != "map4_08n"
        or sell_auto_tile.get("x") != 18
        or sell_auto_tile.get("y") != 42
        or sell_auto_state.get("money") != 983056
        or sell_auto_herb.get("count") != 3
        or (sell_auto_progress.get("counts") or {}).get("shop-buy-prototype") != 1
        or (sell_auto_progress.get("counts") or {}).get("shop-sell-prototype") != 1
        or sell_auto_save.get("originalShopRuntimeImplemented") is not False
        or sell_auto_save.get("originalShopPriceTableMapped") is not False
        or sell_auto_save.get("originalShopInventoryMapped") is not False
        or sell_auto_save.get("originalMoneyMutationImplemented") is not False
	    or state.get("loadedAfterSell") is not True
	    or dialogue_gate_before.get("kind") != "dialogue-candidate"
	    or dialogue_gate_before.get("text") != "Enter -> 대사 1/2"
	    or dialogue_gate_before.get("blockId") != "event-dialogue-block-005"
	    or "event-dialogue-block-005" not in dialogue_gate_ids
	    or "event-dialogue-block-026" not in dialogue_gate_ids
	    or dialogue_gate.get("dialogueCompleteCount") != 2
	    or dialogue_gate_after.get("kind") != "shop-candidate"
	    or dialogue_gate_after.get("text") != "Enter -> 상점 6"
	    or action_prompt.get("kind") != "shop-candidate"
	    or action_prompt.get("text") != "Enter -> 상점 6"
        or action_prompt.get("scope") != "tileset-family"
        or action_prompt.get("count") != 6
        or action_prompt.get("opensMenuMode") != "shop-buy"
        or action_prompt.get("originalShopRuntimeImplemented") is not False
        or before.get("canvasPointer") is not True
        or (before.get("promptRect") or {}).get("width", 0) <= 0
        or (before.get("promptRect") or {}).get("height", 0) <= 0
        or shop_action.get("scope") != "tileset-family"
        or shop_action.get("count") != 6
        or shop_action.get("opensMenuMode") != "shop-buy"
        or shop_action.get("originalShopRuntimeImplemented") is not False
        or before.get("mode") != "shop-buy"
        or "구매 약초 12" not in (before.get("labels") or [])
        or before.get("money") != 983062
        or before.get("herbCount") != 3
        or after.get("money") != 983050
        or after.get("herbCount") != 4
        or after.get("mode") != "shop-buy"
        or "약초 구입 4개 소지금 983050" != after.get("notice")
        or purchase.get("itemKey") != "herb"
        or purchase.get("itemName") != "약초"
        or purchase.get("price") != 12
        or purchase.get("moneyBefore") != 983062
        or purchase.get("moneyAfter") != 983050
        or purchase.get("countBefore") != 3
        or purchase.get("countAfter") != 4
        or purchase.get("source") != "prototype-shop-prices"
        or purchase.get("originalShopRuntimeImplemented") is not False
        or purchase.get("originalShopPriceTableMapped") is not False
        or purchase.get("originalShopInventoryMapped") is not False
        or purchase.get("originalMoneyMutationImplemented") is not False
        or progress.get("lastEvent", {}).get("kind") != "shop-buy-prototype"
        or restored.get("map") != "map4_08n"
        or restored.get("money") != 983050
        or restored.get("herbCount") != 4
        or restored.get("progressCount") != 1
        or restored.get("loadedSummaryMoney") != 983050
        or restored.get("loadedSummaryHerbCount") != 4
        or before_sell.get("mode") != "shop-sell"
        or not any("판매 약초 6" in label for label in (before_sell.get("labels") or []))
        or before_sell.get("money") != 983050
        or before_sell.get("herbCount") != 4
        or after_sell.get("money") != 983056
        or after_sell.get("herbCount") != 3
        or after_sell.get("mode") != "shop-sell"
        or "약초 판매 3개 소지금 983056" != after_sell.get("notice")
        or sale.get("itemKey") != "herb"
        or sale.get("itemName") != "약초"
        or sale.get("price") != 6
        or sale.get("moneyBefore") != 983050
        or sale.get("moneyAfter") != 983056
        or sale.get("countBefore") != 4
        or sale.get("countAfter") != 3
        or sale.get("source") != "prototype-shop-prices"
        or sale.get("originalShopRuntimeImplemented") is not False
        or sale.get("originalShopPriceTableMapped") is not False
        or sale.get("originalShopInventoryMapped") is not False
        or sale.get("originalMoneyMutationImplemented") is not False
        or restored_after_sell.get("map") != "map4_08n"
        or restored_after_sell.get("money") != 983056
        or restored_after_sell.get("herbCount") != 3
        or restored_after_sell.get("buyProgressCount") != 1
        or restored_after_sell.get("sellProgressCount") != 1
        or restored_after_sell.get("loadedSummaryMoney") != 983056
        or restored_after_sell.get("loadedSummaryHerbCount") != 3
        or "상점 구매" not in review_lines
        or "상점 판매" not in review_lines
        or "shop-buy:herb" not in review_lines
        or "shop-sell:herb" not in review_lines
        or "price 12" not in review_lines
        or "price 6" not in review_lines
        or "약초" not in review_lines
    ):
        raise WebDriverError(f"candidate shop purchase state is incomplete: {state!r}")


def verify_new_game_shop_state(state: dict) -> None:
    before = state.get("before") or {}
    after = state.get("after") or {}
    restored = state.get("restored") or {}
    before_sell = state.get("beforeSell") or {}
    after_sell = state.get("afterSell") or {}
    restored_after_sell = state.get("restoredAfterSell") or {}
    purchase = after.get("purchase") or {}
    sale = after_sell.get("sale") or {}
    buy_auto_save = state.get("autoSave") or after.get("autoSave") or purchase.get("autoSave") or {}
    buy_auto_state = buy_auto_save.get("runtimeState") or {}
    buy_auto_herb = next((item for item in buy_auto_state.get("items") or [] if item.get("key") == "herb"), {})
    buy_auto_progress = buy_auto_save.get("progress") or {}
    buy_auto_tile = buy_auto_save.get("payloadTile") or {}
    sell_auto_save = state.get("autoSaveAfterSell") or after_sell.get("autoSave") or sale.get("autoSave") or {}
    sell_auto_state = sell_auto_save.get("runtimeState") or {}
    sell_auto_herb = next((item for item in sell_auto_state.get("items") or [] if item.get("key") == "herb"), {})
    sell_auto_progress = sell_auto_save.get("progress") or {}
    sell_auto_tile = sell_auto_save.get("payloadTile") or {}
    action_prompt = before.get("actionPrompt") or {}
    shop_action = before.get("shopAction") or {}
    dialogue_gate = before.get("dialogueGate") or {}
    dialogue_gate_before = dialogue_gate.get("beforePrompt") or {}
    dialogue_gate_after = dialogue_gate.get("afterPrompt") or {}
    dialogue_gate_ids = dialogue_gate.get("completedIds") or []
    verify_shop_dialogue_gate_vm(dialogue_gate)
    review_lines = "\n".join((restored_after_sell.get("progressReview") or {}).get("lines") or [])
    verify_item_text_provenance(purchase)
    verify_item_text_provenance(sale)
    verify_item_text_provenance(buy_auto_save)
    verify_item_text_provenance(sell_auto_save)
    verify_item_text_provenance((purchase.get("progressEvent") or {}).get("detail") or {})
    verify_item_text_provenance((sale.get("progressEvent") or {}).get("detail") or {})
    verify_shop_transaction_feedback(
        state,
        "buyFeedbackLog",
        source="shop-buy-feedback",
        transaction="buy",
        item_key="herb",
        item_name="약초",
        price=12,
        money_before=60,
        money_after=48,
        count_before=1,
        count_after=2,
        text="약초 +1 / -12",
    )
    verify_shop_transaction_feedback(
        state,
        "buyFeedbackRender",
        source="shop-buy-feedback",
        transaction="buy",
        item_key="herb",
        item_name="약초",
        price=12,
        money_before=60,
        money_after=48,
        count_before=1,
        count_after=2,
        text="약초 +1 / -12",
        rendered=True,
    )
    verify_shop_transaction_feedback(
        state,
        "sellFeedbackLog",
        source="shop-sell-feedback",
        transaction="sell",
        item_key="herb",
        item_name="약초",
        price=6,
        money_before=48,
        money_after=54,
        count_before=2,
        count_after=1,
        text="약초 -1 / +6",
    )
    verify_shop_transaction_feedback(
        state,
        "sellFeedbackRender",
        source="shop-sell-feedback",
        transaction="sell",
        item_key="herb",
        item_name="약초",
        price=6,
        money_before=48,
        money_after=54,
        count_before=2,
        count_after=1,
        text="약초 -1 / +6",
        rendered=True,
    )
    verify_shop_sound_state(state, "after", "buySoundState", "new-game shop buy")
    verify_shop_sound_state(state, "afterSell", "sellSoundState", "new-game shop sell")
    if (
        state.get("openResult") is not True
        or state.get("buyResult") is not True
        or state.get("saved") is not True
        or buy_auto_save.get("saved") is not True
        or buy_auto_save.get("source") != "shop-buy-prototype"
        or buy_auto_save.get("payloadMap") != "map4_08n"
        or buy_auto_tile.get("x") != 18
        or buy_auto_tile.get("y") != 42
        or buy_auto_state.get("money") != 48
        or buy_auto_herb.get("count") != 2
        or (buy_auto_progress.get("counts") or {}).get("shop-buy-prototype") != 1
        or buy_auto_save.get("originalShopRuntimeImplemented") is not False
        or buy_auto_save.get("originalShopPriceTableMapped") is not False
        or buy_auto_save.get("originalShopInventoryMapped") is not False
        or buy_auto_save.get("originalMoneyMutationImplemented") is not False
        or state.get("loaded") is not True
        or state.get("openSellResult") is not True
        or state.get("sellResult") is not True
        or state.get("savedAfterSell") is not True
        or sell_auto_save.get("saved") is not True
        or sell_auto_save.get("source") != "shop-sell-prototype"
        or sell_auto_save.get("payloadMap") != "map4_08n"
        or sell_auto_tile.get("x") != 18
        or sell_auto_tile.get("y") != 42
        or sell_auto_state.get("money") != 54
        or sell_auto_herb.get("count") != 1
        or (sell_auto_progress.get("counts") or {}).get("shop-buy-prototype") != 1
        or (sell_auto_progress.get("counts") or {}).get("shop-sell-prototype") != 1
        or sell_auto_save.get("originalShopRuntimeImplemented") is not False
        or sell_auto_save.get("originalShopPriceTableMapped") is not False
        or sell_auto_save.get("originalShopInventoryMapped") is not False
	    or sell_auto_save.get("originalMoneyMutationImplemented") is not False
	    or state.get("loadedAfterSell") is not True
	    or dialogue_gate_before.get("kind") != "dialogue-candidate"
	    or dialogue_gate_before.get("text") != "Enter -> 대사 1/2"
	    or dialogue_gate_before.get("blockId") != "event-dialogue-block-005"
	    or "event-dialogue-block-005" not in dialogue_gate_ids
	    or "event-dialogue-block-026" not in dialogue_gate_ids
	    or dialogue_gate.get("dialogueCompleteCount") != 2
	    or dialogue_gate_after.get("kind") != "shop-candidate"
	    or dialogue_gate_after.get("text") != "Enter -> 상점 6"
	    or action_prompt.get("kind") != "shop-candidate"
	    or action_prompt.get("text") != "Enter -> 상점 6"
        or action_prompt.get("scope") != "tileset-family"
        or action_prompt.get("opensMenuMode") != "shop-buy"
        or action_prompt.get("originalShopRuntimeImplemented") is not False
        or before.get("canvasPointer") is not True
        or (before.get("promptRect") or {}).get("width", 0) <= 0
        or (before.get("promptRect") or {}).get("height", 0) <= 0
        or before.get("loadedSaveSummary") is not None
        or shop_action.get("scope") != "tileset-family"
        or shop_action.get("count") != 6
        or shop_action.get("opensMenuMode") != "shop-buy"
        or before.get("money") != 60
        or before.get("herbCount") != 1
        or "구매 약초 12" not in (before.get("labels") or [])
        or after.get("money") != 48
        or after.get("herbCount") != 2
        or after.get("notice") != "약초 구입 2개 소지금 48"
        or after.get("loadedSaveSummary") is not None
        or purchase.get("itemKey") != "herb"
        or purchase.get("itemName") != "약초"
        or purchase.get("price") != 12
        or purchase.get("moneyBefore") != 60
        or purchase.get("moneyAfter") != 48
        or purchase.get("countBefore") != 1
        or purchase.get("countAfter") != 2
        or purchase.get("source") != "prototype-shop-prices"
        or purchase.get("originalShopRuntimeImplemented") is not False
        or purchase.get("originalShopPriceTableMapped") is not False
        or purchase.get("originalShopInventoryMapped") is not False
        or purchase.get("originalMoneyMutationImplemented") is not False
        or restored.get("loaded") is not True
        or restored.get("map") != "map4_08n"
        or restored.get("money") != 48
        or restored.get("herbCount") != 2
        or restored.get("buyProgressCount") != 1
        or restored.get("loadedSaveSummary") is not None
        or before_sell.get("money") != 48
        or before_sell.get("herbCount") != 2
        or not any("판매 약초 6" in label for label in (before_sell.get("labels") or []))
        or after_sell.get("money") != 54
        or after_sell.get("herbCount") != 1
        or after_sell.get("notice") != "약초 판매 1개 소지금 54"
        or sale.get("itemKey") != "herb"
        or sale.get("itemName") != "약초"
        or sale.get("price") != 6
        or sale.get("moneyBefore") != 48
        or sale.get("moneyAfter") != 54
        or sale.get("countBefore") != 2
        or sale.get("countAfter") != 1
        or sale.get("source") != "prototype-shop-prices"
        or sale.get("originalShopRuntimeImplemented") is not False
        or sale.get("originalShopPriceTableMapped") is not False
        or sale.get("originalShopInventoryMapped") is not False
        or sale.get("originalMoneyMutationImplemented") is not False
        or restored_after_sell.get("map") != "map4_08n"
        or restored_after_sell.get("money") != 54
        or restored_after_sell.get("herbCount") != 1
        or restored_after_sell.get("buyProgressCount") != 1
        or restored_after_sell.get("sellProgressCount") != 1
        or restored_after_sell.get("loadedSaveSummary") is not None
        or "상점 구매" not in review_lines
        or "상점 판매" not in review_lines
        or "shop-buy:herb" not in review_lines
        or "shop-sell:herb" not in review_lines
        or "price 12" not in review_lines
        or "price 6" not in review_lines
        or "약초" not in review_lines
    ):
        raise WebDriverError(f"new-game shop state is incomplete: {state!r}")


def verify_new_game_shop_title_continue(title_state: dict, title_click: dict, state: dict) -> None:
    title_labels = [str(label) for label in (title_state.get("titleMenuLabels") or [])]
    progress_lines = "\n".join((state.get("progressReview") or {}).get("lines") or [])
    saved_payload = state.get("savedPayload") or {}
    payload_state = saved_payload.get("runtimeState") or {}
    payload_herb = next((item for item in payload_state.get("items") or [] if item.get("key") == "herb"), {})
    if (
        title_state.get("scene") != "title"
        or title_state.get("quickLoadText") != "이어하기"
        or "continue" not in (title_state.get("titleMenuKeys") or [])
        or not any("이어하기 map4_08n 18,42" in label for label in title_labels)
        or title_click.get("ok") is not True
        or state.get("titleContinue") is not True
        or state.get("loaded") is not True
        or state.get("scene") != "map"
        or state.get("map") != "map4_08n"
        or state.get("loadedSaveSummary") is not None
        or state.get("money") != 54
        or state.get("herbCount") != 1
        or state.get("buyProgressCount") != 1
        or state.get("sellProgressCount") != 1
        or saved_payload.get("map") != "map4_08n"
        or saved_payload.get("loadedSaveSummary") is not None
        or payload_state.get("money") != 54
        or payload_herb.get("count") != 1
        or state.get("quickLoadText") != "임시 불러오기"
        or "상점 구매" not in progress_lines
        or "상점 판매" not in progress_lines
        or "shop-buy:herb" not in progress_lines
        or "shop-sell:herb" not in progress_lines
        or "price 12" not in progress_lines
        or "price 6" not in progress_lines
        or "약초" not in progress_lines
    ):
        raise WebDriverError(
            f"unexpected new-game shop title continue state: title={title_state!r} "
            f"click={title_click!r} restore={state!r}"
        )


def verify_shop_completion_objective(state: dict) -> None:
    objective_before = state.get("objectiveBefore") or {}
    objective_action = state.get("objectiveAction") or {}
    objective_notice = state.get("objectiveCompletionNotice") or {}
    objective_dialogue = state.get("objectiveActiveDialogueBlock") or {}
    notice_lines = " ".join(objective_notice.get("lines") or [])
    completion = objective_notice.get("completion") or {}
    if (
        state.get("objectiveResult") is not True
        or state.get("scene") != "map"
        or state.get("mapName") != "map4_08n"
        or state.get("money") != 54
        or "herb:1" not in (state.get("runtimeItems") or [])
        or objective_before.get("title") != "후보 상점 완료 map4_08n"
        or objective_before.get("nextAction") != "완료 알림 확인"
        or objective_before.get("detail") != "구매 약초 / 판매 약초"
        or objective_before.get("source") != "prototype-shop-completion"
        or objective_action.get("action") != "shop-completion-notice"
        or objective_action.get("activeId") != "shop-complete:map4_08n"
        or objective_action.get("prototypeShopPurchaseImplemented") is not True
        or objective_action.get("prototypeShopSellImplemented") is not True
        or objective_action.get("originalShopRuntimeImplemented") is not False
        or objective_action.get("originalShopPriceTableMapped") is not False
        or objective_action.get("originalShopInventoryMapped") is not False
        or objective_action.get("originalMoneyMutationImplemented") is not False
        or objective_action.get("originalStoryFlagRuntimeImplemented") is not False
        or objective_notice.get("blockId") != "shop-complete:map4_08n"
        or objective_notice.get("map") != "map4_08n"
        or "상점 거래 완료 2/2" not in notice_lines
        or "구매 약초 1 -> 2 / 소지금 60 -> 48" not in notice_lines
        or "판매 약초 2 -> 1 / 소지금 48 -> 54" not in notice_lines
        or completion.get("completed") is not True
        or completion.get("completedCount") != 2
        or completion.get("buyEventCount") != 1
        or completion.get("sellEventCount") != 1
        or completion.get("buyItemName") != "약초"
        or completion.get("sellItemName") != "약초"
        or completion.get("buyMoneyBefore") != 60
        or completion.get("buyMoneyAfter") != 48
        or completion.get("sellMoneyBefore") != 48
        or completion.get("sellMoneyAfter") != 54
        or completion.get("source") != "prototype-shop-prices"
        or completion.get("prototypeShopPurchaseImplemented") is not True
        or completion.get("prototypeShopSellImplemented") is not True
        or completion.get("originalShopRuntimeImplemented") is not False
        or completion.get("originalShopPriceTableMapped") is not False
        or completion.get("originalShopInventoryMapped") is not False
        or completion.get("originalMoneyMutationImplemented") is not False
        or completion.get("originalStoryFlagRuntimeImplemented") is not False
        or objective_notice.get("prototypeShopPurchaseImplemented") is not True
        or objective_notice.get("prototypeShopSellImplemented") is not True
        or objective_notice.get("originalShopRuntimeImplemented") is not False
        or objective_notice.get("originalShopPriceTableMapped") is not False
        or objective_notice.get("originalShopInventoryMapped") is not False
        or objective_notice.get("originalMoneyMutationImplemented") is not False
        or objective_notice.get("originalStoryFlagRuntimeImplemented") is not False
        or objective_dialogue.get("blockId") != "shop-complete:map4_08n"
        or objective_dialogue.get("line") != "상점 거래 완료 2/2"
        or objective_dialogue.get("lineCount", 0) < 5
    ):
        raise WebDriverError(f"unexpected shop completion objective state: {state!r}")
    verify_shop_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_shop_menu_browser_smoke.json").write_text(
        json.dumps(report, ensure_ascii=False, separators=(",", ":")) + "\n",
        encoding="utf-8",
    )
    lines = [
        "# Candidate Shop Menu Browser Smoke",
        "",
        f"- status: `{report.get('status')}`",
        f"- shop menu: `{report.get('shopMenu')}`",
        f"- shop candidate menu: `{report.get('shopCandidateMenu')}`",
        f"- shop action: `{report.get('shopAction')}`",
        f"- shop purchase: `{report.get('shopPurchase')}`",
        f"- shop sale: `{report.get('shopSale')}`",
        f"- new game shop: `{report.get('newGameShop')}`",
        f"- new game shop objective: `{report.get('newGameShopObjective')}`",
        f"- new game shop title continue: `{report.get('newGameShopTitleContinue')}`",
        f"- new game shop title objective: `{report.get('newGameShopTitleObjective')}`",
        "",
    ]


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_shop_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": "map4_08n",
                    "startTile": "18,42",
                },
            )
            execute_js(port, session_id, shop_menu_start_script(), timeout=3)
            state = wait_for_shop_menu_state(port, session_id)
            verify_shop_menu_state(state)
            shop_menu = state.get("shopMenu") or {}
            names = ",".join(row.get("name") or "" for row in (shop_menu.get("rows") or [])[:4])
            context_ids = ",".join(shop_menu.get("contextBlockIds") or [])
            execute_js(port, session_id, shop_candidate_menu_selection_script(), timeout=3)
            shop_candidate_menu_state = wait_for_shop_candidate_menu_selection(port, session_id)
            verify_shop_candidate_menu_selection(shop_candidate_menu_state)
            url_purchase = load_map(
                base,
                port,
                session_id,
                {
                    "map": "map2_02d",
                    "startTile": "11,12",
                    "publicSave": "flack3r-savedat2",
                },
            )
            execute_js(port, session_id, shop_purchase_start_script(), timeout=3)
            purchase_state = wait_for_shop_purchase_state(port, session_id)
            verify_shop_purchase_state(purchase_state)
            url_new_game_shop = load_map(
                base,
                port,
                session_id,
                {
                    "map": "map4_08n",
                    "startTile": "18,42",
                },
            )
            execute_js(port, session_id, "localStorage.removeItem(RUNTIME_SAVE_KEY); return true;", timeout=3)
            execute_js(port, session_id, new_game_shop_start_script(), timeout=3)
            new_game_shop_state = wait_for_new_game_shop_state(port, session_id)
            verify_new_game_shop_state(new_game_shop_state)
            new_game_shop_objective = execute_js(
                port,
                session_id,
                shop_completion_objective_capture_script(),
                timeout=3,
            )
            if not isinstance(new_game_shop_objective, dict):
                raise WebDriverError(f"new-game shop objective capture failed: {new_game_shop_objective!r}")
            verify_shop_completion_objective(new_game_shop_objective)
            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_new_game_shop_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"new-game shop title continue button was not usable: {title_click!r}")
            wait_for_map_runtime(port, session_id, "map4_08n")
            execute_js(port, session_id, capture_new_game_shop_after_title_continue_script(), timeout=3)
            new_game_shop_title_state = wait_for_new_game_shop_title_restore_state(port, session_id)
            verify_new_game_shop_title_continue(title_state, title_click, new_game_shop_title_state)
            new_game_shop_title_objective = execute_js(
                port,
                session_id,
                shop_completion_objective_capture_script(),
                timeout=3,
            )
            if not isinstance(new_game_shop_title_objective, dict):
                raise WebDriverError(f"new-game shop title objective capture failed: {new_game_shop_title_objective!r}")
            verify_shop_completion_objective(new_game_shop_title_objective)
            report = {
                "status": "passed",
                "url": url,
                "purchaseUrl": url_purchase,
                "newGameShopUrl": url_new_game_shop,
                "titleUrl": title_url,
                "shopMenu": (
                    f"label=상점 후보 설명 via=상점 구매 6 directShopReviewRows={state.get('directShopReviewRows', 0) > 0} active=shop-candidates:map4_08n "
                    f"source={shop_menu.get('source')} "
                    f"table={shop_menu.get('itemTableStartVaHex')} "
                    "itemTextTable=items "
                    "itemTextRef=0x0048b984 "
                    "itemTextVa=0x0048ba7c "
                    f"scope={shop_menu.get('scope')} "
                    f"items={names} "
                    f"contexts={context_ids} "
                    "originalShopRuntimeImplemented=False "
                    "prototypeShopPurchaseImplemented=True "
                    "prototypeShopSellImplemented=True "
                    "originalShopPriceTableMapped=False "
                    "originalShopInventoryMapped=False "
                    "originalStoryFlagRuntimeImplemented=False"
                ),
                "shopCandidateMenu": (
                    f"menuCount={(shop_candidate_menu_state.get('marker') or {}).get('count')} "
                    f"selected={(shop_candidate_menu_state.get('selection') or {}).get('blockId')} "
                    f"scope={(shop_candidate_menu_state.get('selection') or {}).get('scope')} "
                    f"itemCount={(shop_candidate_menu_state.get('selection') or {}).get('itemCount')} "
                    f"contextCount={(shop_candidate_menu_state.get('selection') or {}).get('contextCount')} "
                    f"selectionSource={(shop_candidate_menu_state.get('selection') or {}).get('source')} "
                    f"commandResult={shop_candidate_menu_state.get('commandResult')} "
                    f"selectResult={shop_candidate_menu_state.get('selectResult')} "
                    f"afterMenuMode={shop_candidate_menu_state.get('menuMode')} "
                    f"opensMenuMode={(shop_candidate_menu_state.get('selection') or {}).get('opensMenuMode')} "
                    f"buyMenuSource={(shop_candidate_menu_state.get('buyMenu') or {}).get('source')} "
                    f"shopCandidateSource={(shop_candidate_menu_state.get('buyMenu') or {}).get('shopCandidateSource')} "
                    "originalShopRuntimeImplemented=False originalShopPriceTableMapped=False "
                    "originalShopInventoryMapped=False originalMoneyMutationImplemented=False "
                    "originalStoryFlagRuntimeImplemented=False"
                ),
                "shopAction": (
                    "dialogueGateBeforeShop=True "
                    f"firstPrompt={(((purchase_state.get('before') or {}).get('dialogueGate') or {}).get('beforePrompt') or {}).get('text')} "
                    f"firstBlock={(((purchase_state.get('before') or {}).get('dialogueGate') or {}).get('beforePrompt') or {}).get('blockId')} "
                    f"completedDialogue={','.join((((purchase_state.get('before') or {}).get('dialogueGate') or {}).get('completedIds') or []))} "
                    f"dialogueCompleteCount={(((purchase_state.get('before') or {}).get('dialogueGate') or {}).get('dialogueCompleteCount'))} "
                    f"{shop_dialogue_gate_vm_report(((purchase_state.get('before') or {}).get('dialogueGate') or {}))} "
                    "prompt=Enter -> 상점 6 kind=shop-candidate scope=tileset-family "
                    "opensMenuMode=shop-buy canvasPointer=True originalShopRuntimeImplemented=False "
                    "originalShopPriceTableMapped=False"
                ),
                "shopPurchase": (
                    "item=약초 price=12 moneyBefore=983062 moneyAfter=983050 "
                    "countBefore=3 countAfter=4 restoredMoney=983050 restoredCount=4 "
                    f"autoSaved={(purchase_state.get('autoSave') or {}).get('saved')} "
                    f"autoSource={(purchase_state.get('autoSave') or {}).get('source')} "
                    "itemTextTable=items "
                    "itemTextRef=0x0048b984 "
                    "itemTextVa=0x0048ba7c "
                    f"buyFeedback={shop_transaction_feedback_summary(purchase_state, 'buyFeedbackLog')} "
                    f"buyFeedbackRender={shop_transaction_feedback_rendered(purchase_state, 'buyFeedbackRender')} "
                    f"buyFailFeedback={shop_transaction_feedback_summary(purchase_state.get('buyInsufficientFailure') or {}, 'buyFeedbackLog')} "
                    f"buyFailFeedbackRender={shop_transaction_feedback_rendered(purchase_state.get('buyInsufficientFailure') or {}, 'buyFeedbackRender')} "
                    f"buyFullFeedback={shop_transaction_feedback_summary(purchase_state.get('buyFullFailure') or {}, 'buyFeedbackLog')} "
                    f"buyFullFeedbackRender={shop_transaction_feedback_rendered(purchase_state.get('buyFullFailure') or {}, 'buyFeedbackRender')} "
                    f"buySound={((purchase_state.get('buyFeedbackLast') or {}).get('shopTransactionSound') or '')} "
                    f"buySoundSrc={((purchase_state.get('buyFeedbackLast') or {}).get('shopTransactionSoundSrc') or '')} "
                    f"buySoundPlayed={((purchase_state.get('buyFeedbackLast') or {}).get('shopTransactionSoundPlayed'))} "
                    f"buySoundItemCount={((((purchase_state.get('after') or {}).get('soundState') or {}).get('counts') or {}).get('item'))} "
                    "progressCount=1 source=prototype-shop-prices "
                    "originalShopRuntimeImplemented=False "
                    "originalShopPriceTableMapped=False "
                    "originalShopInventoryMapped=False "
                    "originalMoneyMutationImplemented=False"
                ),
                "shopSale": (
                    "item=약초 price=6 moneyBefore=983050 moneyAfter=983056 "
                    "countBefore=4 countAfter=3 restoredMoney=983056 restoredCount=3 "
                    f"autoSaved={(purchase_state.get('autoSaveAfterSell') or {}).get('saved')} "
                    f"autoSource={(purchase_state.get('autoSaveAfterSell') or {}).get('source')} "
                    "itemTextTable=items "
                    "itemTextRef=0x0048b984 "
	                    "itemTextVa=0x0048ba7c "
	                    f"sellFeedback={shop_transaction_feedback_summary(purchase_state, 'sellFeedbackLog')} "
	                    f"sellFeedbackRender={shop_transaction_feedback_rendered(purchase_state, 'sellFeedbackRender')} "
	                    f"sellFailFeedback={shop_transaction_feedback_summary(purchase_state.get('sellEmptyFailure') or {}, 'sellFeedbackLog')} "
	                    f"sellFailFeedbackRender={shop_transaction_feedback_rendered(purchase_state.get('sellEmptyFailure') or {}, 'sellFeedbackRender')} "
	                    f"sellSound={((purchase_state.get('sellFeedbackLast') or {}).get('shopTransactionSound') or '')} "
	                    f"sellSoundSrc={((purchase_state.get('sellFeedbackLast') or {}).get('shopTransactionSoundSrc') or '')} "
	                    f"sellSoundPlayed={((purchase_state.get('sellFeedbackLast') or {}).get('shopTransactionSoundPlayed'))} "
	                    f"sellSoundItemCount={((((purchase_state.get('afterSell') or {}).get('soundState') or {}).get('counts') or {}).get('item'))} "
	                    "buyProgressCount=1 sellProgressCount=1 source=prototype-shop-prices "
                    "originalShopRuntimeImplemented=False "
                    "originalShopPriceTableMapped=False "
                    "originalShopInventoryMapped=False "
                    "originalMoneyMutationImplemented=False"
                ),
                "newGameShop": (
                    "dialogueGateBeforeShop=True "
                    f"firstPrompt={(((new_game_shop_state.get('before') or {}).get('dialogueGate') or {}).get('beforePrompt') or {}).get('text')} "
                    f"firstBlock={(((new_game_shop_state.get('before') or {}).get('dialogueGate') or {}).get('beforePrompt') or {}).get('blockId')} "
                    f"completedDialogue={','.join((((new_game_shop_state.get('before') or {}).get('dialogueGate') or {}).get('completedIds') or []))} "
                    f"{shop_dialogue_gate_vm_report(((new_game_shop_state.get('before') or {}).get('dialogueGate') or {}))} "
                    "item=약초 price=12 moneyBefore=60 moneyAfter=48 "
                    "countBefore=1 countAfter=2 restoredMoney=48 restoredCount=2 "
                    "sellPrice=6 sellMoneyAfter=54 sellCountAfter=1 "
                    "restoredSellMoney=54 restoredSellCount=1 "
                    "buyProgressCount=1 sellProgressCount=1 "
                    f"buyAutoSource={(new_game_shop_state.get('autoSave') or {}).get('source')} "
                    f"sellAutoSource={(new_game_shop_state.get('autoSaveAfterSell') or {}).get('source')} "
                    "itemTextTable=items "
                    "itemTextRef=0x0048b984 "
                    "itemTextVa=0x0048ba7c "
                    f"buyFeedback={shop_transaction_feedback_summary(new_game_shop_state, 'buyFeedbackLog')} "
                    f"buyFeedbackRender={shop_transaction_feedback_rendered(new_game_shop_state, 'buyFeedbackRender')} "
                    f"sellFeedback={shop_transaction_feedback_summary(new_game_shop_state, 'sellFeedbackLog')} "
                    f"sellFeedbackRender={shop_transaction_feedback_rendered(new_game_shop_state, 'sellFeedbackRender')} "
                    f"buySound={((new_game_shop_state.get('buyFeedbackLast') or {}).get('shopTransactionSound') or '')} "
                    f"buySoundSrc={((new_game_shop_state.get('buyFeedbackLast') or {}).get('shopTransactionSoundSrc') or '')} "
                    f"buySoundPlayed={((new_game_shop_state.get('buyFeedbackLast') or {}).get('shopTransactionSoundPlayed'))} "
                    f"sellSound={((new_game_shop_state.get('sellFeedbackLast') or {}).get('shopTransactionSound') or '')} "
                    f"sellSoundSrc={((new_game_shop_state.get('sellFeedbackLast') or {}).get('shopTransactionSoundSrc') or '')} "
                    f"sellSoundPlayed={((new_game_shop_state.get('sellFeedbackLast') or {}).get('shopTransactionSoundPlayed'))} "
                    f"buySoundItemCount={((((new_game_shop_state.get('after') or {}).get('soundState') or {}).get('counts') or {}).get('item'))} "
                    f"sellSoundItemCount={((((new_game_shop_state.get('afterSell') or {}).get('soundState') or {}).get('counts') or {}).get('item'))} "
                    "loadedSaveSummary=False source=prototype-shop-prices "
                    "originalShopRuntimeImplemented=False "
                    "originalShopPriceTableMapped=False "
                    "originalShopInventoryMapped=False "
                    "originalMoneyMutationImplemented=False "
                    "originalStoryFlagRuntimeImplemented=False"
                ),
                "newGameShopObjective": (
                    "objective=후보 상점 완료 map4_08n "
                    "objectiveNext=완료 알림 확인 "
                    "objectiveAction=shop-completion-notice "
                    "objectiveActiveId=shop-complete:map4_08n "
                    "buy=약초 1->2 money=60->48 "
                    "sell=약초 2->1 money=48->54 "
                    f"{shop_completion_notice_feedback_summary(new_game_shop_objective)} "
                    "originalShopRuntimeImplemented=False "
                    "originalShopPriceTableMapped=False "
                    "originalShopInventoryMapped=False "
                    "originalMoneyMutationImplemented=False "
                    "originalStoryFlagRuntimeImplemented=False"
                ),
                "newGameShopTitleContinue": (
                    "map=map4_08n titleContinue=True "
                    f"label={next((label for label in (title_state.get('titleMenuLabels') or []) if '이어하기 map4_08n' in str(label)), '')} "
                    "item=약초 restoredMoney=54 restoredCount=1 "
                    "buyProgressCount=1 sellProgressCount=1 "
                    f"quickLoadText={new_game_shop_title_state.get('quickLoadText')} "
                    f"search={new_game_shop_title_state.get('search')} "
                    "loadedSaveSummary=False source=prototype-shop-prices "
                    "originalShopRuntimeImplemented=False "
                    "originalShopPriceTableMapped=False "
                    "originalShopInventoryMapped=False "
                    "originalMoneyMutationImplemented=False "
                    "originalStoryFlagRuntimeImplemented=False"
                ),
                "newGameShopTitleObjective": (
                    "objective=후보 상점 완료 map4_08n "
                    "objectiveNext=완료 알림 확인 "
                    "objectiveAction=shop-completion-notice "
                    "objectiveActiveId=shop-complete:map4_08n "
                    "titleContinue=True "
                    f"{shop_completion_notice_feedback_summary(new_game_shop_title_objective)} "
                    "originalShopRuntimeImplemented=False "
                    "originalShopPriceTableMapped=False "
                    "originalShopInventoryMapped=False "
                    "originalMoneyMutationImplemented=False "
                    "originalStoryFlagRuntimeImplemented=False"
                ),
                "snapshots": {
                    "shopMenu": state,
                    "shopCandidateMenu": shop_candidate_menu_state,
                    "shopPurchase": purchase_state,
                    "newGameShop": new_game_shop_state,
                    "newGameShopObjective": new_game_shop_objective,
                    "newGameShopTitle": title_state,
                    "newGameShopTitleRestore": new_game_shop_title_state,
                    "newGameShopTitleObjective": new_game_shop_title_objective,
                },
            }
            write_report(report)
            print(
                f"ok candidate shop menu browser {report['shopMenu']} "
                f"menu={report['shopCandidateMenu']} "
                f"newGameShopTitleContinue={report['newGameShopTitleContinue']}"
            )
        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()
