"use strict";

const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const test = require("node:test");

const repositoryRoot = path.resolve(__dirname, "..", "..");
const webRoot = path.join(repositoryRoot, "src", "MBN_STOCK_WEBVIEW.LegacyParityApp", "Web");
const markup = fs.readFileSync(path.join(webRoot, "index.html"), "utf8");
const styles = fs.readFileSync(path.join(webRoot, "styles.css"), "utf8");
const app = fs.readFileSync(path.join(webRoot, "app.js"), "utf8");
const navigation = fs.readFileSync(path.join(webRoot, "workspace-navigation.js"), "utf8");
const packageInputHarness = fs.readFileSync(path.join(
  repositoryRoot, "scripts", "Test-LegacyPackageInput.mjs"), "utf8");
const uiOnlySmokeHarness = fs.readFileSync(path.join(
  repositoryRoot, "scripts", "Test-LegacyPackageUiOnlySmoke.mjs"), "utf8");

const expectedMarketTabs = [
  "overseas",
  "exchange",
  "index",
  "kospiIndustry",
  "kosdaqIndustry",
  "comparison",
  "theme",
  "overseasStocks",
  "expert",
  "tradingHalt"
];

function pendingMenuFocusHarness() {
  const start = navigation.indexOf("function syncMarket");
  const end = navigation.indexOf("function select", start);
  assert.ok(start >= 0 && end > start);
  const source = navigation.slice(start, end);
  const documentObject = {
    body: {},
    documentElement: {},
    activeElement: null
  };
  const catalogChild = {};
  const unrelatedNavigationItem = {};
  let scheduled = null;
  const pendingButton = {
    disabled: false,
    focused: false,
    focus() {
      this.focused = true;
      documentObject.activeElement = this;
    }
  };
  const api = new Function(
    "document",
    "window",
    "catalogWorkspace",
    "pendingButton",
    `
      let activeWorkspace = "catalog";
      let activeMarketTabId = "old";
      let pendingKeyboardMarketTabId = "theme";
      const region = { dataset: {} };
      const title = { textContent: "" };
      function marketButton(tabId) {
        return tabId === "theme" ? pendingButton : null;
      }
      function refreshCurrentMenu() {}
      function syncHeadingIcon() {}
      function marketLabel() { return "테마"; }
      ${source}
      return { syncMarket, hasPendingKeyboardMarketFocus };
    `)(
      documentObject,
      { requestAnimationFrame(callback) { scheduled = callback; } },
      { contains(element) { return element === catalogChild; } },
      pendingButton);
  return {
    ...api,
    documentObject,
    catalogChild,
    unrelatedNavigationItem,
    pendingButton,
    runFrame() {
      assert.equal(typeof scheduled, "function");
      scheduled();
    }
  };
}

test("operator shell exposes stock and each graphic screen beside an always-present schedule", () => {
  for (const id of [
    "workspace-region",
    "workspace-navigation",
    "workspace-nav-toggle",
    "workspace-stock-tab",
    "market-tabs",
    "workspace-settings-tab",
    "workspace-title",
    "stock-workspace",
    "catalog-workspace",
    "settings-workspace",
    "workspace-splitter",
    "playlist-drop-zone"
  ]) {
    assert.match(markup, new RegExp(`id="${id}"`));
  }

  const workspaceStart = markup.indexOf('id="workspace-region"');
  const navigationStart = markup.indexOf('id="workspace-navigation"');
  const marketStart = markup.indexOf('id="market-tabs"');
  const settingsTabStart = markup.indexOf('id="workspace-settings-tab"');
  const stockStart = markup.indexOf('id="stock-workspace"');
  const catalogStart = markup.indexOf('id="catalog-workspace"');
  const settingsStart = markup.indexOf('id="settings-workspace"');
  const splitterStart = markup.indexOf('id="workspace-splitter"');
  const scheduleStart = markup.indexOf('class="playlist-panel"');
  assert.ok(workspaceStart >= 0 && navigationStart > workspaceStart);
  assert.ok(marketStart > navigationStart && settingsTabStart > marketStart);
  assert.ok(stockStart > settingsTabStart);
  assert.ok(catalogStart > stockStart);
  assert.ok(settingsStart > catalogStart && splitterStart > settingsStart);
  assert.ok(scheduleStart > splitterStart);
  assert.match(markup,
    /<\/section>\s*<\/div>\s*<\/section>\s*<\/section>\s*<div id="workspace-splitter"[^>]*><\/div>\s*<section class="playlist-panel"/);
  assert.match(markup,
    /id="workspace-splitter"[^>]*class="workspace-splitter"[^>]*role="separator"[^>]*aria-label="작업 영역과 송출 스케줄 너비 조절"[^>]*aria-orientation="vertical"[^>]*aria-controls="workspace-region"[^>]*aria-valuemin="34"[^>]*aria-valuemax="48"[^>]*aria-valuenow="39"[^>]*tabindex="0"/);
  assert.match(markup, /id="catalog-workspace"[^>]*hidden inert/);
  assert.match(markup, /id="settings-workspace"[^>]*hidden inert/);
  const scheduleHeaderEnd = markup.indexOf("</header>", scheduleStart);
  const scheduleHeader = markup.slice(scheduleStart, scheduleHeaderEnd);
  assert.match(scheduleHeader, /<h2>송출 스케줄<\/h2>/);
  assert.doesNotMatch(scheduleHeader, /<small\b/);
  assert.match(scheduleHeader,
    /class="playlist-state-legend"[^>]*aria-label="스케줄 상태"[\s\S]*?class="legend-selected"[\s\S]*?class="legend-next"[\s\S]*?class="legend-on-air"/);
});

test("the sidebar owns the ten native market buttons and keeps settings outside their order", () => {
  const marketStart = markup.indexOf('<div id="market-tabs"');
  const marketEnd = markup.indexOf("</div>", marketStart);
  assert.ok(marketStart >= 0 && marketEnd > marketStart);
  const marketMarkup = markup.slice(marketStart, marketEnd);
  const actualTabs = Array.from(
    marketMarkup.matchAll(/data-tab-id="([^"]+)"/g), match => match[1]);
  assert.deepEqual(actualTabs, expectedMarketTabs);
  assert.equal((marketMarkup.match(/<button\b/g) || []).length, expectedMarketTabs.length);
  assert.doesNotMatch(marketMarkup, /workspace-settings-tab|workspace-nav-section-label/);
  for (const tabId of expectedMarketTabs) {
    assert.match(marketMarkup,
      new RegExp(`data-tab-id="${tabId}"[\\s\\S]*?class="workspace-nav-label"`));
  }
});

test("layout keeps an adjustable 34-to-48-percent schedule beside the workspace", () => {
  assert.match(styles,
    /\.operator-shell\s*\{[^}]*grid-template-columns:\s*minmax\(780px,\s*1fr\)\s+var\(--workspace-splitter-width\)\s+minmax\(600px,\s*var\(--schedule-pane-width,[^)]+\)\)/);
  assert.match(styles, /--workspace-splitter-width:\s*8px/);
  assert.match(styles,
    /\.workspace-splitter\s*\{[^}]*cursor:\s*col-resize[^}]*touch-action:\s*none/);
  assert.match(styles, /body\.resizing-workspace\s*\{[^}]*cursor:\s*col-resize[^}]*user-select:\s*none/);
  assert.match(styles, /\.workspace-splitter:focus-visible/);
  assert.match(styles,
    /@media \(forced-colors:\s*active\)[\s\S]*?\.workspace-splitter/);
  assert.match(styles,
    /\.workspace-region\s*\{[^}]*grid-template-columns:\s*var\(--workspace-nav-width\) minmax\(0, 1fr\)/);
  assert.match(styles, /\.workspace-region\.nav-collapsed/);
  assert.match(styles, /--workspace-nav-collapsed:\s*62px/);
  assert.match(styles,
    /\.workspace-navigation\s*\{[^}]*grid-template-rows:\s*66px minmax\(0, 1fr\) auto/);
  assert.match(styles, /\.workspace-nav-footer\s*\{[^}]*border-top/);
  assert.match(styles,
    /\.workspace-navigation \.market-tabs button\s*\{[^}]*width:\s*100%[^}]*height:\s*46px/);
  assert.match(styles,
    /\.workspace-nav-item:focus-visible\s*\{[\s\S]*?outline:\s*2px solid var\(--workspace-accent\)/);
  assert.match(styles, /\.playlist-toolbar\s*\{[^}]*flex-wrap:\s*wrap/);
  assert.match(styles, /\.playlist-row\s*\{[^}]*min-width:\s*600px/);
  assert.match(packageInputHarness,
    /widthRatio < 1\.05 \|\| widthRatio > 1\.95/);
  assert.match(packageInputHarness,
    /workspaceShare < 0\.50 \|\| workspaceShare > 0\.67/);
  assert.match(packageInputHarness,
    /scheduleShare < 0\.33 \|\| scheduleShare > 0\.49/);
  assert.match(uiOnlySmokeHarness,
    /workspaceToScheduleWidthRatio < 1\.05 \|\|[\s\S]*?workspaceToScheduleWidthRatio > 1\.95/);
  assert.match(uiOnlySmokeHarness,
    /scheduleShare < 0\.33 \|\|[\s\S]*?scheduleShare > 0\.49/);
  assert.match(packageInputHarness, /adjustable workspace\/schedule split/);
  assert.match(uiOnlySmokeHarness, /adjustable workspace layout/);
  assert.match(packageInputHarness,
    /let baseline = await ensureWorkspace\("stock"[\s\S]*?expand workspace menu before layout verification/);
  assert.match(uiOnlySmokeHarness,
    /\[stockWorkspaceOk, settingsWorkspaceOk, catalogWorkspaceOk\]\.filter\(Boolean\)\.length !== 1/);
  assert.match(uiOnlySmokeHarness,
    /navigationAriaExpanded === "true" && layout\.navigationCollapsedClass === false[\s\S]*?navigationAriaExpanded === "false" && layout\.navigationCollapsedClass === true/);
  assert.doesNotMatch(packageInputHarness, /approximately 2:1 workspace\/schedule split/);
  assert.doesNotMatch(uiOnlySmokeHarness, /approximately 2:1 workspace layout/);
});

test("UI-only smoke settles a remembered startup screen before guarding later intents", () => {
  assert.match(uiOnlySmokeHarness,
    /async function waitForSettledStartup\(\)[\s\S]*?Date\.now\(\) \+ 10_000/);
  assert.match(uiOnlySmokeHarness,
    /lastSample\.bodyBusy === "false"[\s\S]*?lastSample\.connectionText === "DRY RUN"[\s\S]*?lastSample\.playoutStatus\.startsWith\("IDLE"\)/);
  assert.match(uiOnlySmokeHarness,
    /signature === stableSignature[\s\S]*?Date\.now\(\) - stableSince >= 400/);
  const settle = uiOnlySmokeHarness.indexOf(
    "evidence.safety.preGuardStartupSettle = await waitForSettledStartup()");
  const guard = uiOnlySmokeHarness.indexOf("await installNoIntentGuard()", settle);
  assert.ok(settle >= 0 && guard > settle,
    "startup must settle before the zero-intent guard is installed");
  assert.match(uiOnlySmokeHarness, /noNativeIntentAfterGuard:\s*true/);
});

test("package input physically resizes the splitter, verifies persistence, and restores width", () => {
  const start = packageInputHarness.indexOf("async function exerciseWorkspaceSplitter(baseline)");
  const end = packageInputHarness.indexOf("async function ensureWorkspace(", start);
  assert.ok(start >= 0 && end > start, "splitter input exercise must be a bounded helper");
  const exercise = packageInputHarness.slice(start, end);
  for (const contract of [
    /dragWorkspaceSplitterToPercent\(/,
    /pressKey\(keyboardKey, "resize workspace splitter from keyboard"\)/,
    /assertSplitterResizeResult\(/,
    /storedScheduleWidth/,
    /assertFixedSchedule\(baseline, restoredLayout/,
    /outboundMessages\.length/
  ]) {
    assert.match(exercise, contract);
  }
  assert.doesNotMatch(exercise, /\.click\(|localStorage\.setItem|style\.setProperty/);
  const validationStart = packageInputHarness.indexOf(
    "function assertSplitterResizeResult(layout, expectedPercent, label)");
  const validationEnd = packageInputHarness.indexOf(
    "async function dragWorkspaceSplitterToPercent(", validationStart);
  const validation = packageInputHarness.slice(validationStart, validationEnd);
  assert.ok(validationStart >= 0 && validationEnd > validationStart);
  assert.match(validation, /splitterState\?\.valueNow/);
  assert.match(validation,
    /Math\.abs\(layout\.splitterState\.valueNow -\s*Number\(layout\.scheduleWidthPercent\.toFixed\(1\)\)\) > 0\.001/);
  assert.doesNotMatch(validation,
    /splitterState\??\.valueNow\s*!==\s*Math\.round\(layout\.scheduleWidthPercent\)/);
  assert.match(validation, /storedScheduleWidth/);
  assert.match(validation, /scheduleCssWidth/);
  assert.match(validation, /geometryPercent/);

  const dragStart = packageInputHarness.indexOf(
    "async function dragWorkspaceSplitterToPercent(beforeLayout, targetPercent, label)");
  const dragEnd = packageInputHarness.indexOf("async function exerciseWorkspaceSplitter(", dragStart);
  const drag = packageInputHarness.slice(dragStart, dragEnd);
  assert.ok(dragStart >= 0 && dragEnd > dragStart);
  for (const gesture of [
    /await pressMouse\(start, 0\)/,
    /await moveMouse\(target, 0, 1\)/,
    /await releaseMouse\(target, 0\)/
  ]) {
    assert.match(drag, gesture);
  }
  assert.doesNotMatch(drag, /Input\.dispatchMouseEvent|\.click\(/);
  assert.match(packageInputHarness,
    /const splitterResize = await exerciseWorkspaceSplitter\(baseline\)[\s\S]*?"collapse workspace menu"/);
  assert.match(packageInputHarness, /splitterResize,/);
  assert.match(packageInputHarness,
    /ArrowLeft: \{ key: "ArrowLeft", code: "ArrowLeft", virtualKey: 37 \}/);
  assert.match(packageInputHarness,
    /ArrowRight: \{ key: "ArrowRight", code: "ArrowRight", virtualKey: 39 \}/);
});

test("read-only package smoke seals the real visual-card screen and nested title selection", () => {
  const start = packageInputHarness.indexOf(
    "async function verifyVisualCardPresentation()");
  const end = packageInputHarness.indexOf(
    "async function playlistDropGeometry", start);
  assert.ok(start >= 0 && end > start,
    "visual-card package verification must be a bounded helper");
  const visual = packageInputHarness.slice(start, end);
  for (const contract of [
    /selectMainTab\(\s*"overseas"/,
    /button\[data-action-id="fixed-001"\]\.visual-card/,
    /getComputedStyle\(list\)/,
    /columnCount:\s*tracks\.length/,
    /cardHeight:\s*button\.getBoundingClientRect\(\)\.height/,
    /ariaLabel:\s*button\.getAttribute\("aria-label"\)/,
    /hasPreview:\s*Boolean\(preview\)/,
    /wordmarkSource:\s*wordmark\.getAttribute\("src"\)/,
    /wordmarkNaturalWidth:\s*wordmark\.naturalWidth/,
    /metrics\.wordmarkNaturalWidth !== 176/,
    /metrics\.brandProduct !== "V-STOCK"/,
    /metrics\.hasPreview \|\|\s*metrics\.hasState \|\| metrics\.hasKicker/,
    /select fixed graphic from its visual-card title/,
    /outboundMessageCount !== outboundBaseline/,
    /checkpoint\("visual-card-presentation"/
  ]) assert.match(visual, contract);
  assert.match(packageInputHarness,
    /if \(readOnlyProfile\) \{\s*current = await verifyVisualCardPresentation\(\);/);
});

test("UI-only package smoke verifies the local customer wordmark is loaded and visible", () => {
  assert.match(uiOnlySmokeHarness,
    /const brandWordmark = document\.querySelector\("img\.brand-wordmark"\)/);
  assert.match(uiOnlySmokeHarness,
    /source:\s*brandWordmark\?\.getAttribute\("src"\)/);
  assert.match(uiOnlySmokeHarness, /naturalWidth:\s*brandWordmark\?\.naturalWidth/);
  assert.match(uiOnlySmokeHarness, /shell\.brand\.naturalWidth !== 176/);
  assert.match(uiOnlySmokeHarness, /shell\.brand\.naturalHeight !== 43/);
  assert.match(uiOnlySmokeHarness, /shell\.brand\.product !== "V-STOCK"/);
});

test("unified operator theme keeps clear semantic states and concise feedback", () => {
  assert.match(styles,
    /Unified operator theme: visual-only rules; geometry and native contracts stay intact/);
  assert.match(styles, /Compact broadcast-console components/);
  assert.equal((styles.match(/(?:^|\n):root\s*\{/g) || []).length, 1,
    "design tokens must have one authoritative :root block");
  for (const token of [
    "--ui-canvas",
    "--ui-accent",
    "--ui-success",
    "--ui-danger",
    "--ui-warning",
    "--ui-control-border"
  ]) {
    assert.match(styles, new RegExp(`${token}:`));
  }

  assert.match(styles,
    /font-family:\s*"Segoe UI Variable",\s*"Segoe UI",\s*"Noto Sans KR"/);
  assert.match(styles, /button:focus-visible,[\s\S]*?outline:\s*2px solid var\(--ui-accent\)/);
  assert.match(styles, /\.playlist-data-row\.on-air\.selected\s*\{/);
  assert.match(styles, /\.playout-actions \.take-in:not\(:disabled\)\s*\{/);
  assert.match(styles, /\.playout-actions \.next:not\(:disabled\)\s*\{/);
  assert.match(styles, /\.playout-actions \.take-out:not\(:disabled\)\s*\{/);
  assert.match(styles, /\.stock-result\.selected\s*\{/);
  assert.match(styles, /\.playlist-heading-count\s*\{/);
  assert.match(styles, /\.playlist-rows:empty::after\s*\{/);
  assert.match(styles, /\.playlist-state-legend\s*\{/);
  assert.match(styles, /\.playlist-toolbar-group\s*\{/);
  assert.match(styles, /\.search-meta\s*\{/);
  assert.match(styles, /\.stock-results:empty\[data-empty-label\]::after,/);
  assert.match(styles, /content:\s*attr\(data-empty-label\)/);
  assert.match(styles, /body\.busy::before\s*\{/);
  assert.match(styles, /@keyframes operator-progress/);
  assert.match(styles, /\.playout-status\[data-state="outcome-unknown"\]\s*\{/);
  assert.match(styles, /@media \(prefers-reduced-motion:\s*reduce\)/);
  assert.match(markup, /id="stock-search-button"[\s\S]*?<svg[^>]*viewBox="0 0 24 24"/);
  assert.match(markup,
    /id="stock-search-meta"[^>]*class="search-meta"[^>]*role="status"[^>]*aria-live="polite"[^>]*aria-atomic="true"[^>]*hidden/);
  assert.match(markup, /class="playlist-heading-icon"[\s\S]*?<svg[^>]*viewBox="0 0 24 24"/);
  for (const group of ["playlist-toolbar-order", "playlist-toolbar-background",
    "playlist-toolbar-db", "playlist-toolbar-danger"]) {
    assert.match(markup, new RegExp(`class="[^"]*${group}[^"]*"`));
  }
  assert.match(markup,
    /id="playlist-heading-count"[^>]*role="status"[^>]*aria-live="polite"[^>]*aria-atomic="true"/);
  assert.match(app, /stockResults\.dataset\.emptyLabel\s*=\s*"결과 없음"/);
  assert.match(app, /cutList\.dataset\.emptyLabel\s*=\s*Number\.isInteger/);
  assert.match(app, /const nextHeadingCount = `[^`]*\$\{state\.playlist\.length\}[^`]*\$\{enabledCount\}[^`]*`/);
  assert.match(app,
    /if \(playlistHeadingCount\.textContent !== nextHeadingCount\)\s*\{\s*playlistHeadingCount\.textContent = nextHeadingCount;/);
});

test("the complete sidebar is one labelled and collapsible navigation region", () => {
  assert.match(markup,
    /id="workspace-navigation"[^>]*role="navigation"[^>]*aria-label=/);
  assert.match(markup,
    /id="workspace-nav-toggle"[^>]*aria-expanded="true"[\s\S]*?aria-controls="workspace-nav-items workspace-nav-footer"/);
  assert.match(markup,
    /id="workspace-nav-items"[^>]*role="group"[^>]*aria-label=/);
  assert.match(markup,
    /id="market-tabs"[^>]*role="group"[^>]*aria-label=/);
  assert.match(markup, /id="workspace-nav-footer" class="workspace-nav-footer"/);
});

test("sidebar icons form one semantic SVG family and the heading follows the selected menu", () => {
  const mappings = [
    ["id=\"workspace-stock-tab\"", "stock-cut"],
    ["data-tab-id=\"overseas\"", "overseas"],
    ["data-tab-id=\"exchange\"", "exchange"],
    ["data-tab-id=\"index\"", "index"],
    ["data-tab-id=\"kospiIndustry\"", "kospi"],
    ["data-tab-id=\"kosdaqIndustry\"", "kosdaq"],
    ["data-tab-id=\"comparison\"", "comparison"],
    ["data-tab-id=\"theme\"", "theme"],
    ["data-tab-id=\"overseasStocks\"", "overseas-stocks"],
    ["data-tab-id=\"expert\"", "expert"],
    ["data-tab-id=\"tradingHalt\"", "trading-halt"],
    ["id=\"workspace-settings-tab\"", "settings"]
  ];
  for (const [anchor, icon] of mappings) {
    const start = markup.indexOf(anchor);
    const end = markup.indexOf("</button>", start);
    assert.ok(start >= 0 && end > start, `missing menu button: ${anchor}`);
    const button = markup.slice(start, end);
    assert.match(button, new RegExp(`data-icon="${icon}"`));
    assert.match(button, new RegExp(`href="#workspace-icon-${icon}"`));
  }

  const symbolIds = Array.from(
    markup.matchAll(/<symbol id="workspace-icon-([^"]+)" viewBox="0 0 24 24">/g),
    match => match[1]);
  assert.deepEqual(symbolIds, [
    "menu",
    "stock-cut",
    "overseas",
    "exchange",
    "index",
    "kospi",
    "kosdaq",
    "comparison",
    "theme",
    "overseas-stocks",
    "expert",
    "trading-halt",
    "settings"
  ]);

  const kospi = markup.slice(
    markup.indexOf("<symbol id=\"workspace-icon-kospi\""),
    markup.indexOf("</symbol>", markup.indexOf("<symbol id=\"workspace-icon-kospi\"")));
  const kosdaq = markup.slice(
    markup.indexOf("<symbol id=\"workspace-icon-kosdaq\""),
    markup.indexOf("</symbol>", markup.indexOf("<symbol id=\"workspace-icon-kosdaq\"")));
  assert.match(kospi, /M4\.5 18V6h4a4 4 0 0 1 0 8h-4/);
  assert.match(kospi, /M15 6h4M17 6v12M15 18h4/);
  assert.match(kosdaq, /M2\.3 18V6h1\.6c2\.8 0 4\.3 2\.2 4\.3 6/);
  assert.match(kosdaq, /M9 18l2\.5-12H13l2\.5 12M10 14h4\.5/);
  assert.match(kosdaq, /<ellipse cx="19\.2" cy="12" rx="3\.1" ry="6" stroke-width="1\.65">/);
  assert.match(kosdaq, /m19\.5 14\.5 3\.2 4/);
  assert.doesNotMatch(kospi + kosdaq, /<text\b/);
  assert.notEqual(kospi, kosdaq);

  assert.match(styles, /\.workspace-icon-sprite\s*\{[\s\S]*?width:\s*0;[\s\S]*?height:\s*0;/);
  assert.match(styles, /\.workspace-nav-icon\s*\{\s*shape-rendering:\s*geometricPrecision;\s*\}/);
  assert.match(navigation,
    /const titleIcon = document\.querySelector\("\.workspace-heading-icon"\)/);
  assert.match(navigation,
    /function syncHeadingIcon\(button\)[\s\S]*?source\.cloneNode\(true\)[\s\S]*?titleIcon\.replaceChildren\(next\)/);
  assert.match(navigation,
    /activeWorkspace = name;[\s\S]*?refreshCurrentMenu\(\);[\s\S]*?syncHeadingIcon\(selectedButton\);[\s\S]*?title\.textContent/);
  assert.match(navigation,
    /if \(activeWorkspace === "catalog"\)[\s\S]*?syncHeadingIcon\(button\);[\s\S]*?title\.textContent = marketLabel\(button\)/);
});

test("package smokes verify semantic icon painting, heading identity, and collapsed centering", () => {
  for (const harness of [packageInputHarness, uiOnlySmokeHarness]) {
    for (const [tabId, icon] of [
      ["overseas", "overseas"],
      ["exchange", "exchange"],
      ["index", "index"],
      ["kospiIndustry", "kospi"],
      ["kosdaqIndustry", "kosdaq"],
      ["comparison", "comparison"],
      ["theme", "theme"],
      ["overseasStocks", "overseas-stocks"],
      ["expert", "expert"],
      ["tradingHalt", "trading-halt"]
    ]) {
      assert.match(harness, new RegExp(`${tabId}: "${icon}"`));
    }
    assert.match(harness, /svg\?\.getBBox\?\.\(\)/);
    assert.match(harness, /reference:\s*svg\?\.querySelector\("use"\)\?\.getAttribute\("href"\)/);
    assert.match(harness, /Number\.parseFloat\(style\.opacity\) > 0/);
    assert.match(harness, /style\.stroke !== "none" \|\| style\.fill !== "none"/);
    assert.match(harness, /rendered:\s*Boolean\(/);
    assert.match(harness, /expectedHeadingIcon/);
    assert.match(harness, /#workspace-icon-\$\{expectedHeadingIcon\}/);
  }

  assert.match(packageInputHarness, /navigationItems:\s*\[/);
  assert.match(packageInputHarness, /iconCenterOffset:/);
  assert.match(packageInputHarness, /iconCenterOffsetY:/);
  assert.match(packageInputHarness, /item\.iconCenterOffset > 1\.5/);
  assert.match(packageInputHarness, /item\.iconCenterOffsetY > 1\.5/);
  assert.match(packageInputHarness, /item\.labelVisible !== false/);
  assert.match(packageInputHarness, /collapsedIconCenterOffsets:/);
  assert.match(packageInputHarness,
    /tab\.icon\.key === baseline\.marketTabs\[index\]\.icon\.key/);

  assert.match(uiOnlySmokeHarness, /layout\.stockTabIcon\.rendered === true/);
  assert.match(uiOnlySmokeHarness, /layout\.settingsTabIcon\.rendered === true/);
  assert.match(uiOnlySmokeHarness, /tab\.icon\.rendered === true/);
});

test("workspace navigation stores only the approved local presentation preferences", () => {
  assert.match(navigation, /entry\.view\.hidden = !isCurrent/);
  assert.match(navigation, /entry\.view\.toggleAttribute\("inert", !isCurrent\)/);
  assert.match(navigation, /if \(focusWillBeHidden && selectedButton\) selectedButton\.focus\(\)/);
  assert.match(navigation, /button\.setAttribute\("aria-current", isCurrent \? "page" : "false"\)/);
  assert.match(navigation, /region\.classList\.toggle\("nav-collapsed", !isExpanded\)/);
  assert.match(navigation, /event\.key !== "ArrowUp" && event\.key !== "ArrowDown"/);
  assert.match(navigation, /navigation\.querySelectorAll\("\.workspace-nav-item"\)/);
  assert.match(navigation, /event\.key === "Enter" \|\| event\.key === " "/);
  assert.match(navigation, /menuButton\.click\(\)/);
  assert.match(navigation, /pendingKeyboardMarketTabId = keyboardMarketTabId/);
  assert.match(navigation, /pendingButton\.focus\(\)/);
  assert.match(navigation, /window\.requestAnimationFrame\(function/);
  assert.match(navigation, /options\.authoritativeNativeRender === true/);
  assert.match(navigation, /hasPendingKeyboardMarketFocus/);
  assert.match(navigation, /document\.addEventListener\("pointerdown"/);
  assert.doesNotMatch(navigation, /catalogWorkspace\.contains\(active\)/);
  assert.match(navigation, /marketTabs\.addEventListener\("click"/);
  assert.match(navigation, /settingsTab\.addEventListener\("click"/);
  assert.match(navigation, /syncMarket\(button\.dataset\.tabId\)/);
  assert.match(navigation, /setWorkspace\("catalog"\)/);
  assert.deepEqual(
    Array.from(navigation.matchAll(/"(mbn-stock-webview\.[^"]+)"/g), match => match[1]),
    [
      "mbn-stock-webview.last-workspace.v1",
      "mbn-stock-webview.schedule-width.v1"
    ]);
  assert.match(navigation, /window\.localStorage && window\.localStorage\.getItem\(key\)/);
  assert.match(navigation, /window\.localStorage\.setItem\(key, value\)/);
  assert.doesNotMatch(navigation, /postMessage|sessionStorage|send\s*\(/);
});

test("missing external wordmark uses the source-only text fallback", () => {
  assert.match(navigation,
    /brandWordmark\.addEventListener\("error", showBrandWordmarkFallback, \{ once: true \}\)/);
  assert.match(navigation,
    /brandWordmark\.complete && brandWordmark\.naturalWidth === 0/);
  assert.match(navigation,
    /classList\.add\("wordmark-fallback-active"\)/);
});

test("splitter clamps pointer and keyboard resizing and Escape restores the gesture", () => {
  for (const declaration of [
    /const scheduleMinimumPercent = 34/,
    /const scheduleMaximumPercent = 48/,
    /const workspaceMinimumPixels = 780/,
    /const scheduleMinimumPixels = 600/,
    /const splitterPixels = 8/
  ]) {
    assert.match(navigation, declaration);
  }
  assert.match(navigation,
    /const minimum = Math\.max\([\s\S]*?scheduleMinimumPercent,[\s\S]*?scheduleMinimumPixels \/ width \* 100\)/);
  assert.match(navigation,
    /const maximum = Math\.min\([\s\S]*?scheduleMaximumPercent,[\s\S]*?\(width - workspaceMinimumPixels - splitterPixels\) \/ width \* 100\)/);
  assert.match(navigation,
    /return Math\.min\(bounds\.maximum, Math\.max\(bounds\.minimum, value\)\)/);
  assert.match(navigation, /shell\.style\.setProperty\("--schedule-pane-width", serialized\)/);
  for (const attribute of ["aria-valuemin", "aria-valuemax", "aria-valuenow", "aria-valuetext"]) {
    assert.match(navigation, new RegExp(`splitter\\.setAttribute\\("${attribute}"`));
  }
  assert.match(navigation,
    /splitter\.setAttribute\("aria-valuemin", bounds\.minimum\.toFixed\(1\)\)/);
  assert.match(navigation,
    /splitter\.setAttribute\("aria-valuemax", bounds\.maximum\.toFixed\(1\)\)/);
  assert.match(navigation,
    /splitter\.setAttribute\("aria-valuenow", scheduleWidthPercent\.toFixed\(1\)\)/);
  assert.doesNotMatch(navigation,
    /aria-valuemin"[\s\S]{0,80}Math\.ceil|aria-valuemax"[\s\S]{0,80}Math\.floor/);
  assert.match(navigation,
    /event\.button !== 0 \|\| event\.isPrimary === false \|\| splitterGesture/);
  assert.match(navigation, /splitter\.setPointerCapture\?\.\(event\.pointerId\)/);
  for (const eventName of ["pointerdown", "pointermove", "pointerup", "pointercancel",
    "lostpointercapture"]) {
    assert.match(navigation,
      new RegExp(`splitter\\.addEventListener\\("${eventName}"`));
  }
  assert.match(navigation,
    /event\.key === "Escape" && splitterGesture[\s\S]*?event\.stopImmediatePropagation\(\)[\s\S]*?finishSplitterGesture\(true\)/);
  assert.match(navigation, /event\.key === "ArrowLeft"\) next \+= 2/);
  assert.match(navigation, /event\.key === "ArrowRight"\) next -= 2/);
  assert.match(navigation, /event\.key === "Home"\) next = numericScheduleBounds\(\)\.minimum/);
  assert.match(navigation, /event\.key === "End"\) next = numericScheduleBounds\(\)\.maximum/);
  assert.match(navigation,
    /applyScheduleWidth\(\s*restore === true \? gesture\.startPercent : scheduleWidthPercent,[\s\S]*?restore !== true\)/);
});

test("fractional splitter bounds always publish an ordered ARIA range", () => {
  const declarations = [
    "scheduleMinimumPercent",
    "scheduleMaximumPercent",
    "workspaceMinimumPixels",
    "scheduleMinimumPixels",
    "splitterPixels"
  ].map(name => {
    const match = navigation.match(new RegExp(`const ${name} = [^;]+;`));
    assert.ok(match, `missing ${name}`);
    return match[0];
  }).join("\n");
  const functionsStart = navigation.indexOf("function numericScheduleBounds()");
  const functionsEnd = navigation.indexOf("function schedulePercentFromPointer", functionsStart);
  assert.ok(functionsStart >= 0 && functionsEnd > functionsStart);
  const functionsSource = navigation.slice(functionsStart, functionsEnd);

  for (const shellWidth of [1388, 1389, 1501, 1919]) {
    const attributes = new Map();
    const properties = new Map();
    const shell = {
      getBoundingClientRect() { return { width: shellWidth }; },
      style: { setProperty(name, value) { properties.set(name, value); } }
    };
    const splitter = {
      setAttribute(name, value) { attributes.set(name, value); }
    };
    const api = Function("shell", "splitter", "writeStoredValue", `
      "use strict";
      ${declarations}
      let scheduleWidthPercent = null;
      ${functionsSource}
      return { numericScheduleBounds, applyScheduleWidth };
    `)(shell, splitter, () => {});
    const bounds = api.numericScheduleBounds();
    for (const requested of [bounds.minimum - 5,
      (bounds.minimum + bounds.maximum) / 2, bounds.maximum + 5]) {
      assert.equal(api.applyScheduleWidth(requested, false), true);
      const minimum = Number(attributes.get("aria-valuemin"));
      const maximum = Number(attributes.get("aria-valuemax"));
      const now = Number(attributes.get("aria-valuenow"));
      assert.ok(Number.isFinite(minimum) && Number.isFinite(maximum) && Number.isFinite(now));
      assert.ok(minimum <= now && now <= maximum,
        `ARIA range is unordered at ${shellWidth}px: ${minimum} <= ${now} <= ${maximum}`);
      assert.match(attributes.get("aria-valuemin"), /^\d+\.\d$/u);
      assert.match(attributes.get("aria-valuemax"), /^\d+\.\d$/u);
      assert.match(attributes.get("aria-valuenow"), /^\d+\.\d$/u);
      assert.match(properties.get("--schedule-pane-width"), /^\d+\.\d{3}%$/u);
    }
  }
});

test("explicit menu input wins over any deferred last-workspace restore", () => {
  assert.match(navigation,
    /stockTab\.addEventListener\("click", function \(\) \{\s*startPreferenceApplied = true;\s*setWorkspace\("stock"\);\s*\}\)/);
  assert.match(navigation,
    /settingsTab\.addEventListener\("click", function \(\) \{\s*startPreferenceApplied = true;\s*setWorkspace\("settings"\);\s*\}\)/);
  assert.match(navigation,
    /marketTabs\.addEventListener\("click", function \(event\) \{[\s\S]*?if \(!button \|\| !marketTabs\.contains\(button\)\) return;\s*startPreferenceApplied = true;\s*syncMarket\(button\.dataset\.tabId\);\s*setWorkspace\("catalog"\);/);
});

test("native tab rendering preserves sidebar icons and reconciles the active market", () => {
  const app = fs.readFileSync(path.join(webRoot, "app.js"), "utf8");
  assert.match(app, /button\.querySelector\("\.workspace-nav-label"\)/);
  assert.match(app, /if \(label\) label\.textContent = tab\.label/);
  assert.match(app, /LegacyWorkspaceNavigation\?\.syncMarket/);
  assert.match(app, /authoritativeNativeRender: true/);
  assert.match(app, /activeTab && state\.isBusy !== true/);
  assert.match(app, /hasPendingKeyboardMarketFocus\?\.\(\) === true/);
  assert.match(app,
    /!modalFocusManager\.getActiveModal\(\) && !keyboardMarketFocusPending/);
  assert.doesNotMatch(app,
    /button\.classList\.toggle\("active"[^\n]*\n\s*button\.setAttribute\("aria-current"/);
});

test("workspace navigation loads before the native-authority application bridge", () => {
  const navigationScript = markup.indexOf('<script src="workspace-navigation.js"></script>');
  const applicationScript = markup.indexOf('<script src="app.js"></script>');
  assert.ok(navigationScript >= 0 && applicationScript > navigationScript);
});

test("keyboard menu focus wins after a new graphics tree renders, but not after a deliberate move", () => {
  const stolen = pendingMenuFocusHarness();
  stolen.syncMarket("theme");
  assert.equal(stolen.hasPendingKeyboardMarketFocus("theme"), true,
    "the optimistic capture-phase selection retains keyboard ownership");
  assert.equal(stolen.pendingButton.focused, false,
    "the optimistic capture-phase selection cannot restore focus early");
  stolen.syncMarket("theme", { authoritativeNativeRender: true });
  stolen.documentObject.activeElement = stolen.documentObject.body;
  stolen.runFrame();
  assert.equal(stolen.pendingButton.focused, true,
    "the final native render restores focus lost while the menu was disabled");

  for (const deliberateFocus of ["sidebar", "content"]) {
    const deliberate = pendingMenuFocusHarness();
    deliberate.syncMarket("theme");
    deliberate.syncMarket("theme", { authoritativeNativeRender: true });
    deliberate.documentObject.activeElement = deliberateFocus === "sidebar"
      ? deliberate.unrelatedNavigationItem
      : deliberate.catalogChild;
    deliberate.runFrame();
    assert.equal(deliberate.pendingButton.focused, false,
    "a subsequent deliberate sidebar focus move remains authoritative");
  }
});
