"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 app = fs.readFileSync(path.join(webRoot, "app.js"), "utf8");
const playout = fs.readFileSync(path.join(webRoot, "playout-ui.js"), "utf8");
const markup = fs.readFileSync(path.join(webRoot, "index.html"), "utf8");
const styles = fs.readFileSync(path.join(webRoot, "styles.css"), "utf8");

test("legacy shell exposes the original stock, cut, playlist and playout controls", () => {
  for (const id of [
    "stock-search",
    "stock-search-button",
    "stock-results",
    "market-tabs",
    "category-tree",
    "catalog-expand-all",
    "moving-average-5",
    "moving-average-20",
    "cut-list",
    "cut-drag-feedback",
    "playlist-drop-zone",
    "playlist-rows",
    "playlist-enable-all",
    "playlist-move-up",
    "playlist-move-down",
    "playlist-delete-selected",
    "playlist-delete-all",
    "db-save-button",
    "db-load-button",
    "named-playlist-modal",
    "named-playlist-definitions",
    "named-playlist-new-title",
    "named-playlist-create-button",
    "named-playlist-delete-button",
    "named-playlist-confirm-button",
    "legacy-dialog"
  ]) {
    assert.match(markup, new RegExp("id=\\\"" + id + "\\\""));
  }
  assert.match(markup, /TAKE IN/);
  assert.match(markup, /TAKE OUT/);
  assert.match(markup, /NEXT/);
  assert.match(markup, /class="connection-state"[^>]*>미연결<\/div>/);
  assert.match(markup,
    /class="[^"]*\bactive\b[^"]*"[^>]*data-tab-id="overseas"[\s\S]*?<span class="workspace-nav-label">해외<\/span>[\s\S]*?<\/button>/);
  assert.match(markup,
    /data-tab-id="tradingHalt"[\s\S]*?<span class="workspace-nav-label">거래정지<\/span>[\s\S]*?<\/button>/);
});

test("web layer sends intent and does not own scene, DB identity or playlist state", () => {
  assert.doesNotMatch(app, /(?:sceneCode|cutCode|stockCode|dataCode|builderKey)/);
  assert.doesNotMatch(app, /(?:localStorage|sessionStorage|playlist\.push|fetch\s*\()/);
  assert.doesNotMatch(app, /\b(?:5001|N5001|5006|5011|5086|8035|8061|8040|8046|8051|8056|8003|5037)\b/);
});

test("legacy stock double click is represented by pointer-down facts", () => {
  assert.match(app, /cut-pointer-down/);
  assert.match(app, /timestampMilliseconds/);
  assert.match(app, /event\.ctrlKey/);
  assert.match(app, /event\.shiftKey/);
  const cutFactory = app.slice(app.indexOf("function createCutElement"), app.indexOf("function renderCuts"));
  assert.doesNotMatch(cutFactory, /dblclick/);
  assert.match(cutFactory, /allowAppend: !playlistMutationLocked/);
  assert.match(cutFactory, /if \(!playlistMutationLocked\)/);
  assert.match(app, /send\("clear-cut-selection", \{\}\)/);
  assert.match(app, /event\.target !== cutList/);
  assert.match(app, /button\.addEventListener\("keydown"/);
  assert.match(app, /stockResults\.querySelectorAll\("button\[data-result-index\]"\)/);
});

test("cut rows are reconciled in place while the legacy pointer drag remains captured", () => {
  assert.match(app, /dataset\.physicalIndex/);
  assert.match(app, /function createCutElement/);
  const cutFactory = app.slice(
    app.indexOf("function createCutElement"),
    app.indexOf("function clearPlaylistDragVisuals"));
  assert.match(cutFactory, /beginCutDragGesture\(element, event, position\)/);
  assert.match(cutFactory, /addEventListener\("pointermove", onCutPointerMove\)/);
  assert.match(cutFactory, /addEventListener\("lostpointercapture", onCutPointerCancel\)/);
  assert.match(cutFactory, /const cutDragLocked = isOperatorMutationLocked\(state\)/);
  assert.match(cutFactory, /!isCutSelectionLocked\(state\)/);
  assert.match(cutFactory, /element\.draggable = false/);
  assert.match(cutFactory, /dataset\.cutDragEnabled = cutSelectionEnabled && !cutDragLocked/);
  assert.doesNotMatch(cutFactory, /addEventListener\("dragstart"/);
  assert.doesNotMatch(app, /cutList\.replaceChildren/);
});

test("program keeps cut append available while structural playlist edits stay locked", () => {
  const sourceStart = app.indexOf("function isOperatorMutationLocked");
  const sourceEnd = app.indexOf("function dragDropPosition", sourceStart);
  assert.ok(sourceStart >= 0 && sourceEnd > sourceStart);
  const locks = new Function(`
    ${app.slice(sourceStart, sourceEnd)}
    return {
      isOperatorMutationLocked,
      isPlaylistStructureMutationLocked,
      isPlaylistProgramEnableMutationLocked,
      canDeleteSelectedPlaylistRows,
      isCutSelectionLocked
    };
  `)();

  const programState = {
    isBusy: false,
    fixedSectionBatch: null,
    playlist: [
      { rowId: "on-air", isSelected: false },
      { rowId: "tail", isSelected: true }
    ],
    playout: {
      phase: "program",
      currentCueIndexZeroBased: 0,
      onAirCueIndexZeroBased: 0,
      isBusy: false,
      outcomeUnknown: false,
      isPlayCompletionPending: false,
      isTakeOutCompletionPending: false
    }
  };
  assert.equal(locks.isOperatorMutationLocked(programState), false,
    "safe tail append and independent DB edits remain available");
  assert.equal(locks.isPlaylistStructureMutationLocked(programState), true,
    "reorder, mouse checkbox changes, and whole-list replacement remain blocked");
  assert.equal(locks.isPlaylistProgramEnableMutationLocked(programState), false,
    "legacy Space and all-checkbox enabled-state changes remain available in PROGRAM");
  assert.equal(locks.canDeleteSelectedPlaylistRows(programState), true,
    "a selected row after the on-air row can be deleted");
  assert.equal(locks.isCutSelectionLocked(programState), false);

  programState.playlist[0].isSelected = true;
  programState.playlist[1].isSelected = false;
  assert.equal(locks.canDeleteSelectedPlaylistRows(programState), false,
    "the on-air row cannot be deleted");

  programState.playout.isBusy = true;
  assert.equal(locks.isOperatorMutationLocked(programState), true);
  assert.equal(locks.isPlaylistProgramEnableMutationLocked(programState), true);
  assert.equal(locks.isCutSelectionLocked(programState), true);

  programState.playout.isBusy = false;
  programState.playout.phase = "prepared";
  assert.equal(locks.isPlaylistProgramEnableMutationLocked(programState), true,
    "PREPARED remains locked without an original parity basis");
  programState.playout.phase = "idle";
  assert.equal(locks.isPlaylistProgramEnableMutationLocked(programState), false);
});

test("cut drag feedback follows the pointer and exposes allowed target state", () => {
  const sourceStart = app.indexOf("function showCutDragFeedback");
  const sourceEnd = app.indexOf("function beginCutDragGesture", sourceStart);
  assert.ok(sourceStart >= 0 && sourceEnd > sourceStart);
  const feedback = {
    hidden: true,
    dataset: {},
    offsetWidth: 120,
    offsetHeight: 30,
    classList: {
      values: new Set(),
      toggle(name, enabled) {
        if (enabled) this.values.add(name);
        else this.values.delete(name);
      }
    },
    style: {}
  };
  const show = new Function("cutDragFeedback", "document", `
    ${app.slice(sourceStart, sourceEnd)}
    return showCutDragFeedback;
  `)(feedback, { documentElement: { clientWidth: 800, clientHeight: 600 } });

  show({ clientX: 100, clientY: 80 }, false);
  assert.equal(feedback.hidden, false);
  assert.equal(feedback.dataset.dropAllowed, "false");
  assert.equal(feedback.style.left, "114px");
  assert.equal(feedback.style.top, "94px");
  assert.equal(feedback.classList.values.has("allowed"), false);

  show({ clientX: 790, clientY: 590 }, true);
  assert.equal(feedback.dataset.dropAllowed, "true");
  assert.equal(feedback.style.left, "676px");
  assert.equal(feedback.style.top, "566px");
  assert.equal(feedback.classList.values.has("allowed"), true);
  assert.match(styles, /\.cut-drag-feedback \{[^}]*pointer-events: none/);
  assert.match(styles, /\.cut-drag-feedback\.allowed/);
});

test("state rendering preserves stock and playlist focus like native controls", () => {
  assert.match(app, /dataset\.resultIndex/);
  assert.match(app, /dataset\.rowId/);
  assert.match(app, /aria-current/);
  assert.match(app, /scrollIntoView\(\{ block: "nearest", inline: "nearest" \}\)/);
  assert.doesNotMatch(app, /stockResults\.replaceChildren/);
  assert.doesNotMatch(app, /playlistRows\.replaceChildren/);
});

test("playlist keeps candidate, selection, and pink on-air states independent", () => {
  const sourceStart = app.indexOf("function playlistPlayoutPresentation");
  const sourceEnd = app.indexOf("function presentCurrentPlaylistRow", sourceStart);
  assert.ok(sourceStart >= 0 && sourceEnd > sourceStart);
  const presentation = new Function(`
    ${app.slice(sourceStart, sourceEnd)}
    return playlistPlayoutPresentation;
  `)();
  const state = {
    playout: {
      phase: "program",
      currentEntryId: "row-current",
      currentCueIndexZeroBased: 1,
      onAirEntryId: "row-current",
      onAirCueIndexZeroBased: 1,
      pageIndexZeroBased: 1,
      pageCount: 3
    }
  };

  assert.deepEqual(presentation(state, {
    rowId: "row-current", isSelected: false, pageText: "1/3"
  }, 1), {
    isSelected: false,
    isCurrent: true,
    isOnAir: true,
    pageText: "2/3",
    focusKey: "program:row-current:1:3"
  });
  assert.deepEqual(presentation(state, {
    rowId: "row-candidate", isSelected: true, pageText: "1/1"
  }, 1), {
    isSelected: true,
    isCurrent: false,
    isOnAir: false,
    pageText: "1/1",
    focusKey: ""
  });
  assert.equal(presentation({
    playout: { ...state.playout, currentEntryId: null }
  }, { rowId: "row-fallback", isSelected: false, pageText: "1/3" }, 1).isCurrent, true);

  const failedNextState = {
    playout: {
      ...state.playout,
      currentEntryId: "row-unplayable",
      currentCueIndexZeroBased: 2,
      onAirEntryId: "row-current",
      onAirCueIndexZeroBased: 1
    }
  };
  assert.deepEqual(presentation(failedNextState, {
    rowId: "row-unplayable", isSelected: false, pageText: "1/1"
  }, 2), {
    isSelected: false,
    isCurrent: true,
    isOnAir: false,
    pageText: "1/1",
    focusKey: "program:row-unplayable:-1:0"
  });
  assert.deepEqual(presentation(failedNextState, {
    rowId: "row-current", isSelected: false, pageText: "1/3"
  }, 1), {
    isSelected: false,
    isCurrent: false,
    isOnAir: true,
    pageText: "2/3",
    focusKey: ""
  });
  assert.match(app, /presentation\.isOnAir \? " on-air"/);
  assert.match(app, /presentation\.isSelected \|\| isCandidate \? " selected"/);
  assert.match(app, /presentation\.pageText/);
});

test("PROGRAM F8 remains TAKE IN while end-of-playlist NEXT is closed", () => {
  const sourceStart = playout.indexOf("function nextLabel");
  const sourceEnd = playout.indexOf("function render", sourceStart);
  assert.ok(sourceStart >= 0 && sourceEnd > sourceStart);
  const contract = new Function(`
    ${playout.slice(sourceStart, sourceEnd)}
    return { nextLabel, canTakeInFromState, canAdvanceFromState };
  `)();

  assert.equal(contract.canTakeInFromState({ phase: "idle" }, true), true);
  assert.equal(contract.canTakeInFromState({ phase: "prepared" }, false), true);
  assert.equal(contract.canTakeInFromState({ phase: "program" }, true), true);
  assert.equal(contract.canTakeInFromState({ phase: "program" }, false), false);
  assert.equal(contract.canAdvanceFromState({
    phase: "program", nextKind: "playlistNext"
  }), true);
  assert.equal(contract.canAdvanceFromState({
    phase: "program", nextKind: "endOfPlaylist"
  }), false);
  assert.match(contract.nextLabel({ nextKind: "endOfPlaylist" }), /마지막/);
});

test("IDLE Escape keeps the emergency StopAll path available", () => {
  assert.match(playout, /takeOut\.disabled = busy \|\| !commandAvailable;/);
  assert.doesNotMatch(
    playout,
    /takeOut\.disabled[\s\S]{0,120}playout\.phase !== "prepared"/);
});

test("legacy child modal Escape is inert and cannot leak into the playout listener", () => {
  const appScript = markup.indexOf('<script src="app.js"></script>');
  const playoutScript = markup.indexOf('<script src="playout-ui.js"></script>');
  assert.ok(appScript >= 0 && playoutScript > appScript);

  const keydownMarker = 'document.addEventListener("keydown", function (event) {';
  const appHandlerStart = app.lastIndexOf(keydownMarker);
  const appHandlerBodyStart = appHandlerStart + keydownMarker.length;
  const appHandlerEnd = app.indexOf("\n  });", appHandlerBodyStart);
  assert.ok(appHandlerStart >= 0 && appHandlerEnd > appHandlerBodyStart);
  const handleAppKeydown = new Function(
    "event",
    "modalFocusManager",
    "cutDragGesture",
    "clearCutDragGesture",
    "playlistDragGesture",
    "clearPlaylistDragGesture",
    "deferredPlaylistPlainSelectionRowId",
    "legacyAlertDialog",
    "legacyDialogIsConfirmation",
    "send",
    app.slice(appHandlerBodyStart, appHandlerEnd));

  const playoutHandlerStart = playout.lastIndexOf(keydownMarker);
  const playoutHandlerBodyStart = playoutHandlerStart + keydownMarker.length;
  const playoutHandlerEnd = playout.indexOf("\n  });", playoutHandlerBodyStart);
  assert.ok(playoutHandlerStart >= 0 && playoutHandlerEnd > playoutHandlerBodyStart);
  const handlePlayoutKeydown = new Function(
    "event",
    "hasOpenDialog",
    "backgroundFile",
    "backgroundNone",
    "takeIn",
    "takeOut",
    playout.slice(playoutHandlerBodyStart, playoutHandlerEnd));

  const clicks = [];
  const button = name => ({ disabled: false, click() { clicks.push(name); } });
  const backgroundFile = button("choose-background");
  const backgroundNone = button("toggle-background");
  const takeIn = button("take-in");
  const takeOut = button("take-out");
  const modal = { hidden: false };

  for (const key of ["F2", "F3", "F8"]) {
    const modalKey = {
      key,
      repeat: false,
      defaultPrevented: false,
      preventDefault() { this.defaultPrevented = true; }
    };
    handlePlayoutKeydown(
      modalKey,
      () => !modal.hidden,
      backgroundFile,
      backgroundNone,
      takeIn,
      takeOut);
    assert.equal(modalKey.defaultPrevented, true, key + " browser default must be consumed");
  }
  assert.deepEqual(clicks, []);

  const escape = new Event("keydown", { cancelable: true });
  Object.defineProperties(escape, {
    key: { value: "Escape" },
    repeat: { value: false }
  });
  const childDialog = {};
  const legacyAlertDialog = {};
  const sent = [];
  handleAppKeydown(
    escape,
    { getActiveModal() { return childDialog; } },
    null,
    () => {},
    null,
    () => {},
    null,
    legacyAlertDialog,
    false,
    (type) => sent.push(type));
  assert.equal(modal.hidden, false);
  assert.equal(escape.defaultPrevented, true);

  handlePlayoutKeydown(
    escape,
    () => !modal.hidden,
    backgroundFile,
    backgroundNone,
    takeIn,
    takeOut);
  assert.deepEqual(clicks, []);
  assert.deepEqual(sent, []);

  const alertEscape = new Event("keydown", { cancelable: true });
  Object.defineProperties(alertEscape, {
    key: { value: "Escape" },
    repeat: { value: false }
  });
  handleAppKeydown(
    alertEscape,
    { getActiveModal() { return legacyAlertDialog; } },
    null,
    () => {},
    null,
    () => {},
    null,
    legacyAlertDialog,
    false,
    (type) => sent.push(type));
  assert.equal(alertEscape.defaultPrevented, true);
  assert.deepEqual(sent, ["dismiss-dialog"]);
});

test("started pointer drag owns playout keys and Escape cancels without TAKE OUT", () => {
  const keydownMarker = 'document.addEventListener("keydown", function (event) {';
  const start = app.lastIndexOf(keydownMarker) + keydownMarker.length;
  const end = app.indexOf("\n  });", start);
  const handle = new Function(
    "event",
    "modalFocusManager",
    "cutDragGesture",
    "clearCutDragGesture",
    "playlistDragGesture",
    "clearPlaylistDragGesture",
    "deferredPlaylistPlainSelectionRowId",
    "legacyAlertDialog",
    "legacyDialogIsConfirmation",
    "send",
    app.slice(start, end));
  const sent = [];
  let clears = 0;
  const manager = { getActiveModal() { return null; } };
  for (const key of ["F2", "F3", "F8", "Escape"]) {
    const event = {
      key,
      repeat: false,
      defaultPrevented: false,
      preventDefault() { this.defaultPrevented = true; }
    };
    handle(
      event,
      manager,
      { started: true },
      () => { clears += 1; },
      null,
      () => {},
      null,
      {},
      false,
      (type) => sent.push(type));
    assert.equal(event.defaultPrevented, true, key + " must stay inside drag loop");
  }
  assert.equal(clears, 1, "only Escape cancels the active drag");

  const playlistEscape = {
    key: "Escape",
    repeat: false,
    defaultPrevented: false,
    preventDefault() { this.defaultPrevented = true; }
  };
  let playlistClears = 0;
  handle(
    playlistEscape,
    manager,
    null,
    () => {},
    { started: true },
    () => { playlistClears += 1; },
    "row-a",
    {},
    false,
    (type) => sent.push(type));
  assert.equal(playlistEscape.defaultPrevented, true);
  assert.equal(playlistClears, 1);
  assert.deepEqual(sent, []);
});

test("playlist controls send intent for selection, enabled state, movement and deletion", () => {
  for (const intent of [
    "select-playlist-row",
    "activate-playlist-row",
    "set-playlist-enabled",
    "set-all-playlist-enabled",
    "reorder-playlist-rows",
    "move-selected-playlist-rows",
    "delete-selected-playlist-rows",
    "delete-all-playlist-rows"
  ]) {
    assert.match(app, new RegExp("send\\(\\\"" + intent + "\\\""));
  }
  assert.match(app, /control: event\.ctrlKey/);
  assert.match(app, /shift: event\.shiftKey/);
  assert.match(app, /event\.key === "Delete"/);
  const deleteBranch = app.slice(
    app.lastIndexOf('if (event.key === "Delete"'),
    app.lastIndexOf('} else if (event.key === " "'));
  assert.match(
    deleteBranch,
    /if \(!playlistDeleteSelectedLocked\) send\("delete-selected-playlist-rows"/);
  assert.doesNotMatch(deleteBranch, /event\.preventDefault\(\)/);
  assert.match(app, /send\("toggle-active-playlist-enabled", \{\}\)/);
  const spaceBranch = app.slice(
    app.lastIndexOf('} else if (event.key === " "'),
    app.lastIndexOf('} else if ((event.key === "Home"'));
  assert.match(spaceBranch, /if \(!playlistProgramEnableMutationLocked\)/);
  assert.doesNotMatch(spaceBranch, /set-playlist-enabled/);
  assert.match(app, /if \(!playlistMutationLocked\)/);
  assert.match(app, /select-playlist-boundary/);
  assert.match(app, /event\.key === "Home"/);
  assert.match(app, /event\.key === "End"/);
  const homeEndBranchStart = app.lastIndexOf('} else if ((event.key === "Home"');
  const homeEndBranch = app.slice(
    homeEndBranchStart,
    app.indexOf("\n  });", homeEndBranchStart));
  assert.doesNotMatch(homeEndBranch, /event\.preventDefault\(\)/);
  const boundaryHelperStart = app.indexOf("function selectPlaylistBoundary");
  const boundaryHelper = app.slice(
    boundaryHelperStart,
    app.indexOf("function movePlaylistKeyboardSelection", boundaryHelperStart));
  assert.doesNotMatch(boundaryHelper, /\.focus\(/);
  assert.match(boundaryHelper, /scrollIntoView/);
  const arrowHelperStart = app.indexOf("function movePlaylistKeyboardSelection");
  const arrowHelper = app.slice(
    arrowHelperStart,
    app.indexOf("function clearDelayedClick", arrowHelperStart));
  assert.match(arrowHelper, /event\.key === "ArrowUp"/);
  assert.match(arrowHelper, /next\.focus\(\{ preventScroll: true \}\)/);
  assert.match(arrowHelper, /send\("select-playlist-row"/);
  assert.match(app, /event\.key === "ArrowDown"/);
  assert.match(app, /row\.isSelected/);
  assert.match(app, /row\.isActive === true/);
  assert.match(app, /enabled\.setAttribute\("aria-disabled", playlistStructureMutationLocked/);
  assert.match(app, /interactionDisabled = playlistStructureMutationLocked/);
  assert.match(app, /playlistEnableAll\.disabled = playlistProgramEnableMutationLocked/);
  assert.match(app, /playlistEnableAll\.indeterminate/);
});

test("playlist reorder reconciles keyed DOM nodes without discarding focus", () => {
  assert.match(app, /const existingRows = new Map\(\)/);
  assert.match(app, /existingRows\.get\(row\.rowId\)/);
  assert.match(app, /playlistRows\.insertBefore\(element, elementAtPosition\)/);
  assert.match(app, /const retainedRowIds = new Set/);
  assert.doesNotMatch(app, /playlistRows\.replaceChild/);
});

test("playlist reorder starts only from the 30 px row header pointer handle", () => {
  assert.match(markup,
    /playlist-header playlist-row">\s*<div aria-hidden="true"><\/div><div>송출<\/div>/u);
  assert.match(styles,
    /\.playlist-row\s*\{[^}]*grid-template-columns:\s*30px 40px 100px/u);
  assert.match(styles, /\.playlist-data-row \.playlist-row-header/u);
  class MiniClassList {
    constructor() { this.names = new Set(); }
    add(...names) { names.forEach((name) => this.names.add(name)); }
    remove(...names) { names.forEach((name) => this.names.delete(name)); }
    contains(name) { return this.names.has(name); }
    toggle(name, force) {
      if (force === undefined ? !this.names.has(name) : force) this.names.add(name);
      else this.names.delete(name);
    }
  }

  class MiniElement extends EventTarget {
    constructor(tagName) {
      super();
      this.tagName = tagName.toUpperCase();
      this.dataset = {};
      this.children = [];
      this.parentElement = null;
      this.attributes = new Map();
      this.classList = new MiniClassList();
      this.listeners = new Map();
      this.bounds = { left: 0, top: 0, width: 30, height: 20, right: 30, bottom: 20 };
      this.capturedPointerId = null;
    }

    set className(value) {
      this.classList = new MiniClassList();
      String(value).split(/\s+/).filter(Boolean)
        .forEach((name) => this.classList.add(name));
    }

    get className() { return [...this.classList.names].join(" "); }
    appendChild(child) {
      child.parentElement = this;
      this.children.push(child);
      return child;
    }
    contains(candidate) {
      return candidate === this || this.children.some((child) => child.contains(candidate));
    }
    closest(selector) {
      if (selector.startsWith(".")) {
        const className = selector.slice(1);
        return this.classList.contains(className)
          ? this
          : this.parentElement?.closest(selector) || null;
      }
      const tags = selector.split(",").map((value) => value.trim().toUpperCase());
      return tags.includes(this.tagName)
        ? this
        : this.parentElement?.closest(selector) || null;
    }
    focus() {}
    addEventListener(type, listener, options) {
      if (!this.listeners.has(type)) this.listeners.set(type, []);
      this.listeners.get(type).push(listener);
      super.addEventListener(type, listener, options);
    }
    invoke(type, event) {
      for (const listener of this.listeners.get(type) || []) {
        listener.call(this, event);
      }
    }
    getBoundingClientRect() { return this.bounds; }
    setPointerCapture(pointerId) { this.capturedPointerId = pointerId; }
    hasPointerCapture(pointerId) { return this.capturedPointerId === pointerId; }
    releasePointerCapture(pointerId) {
      if (this.capturedPointerId === pointerId) this.capturedPointerId = null;
    }
    setAttribute(name, value) { this.attributes.set(name, String(value)); }
    getAttribute(name) { return this.attributes.get(name) ?? null; }
    querySelector(selector) {
      const descendants = this.querySelectorAll("*");
      if (selector === "input") {
        return descendants.find((element) => element.tagName === "INPUT") || null;
      }
      if (selector.startsWith(".")) {
        return descendants.find((element) =>
          element.classList.contains(selector.slice(1))) || null;
      }
      return null;
    }
    querySelectorAll(selector) {
      const descendants = [];
      const visit = (element) => {
        element.children.forEach((child) => {
          descendants.push(child);
          visit(child);
        });
      };
      visit(this);
      if (selector === "*") return descendants;
      if (selector === '.playlist-data-row[aria-selected="true"]') {
        return descendants.filter((element) =>
          element.classList.contains("playlist-data-row") &&
          element.getAttribute("aria-selected") === "true");
      }
      if (selector === ".dragging, .drag-allowed, .drag-before, .drag-after") {
        return descendants.filter((element) =>
          ["dragging", "drag-allowed", "drag-before", "drag-after"]
            .some((name) => element.classList.contains(name)));
      }
      return [];
    }
  }

  const sourceStart = app.indexOf("function clearPlaylistDragVisuals");
  const sourceEnd = app.indexOf("function renderPlaylist", sourceStart);
  assert.ok(sourceStart >= 0 && sourceEnd > sourceStart);
  const createHarness = new Function("document", "playlistRows", "send", "window", `
    let deferredPlaylistPlainSelectionRowId = null;
    let playlistDragGesture = null;
    let playlistMutationLocked = false;
    let playlistStructureMutationLocked = false;
    let cutDragSize = { width: 4, height: 4 };
    function dragDropPosition(event, element) {
      const bounds = element.getBoundingClientRect();
      return event.clientY < bounds.top + bounds.height / 2 ? "before" : "after";
    }
    ${app.slice(sourceStart, sourceEnd)}
    return {
      createPlaylistElement,
      state: () => ({ playlistDragGesture, deferredPlaylistPlainSelectionRowId })
    };
  `);
  let pointedElement = null;
  const document = {
    createElement: (tagName) => new MiniElement(tagName),
    elementFromPoint: () => pointedElement
  };
  const playlistRows = new MiniElement("div");
  playlistRows.bounds = { left: 0, top: 0, width: 500, height: 100,
    right: 500, bottom: 100 };
  const sent = [];
  const legacyCutDrag = require(path.join(webRoot, "legacy-cut-drag.js"));
  const harness = createHarness(
    document, playlistRows, (type, payload) => sent.push({ type, payload }),
    { LegacyCutDrag: legacyCutDrag });
  const source = harness.createPlaylistElement({ rowId: "row-a" });
  source.bounds = { left: 0, top: 0, width: 500, height: 20, right: 500, bottom: 20 };
  source.setAttribute("aria-selected", "true");
  playlistRows.appendChild(source);
  const selectedPeer = harness.createPlaylistElement({ rowId: "row-b" });
  selectedPeer.bounds = { left: 0, top: 20, width: 500, height: 20,
    right: 500, bottom: 40 };
  selectedPeer.setAttribute("aria-selected", "true");
  playlistRows.appendChild(selectedPeer);
  const target = harness.createPlaylistElement({ rowId: "row-c" });
  target.bounds = { left: 0, top: 40, width: 500, height: 20,
    right: 500, bottom: 60 };
  target.setAttribute("aria-selected", "false");
  playlistRows.appendChild(target);

  const sourceHeader = source.children[0];
  const checkbox = source.children[1].children[0];
  const targetHeader = target.children[0];
  sourceHeader.bounds = { left: 0, top: 0, width: 30, height: 20,
    right: 30, bottom: 20 };
  targetHeader.bounds = { left: 0, top: 40, width: 30, height: 20,
    right: 30, bottom: 60 };

  assert.equal(source.draggable, false);
  assert.equal(checkbox.draggable, false);
  assert.equal(source.listeners.has("pointermove"), false);
  assert.equal(sourceHeader.listeners.has("pointermove"), true);

  source.invoke("pointerdown", {
    button: 0,
    currentTarget: source,
    target: checkbox,
    ctrlKey: false,
    shiftKey: false
  });
  assert.deepEqual(sent, [{
    type: "activate-playlist-row",
    payload: { rowId: "row-a" }
  }]);
  sent.length = 0;
  assert.equal(harness.state().playlistDragGesture, null);

  const dataPointerDown = {
    button: 0, pointerId: 1, clientX: 100, clientY: 10,
    ctrlKey: false, shiftKey: false, target: source, currentTarget: source
  };
  source.invoke("pointerdown", dataPointerDown);
  assert.equal(harness.state().deferredPlaylistPlainSelectionRowId, "row-a");
  assert.equal(harness.state().playlistDragGesture, null);
  source.invoke("pointerup", { ...dataPointerDown, button: 0 });
  assert.deepEqual(sent, [{
    type: "select-playlist-row",
    payload: { rowId: "row-a", control: false, shift: false }
  }]);
  sent.length = 0;

  const pointerDown = {
    button: 0, pointerId: 7, clientX: 10, clientY: 10,
    ctrlKey: false, shiftKey: false, target: sourceHeader,
    stopPropagation() {}
  };
  sourceHeader.invoke("pointerdown", { ...pointerDown, currentTarget: sourceHeader });
  assert.deepEqual(sent, [{
    type: "activate-playlist-row",
    payload: { rowId: "row-a" }
  }]);
  sent.length = 0;
  assert.equal(sourceHeader.hasPointerCapture(7), true);
  assert.equal(harness.state().playlistDragGesture.started, false);

  // Crossing the system drag rectangle over a normal data cell starts the
  // gesture but cannot establish a drop target.
  pointedElement = target.children[2];
  sourceHeader.invoke("pointermove", {
    pointerId: 7, buttons: 1, clientX: 100, clientY: 45,
    currentTarget: sourceHeader, preventDefault() {}
  });
  assert.equal(harness.state().playlistDragGesture.started, true);
  assert.equal(harness.state().playlistDragGesture.targetRowId, null);
  sourceHeader.invoke("pointerup", {
    pointerId: 7, button: 0, clientX: 100, clientY: 45,
    currentTarget: sourceHeader, preventDefault() {}
  });
  assert.deepEqual(sent, []);

  // A second gesture dropped on another row's 30 px header sends the existing
  // stable-row C# contract exactly once.
  sourceHeader.invoke("pointerdown", { ...pointerDown, pointerId: 8,
    currentTarget: sourceHeader });
  assert.deepEqual(sent, [{
    type: "activate-playlist-row",
    payload: { rowId: "row-a" }
  }]);
  sent.length = 0;
  pointedElement = targetHeader;
  sourceHeader.invoke("pointermove", {
    pointerId: 8, buttons: 1, clientX: 10, clientY: 45,
    currentTarget: sourceHeader, preventDefault() {}
  });
  sourceHeader.invoke("pointerup", {
    pointerId: 8, button: 0, clientX: 10, clientY: 45,
    currentTarget: sourceHeader, preventDefault() {}
  });
  assert.deepEqual(sent, [{
    type: "reorder-playlist-rows",
    payload: { sourceRowId: "row-a", targetRowId: "row-c", position: "before" }
  }]);
  assert.equal(Object.hasOwn(sent[0].payload, "selectedRowIds"), false);
  assert.equal(sourceHeader.hasPointerCapture(8), false);
  assert.equal(harness.state().playlistDragGesture, null);
});

test("cut drag uses the original pointer rectangle and drops only inside the playlist", () => {
  const start = app.indexOf("function pointInsideElement");
  const end = app.indexOf("function legacyCutKey", start);
  assert.ok(start >= 0 && end > start);
  const drag = app.slice(start, end);
  assert.match(drag, /LegacyCutDrag\.createDragRectangle\(position, cutDragSize\)/);
  assert.match(drag, /LegacyCutDrag\.shouldStart\(gesture\.rectangle, pointerPosition\(event\)\)/);
  assert.match(drag, /event\.buttons & 1/);
  assert.match(drag, /pointInsideElement\(playlistDropZone, event\)/);
  assert.match(drag, /send\("drop-selected-cuts", \{\}\)/);
  assert.match(app, /state\.interactionMetrics/);
  assert.match(app, /metrics\.coordinateSpace !== "host-dip"/);
  assert.match(app, /LegacyCutDrag\.toCssDragSize/);
  assert.match(app, /window\.devicePixelRatio/);
  assert.match(app, /x: event\.clientX - bounds\.left/);
  assert.match(app, /x: Math\.round\(position\.x\)/);
  assert.doesNotMatch(app, /x: Math\.round\(event\.clientX - bounds\.left\)/);
  assert.match(app,
    /cutDragGesture\.rectangle = window\.LegacyCutDrag\.createDragRectangle/);
  assert.doesNotMatch(app, /selectedCutsDragToken|setData\("text\/plain"/);
});

test("operator catalog drag rejects an interactive child pointer origin", () => {
  class MiniDataTransfer {
    constructor() { this.values = new Map(); }
    setData(type, value) { this.values.set(type, value); }
    getData(type) { return this.values.get(type) || ""; }
  }

  const sourceStart = app.indexOf("function clearOperatorCatalogDragVisuals");
  const sourceEnd = app.indexOf("function renderOperatorCatalog", sourceStart);
  assert.ok(sourceStart >= 0 && sourceEnd > sourceStart);
  const createHarness = new Function(
    "operatorCatalogDraftRows",
    "send",
    "operatorCatalogDragMime",
    `
    let draggedOperatorCatalogRowId = null;
    let operatorCatalogDragBlockedRowId = null;
    let operatorCatalogMutationLocked = false;
    function dragDropPosition(event, element) {
      const bounds = element.getBoundingClientRect();
      return event.clientY < bounds.top + bounds.height / 2 ? "before" : "after";
    }
    ${app.slice(sourceStart, sourceEnd)}
    return {
      onOperatorCatalogPointerDown,
      onOperatorCatalogDragStart,
      state: () => ({ draggedOperatorCatalogRowId, operatorCatalogDragBlockedRowId })
    };
  `);
  const harness = createHarness(
    { querySelectorAll: () => [] },
    () => {},
    "text/x-mbn-operator-catalog-row");
  const row = {
    dataset: { operatorCatalogRowId: "catalog-draft-row-a" },
    classList: { add() {}, remove() {} },
    closest: () => null
  };
  const input = { closest: () => input };
  harness.onOperatorCatalogPointerDown({
    button: 0,
    currentTarget: row,
    target: input
  });
  const blockedTransfer = new MiniDataTransfer();
  let prevented = false;
  harness.onOperatorCatalogDragStart({
    currentTarget: row,
    target: row,
    dataTransfer: blockedTransfer,
    preventDefault: () => { prevented = true; }
  });
  assert.equal(prevented, true);
  assert.equal(
    blockedTransfer.getData("text/x-mbn-operator-catalog-row"),
    "");
  assert.deepEqual(harness.state(), {
    draggedOperatorCatalogRowId: null,
    operatorCatalogDragBlockedRowId: null
  });

  harness.onOperatorCatalogPointerDown({
    button: 0,
    currentTarget: row,
    target: row
  });
  const allowedTransfer = new MiniDataTransfer();
  harness.onOperatorCatalogDragStart({
    currentTarget: row,
    target: row,
    dataTransfer: allowedTransfer,
    preventDefault: () => { throw new Error("non-interactive drag was rejected"); }
  });
  assert.equal(
    allowedTransfer.getData("text/x-mbn-operator-catalog-row"),
    "catalog-draft-row-a");
});

test("operator catalog stock result rendering preserves keyed button identity", () => {
  class MiniElement {
    constructor(tagName) {
      this.tagName = tagName;
      this.dataset = {};
      this.className = "";
      this.attributes = new Map();
      this.parentElement = null;
      this.children = [];
      this.children.item = index => this.children[index] ?? null;
    }
    append(...children) {
      for (const child of children) this.insertBefore(child, null);
    }
    insertBefore(child, reference) {
      let insertionIndex = reference == null
        ? this.children.length
        : this.children.indexOf(reference);
      if (insertionIndex < 0) throw new Error("reference is not a child");
      if (child.parentElement) {
        const oldParent = child.parentElement;
        const oldIndex = oldParent.children.indexOf(child);
        oldParent.children.splice(oldIndex, 1);
        if (oldParent === this && oldIndex < insertionIndex) insertionIndex--;
      }
      this.children.splice(insertionIndex, 0, child);
      child.parentElement = this;
      return child;
    }
    querySelector(selector) {
      if (!selector.startsWith(".")) return null;
      const className = selector.slice(1);
      return this.children.find(child =>
        child.className.split(/\s+/).includes(className)) || null;
    }
    querySelectorAll(selector) {
      if (selector === "button[data-operator-catalog-stock-result-id]") {
        return this.children.filter(child =>
          child.dataset.operatorCatalogStockResultId !== undefined);
      }
      return [];
    }
    setAttribute(name, value) { this.attributes.set(name, value); }
    getAttribute(name) { return this.attributes.get(name) ?? null; }
    remove() {
      if (!this.parentElement) return;
      const siblings = this.parentElement.children;
      siblings.splice(siblings.indexOf(this), 1);
      this.parentElement = null;
    }
  }

  const helperStart = app.indexOf("function reconcileOperatorCatalogStockResults");
  const helperEnd = app.indexOf("function renderOperatorCatalog", helperStart);
  assert.ok(helperStart >= 0 && helperEnd > helperStart);
  const container = new MiniElement("div");
  const document = { createElement: tagName => new MiniElement(tagName) };
  const reconcile = new Function(
    "operatorCatalogStockResults",
    "document",
    "configureRovingList",
    `${app.slice(helperStart, helperEnd)}; return reconcileOperatorCatalogStockResults;`
  )(container, document, () => {});
  const rowA = {
    resultId: "stock-a", displayName: "삼성전자", market: "KRX",
    isSelected: false, isCompatible: true
  };
  const rowB = {
    resultId: "stock-b", displayName: "SK하이닉스", market: "KRX",
    isSelected: false, isCompatible: true
  };

  reconcile([rowA, rowB]);
  const firstButton = container.children[0];
  const secondButton = container.children[1];
  reconcile([{ ...rowA, isSelected: true }, rowB]);
  assert.strictEqual(container.children[0], firstButton);
  assert.strictEqual(container.children[1], secondButton);
  assert.equal(firstButton.className, "selected");

  reconcile([rowB, rowA]);
  assert.strictEqual(container.children[0], secondButton);
  assert.strictEqual(container.children[1], firstButton);
  reconcile([rowB]);
  assert.deepEqual(Array.from(container.children), [secondButton]);
  assert.equal(firstButton.parentElement, null);
});

async function runOperatorCatalogStockDoubleClickContract(interClickDelayMilliseconds) {
  class DelegateTarget {
    constructor() { this.listeners = new Map(); }
    addEventListener(type, callback) { this.listeners.set(type, callback); }
    fire(type, target, detail) {
      let prevented = false;
      this.listeners.get(type)({
        target,
        detail,
        preventDefault() { prevented = true; }
      });
      return prevented;
    }
  }

  const listenerStart = app.indexOf(
    'operatorCatalogStockResults.addEventListener("dblclick"');
  const listenerEnd = app.indexOf(
    'operatorCatalogAddStock.addEventListener("click"', listenerStart);
  assert.ok(listenerStart >= 0 && listenerEnd > listenerStart);
  const stockResults = new DelegateTarget();
  const sent = [];
  new Function(
    "operatorCatalogStockResults",
    "send",
    app.slice(listenerStart, listenerEnd)
  )(stockResults, (type, payload) => sent.push({ type, payload }));
  const row = {
    disabled: false,
    dataset: { operatorCatalogStockResultId: "stock-005930" },
    closest(selector) {
      return selector === "button[data-operator-catalog-stock-result-id]" ? this : null;
    }
  };

  stockResults.fire("click", row, 1);
  await new Promise(resolve => setTimeout(resolve, interClickDelayMilliseconds));
  stockResults.fire("click", row, 2);
  assert.equal(stockResults.fire("dblclick", row, 2), true);
  assert.deepEqual(sent, [
    {
      type: "select-operator-catalog-stock",
      payload: { resultId: "stock-005930" }
    },
    {
      type: "activate-operator-catalog-stock",
      payload: { resultId: "stock-005930", buyAmount: null }
    }
  ]);
}

test("operator catalog fast stock double-click selects then activates exactly once", async () => {
  await runOperatorCatalogStockDoubleClickContract(10);
});

test("operator catalog slow Windows stock double-click selects then activates exactly once", async () => {
  await runOperatorCatalogStockDoubleClickContract(350);
});

test("market tabs send closed identity intents and reconcile C# order", () => {
  assert.match(app, /send\("select-tab"/);
  assert.match(app, /send\("hover-swap-tabs"/);
  assert.match(app, /expectedSourceIndex: sourceIndex/);
  assert.match(app, /expectedTargetIndex: targetIndex/);
  assert.doesNotMatch(app, /send\("swap-tabs"/);
  assert.match(app, /dataset\.tabId/);
  assert.match(app, /const existingTabs = new Map\(\)/);
  assert.match(app, /marketTabs\.insertBefore/);
  assert.match(app, /button\.querySelector\("\.workspace-nav-label"\)/);
  assert.match(app, /LegacyWorkspaceNavigation\?\.syncMarket/);
  assert.doesNotMatch(app, /selectedTab\s*=/);
});

test("market tab hover uses the original narrow-source width threshold in both directions", () => {
  const sourceStart = app.indexOf("function hasCrossedLegacyTabSwapThreshold");
  const sourceEnd = app.indexOf("function onMarketTabDragOver", sourceStart);
  assert.ok(sourceStart >= 0 && sourceEnd > sourceStart);
  const crossed = new Function(`
    ${app.slice(sourceStart, sourceEnd)}
    return hasCrossedLegacyTabSwapThreshold;
  `)();
  const tab = (left, width) => ({
    getBoundingClientRect() { return { left, width }; }
  });
  const narrowSource = tab(0, 40);
  const wideTarget = tab(100, 100);

  // Moving right: target.right - source.width = 160, with a strict > comparison.
  assert.equal(crossed(narrowSource, wideTarget, 159, 0, 1), false);
  assert.equal(crossed(narrowSource, wideTarget, 160, 0, 1), false);
  assert.equal(crossed(narrowSource, wideTarget, 161, 0, 1), true);

  // Moving left: target.left + source.width = 140, with a strict < comparison.
  assert.equal(crossed(narrowSource, wideTarget, 141, 2, 1), false);
  assert.equal(crossed(narrowSource, wideTarget, 140, 2, 1), false);
  assert.equal(crossed(narrowSource, wideTarget, 139, 2, 1), true);

  // Equal-width and wider sources keep MainForm's immediate-on-entry behavior.
  assert.equal(crossed(tab(0, 100), wideTarget, 100, 0, 1), true);
  assert.equal(crossed(tab(0, 120), wideTarget, 200, 2, 1), true);
});

test("market tab dragover sends once after crossing and does not poison the busy gate", () => {
  const sourceStart = app.indexOf("function hasCrossedLegacyTabSwapThreshold");
  const sourceEnd = app.indexOf("function requestTabHoverSwap", sourceStart);
  assert.ok(sourceStart >= 0 && sourceEnd > sourceStart);

  const source = {
    dataset: { tabId: "source" },
    closest() { return this; },
    getBoundingClientRect() { return { left: 0, width: 40 }; }
  };
  const target = {
    dataset: { tabId: "target" },
    closest() { return this; },
    getBoundingClientRect() { return { left: 100, width: 100 }; }
  };
  const marketTabs = {
    querySelectorAll() { return [source, target]; }
  };
  let accepted = false;
  const requests = [];
  const harness = new Function(
    "marketTabs",
    "requestTabHoverSwap",
    `
      let draggedTabId = "source";
      let tabDragLastTargetId = null;
      let pendingTabHoverSwap = null;
      ${app.slice(sourceStart, sourceEnd)}
      return {
        dragover: onMarketTabDragOver,
        lastTarget: function () { return tabDragLastTargetId; },
        setPending: function (value) { pendingTabHoverSwap = value; }
      };
    `)(marketTabs, targetId => {
      requests.push(targetId);
      return accepted;
    });
  const dragover = (eventTarget, clientX) => {
    let prevented = false;
    const dataTransfer = { dropEffect: "none" };
    harness.dragover({
      target: eventTarget,
      clientX,
      dataTransfer,
      preventDefault() { prevented = true; }
    });
    assert.equal(prevented, true);
    assert.equal(dataTransfer.dropEffect, "move");
  };

  dragover(target, 159);
  assert.deepEqual(requests, []);
  assert.equal(harness.lastTarget(), null);

  // The threshold is crossed while the request gate rejects the operation.
  dragover(target, 161);
  assert.deepEqual(requests, ["target"]);
  assert.equal(harness.lastTarget(), null);

  // Once accepted, only the first repeated dragover for this crossing is sent.
  accepted = true;
  dragover(target, 170);
  dragover(target, 180);
  assert.deepEqual(requests, ["target", "target"]);
  assert.equal(harness.lastTarget(), "target");

  // Re-entering the dragged source ends that crossing; a later target crossing is new.
  dragover(source, 20);
  assert.equal(harness.lastTarget(), null);
  dragover(target, 170);
  assert.deepEqual(requests, ["target", "target", "target"]);

  harness.setPending({});
  dragover(source, 20);
  dragover(target, 170);
  assert.deepEqual(requests, ["target", "target", "target"]);
});

test("fixed catalog sends opaque leaf and parent batch intents", () => {
  assert.match(app, /state\.fixedCatalog/);
  assert.match(app, /dataset\.actionId/);
  assert.match(app, /dataset\.sectionIndex/);
  assert.match(app, /send\("activate-fixed-action"/);
  assert.match(app, /send\("activate-fixed-section"/);
  assert.match(app, /send\("activate-industry-section"/);
  assert.match(app, /send\("activate-comparison-section"/);
  assert.match(app, /send\("set-moving-averages"/);
  assert.doesNotMatch(app, /builderKey|sceneCode|cutCode|dataCode/);
});

test("UC1 tree separates label selection, disclosure toggling and double-click action", () => {
  const doubleClickStart = app.indexOf('categoryTree.addEventListener("dblclick"');
  const clickStart = app.indexOf(
    'categoryTree.addEventListener("click"', doubleClickStart + 1);
  const changeStart = app.indexOf(
    'categoryTree.addEventListener("change"', clickStart + 1);
  assert.ok(doubleClickStart >= 0 && clickStart > doubleClickStart && changeStart > clickStart);

  class DelegateTarget {
    constructor() { this.listeners = new Map(); }
    addEventListener(type, callback) { this.listeners.set(type, callback); }
    fire(type, target, detail = 1) {
      let prevented = false;
      this.listeners.get(type)({
        target,
        detail,
        preventDefault() { prevented = true; }
      });
      return prevented;
    }
  }

  function createNodes() {
    const details = { open: true };
    const summary = {
      dataset: { treeNodeKey: "root|overseas|fixed:0", sectionIndex: "0" },
      disabled: false,
      parentElement: details,
      getAttribute(name) { return name === "aria-disabled" ? "false" : null; }
    };
    const disclosure = {
      closest(selector) {
        if (selector === "[data-tree-node-key]" ||
            selector === "summary[data-tree-root]" ||
            selector === "summary[data-section-index]") return summary;
        if (selector === "[data-tree-disclosure]") return this;
        return null;
      }
    };
    const label = {
      closest(selector) {
        return selector === "[data-tree-node-key]" ||
          selector === "summary[data-tree-root]" ||
          selector === "summary[data-section-index]"
          ? summary : null;
      }
    };
    const leaf = {
      dataset: { treeNodeKey: "leaf|overseas|fixed:0|fixed-262", actionId: "fixed-262" },
      disabled: false,
      getAttribute() { return null; },
      closest(selector) {
        return selector === "[data-tree-node-key]" ||
          selector === ".tree-item[data-tree-node-key]" ||
          selector === "button[data-action-id]"
          ? this : null;
      }
    };
    return { details, summary, disclosure, label, leaf };
  }

  function createHarness(locked) {
    const categoryTree = new DelegateTarget();
    const sent = [];
    const selected = [];
    const expanded = [];
    new Function(
      "categoryTree", "send", "selectTreeNode", "syncTreeSectionExpanded",
      "treeInteractionLocked",
      app.slice(doubleClickStart, clickStart) + app.slice(clickStart, changeStart))(
      categoryTree,
      (type, payload) => sent.push({ type, payload }),
      node => selected.push(node.dataset.treeNodeKey),
      details => expanded.push(details.open),
      locked);
    return { categoryTree, sent, selected, expanded };
  }

  const nodes = createNodes();
  const harness = createHarness(false);
  assert.equal(harness.categoryTree.fire("click", nodes.label), true);
  assert.equal(nodes.details.open, true);
  assert.deepEqual(harness.sent, []);

  assert.equal(harness.categoryTree.fire("click", nodes.disclosure), true);
  assert.equal(nodes.details.open, false);
  assert.deepEqual(harness.expanded, [false]);

  harness.categoryTree.fire("dblclick", nodes.label, 2);
  harness.categoryTree.fire("dblclick", nodes.leaf, 2);
  assert.equal(nodes.details.open, false);
  assert.deepEqual(harness.sent, [
    { type: "activate-fixed-section", payload: { sectionIndex: 0 } },
    { type: "activate-fixed-action", payload: { actionId: "fixed-262" } }
  ]);

  const lockedNodes = createNodes();
  const locked = createHarness(true);
  locked.categoryTree.fire("click", lockedNodes.disclosure);
  locked.categoryTree.fire("dblclick", lockedNodes.label, 2);
  locked.categoryTree.fire("dblclick", lockedNodes.leaf, 2);
  assert.equal(lockedNodes.details.open, true);
  assert.deepEqual(locked.sent, []);
  assert.deepEqual(locked.selected, []);
});

test("UC1 tree render preserves state but resets Expand and first root on tab reentry", () => {
  const helperStart = app.indexOf("function activeTreeTabId");
  const helperEnd = app.indexOf("function send(", helperStart);
  assert.ok(helperStart >= 0 && helperEnd > helperStart);

  class MiniClassList {
    constructor() { this.values = new Set(); }
    add(value) { this.values.add(value); }
    remove(value) { this.values.delete(value); }
    contains(value) { return this.values.has(value); }
  }

  const document = { activeElement: null };
  class MiniNode {
    constructor(dataset = {}) {
      this.dataset = dataset;
      this.classList = new MiniClassList();
      this.attributes = new Map();
      this.disabled = false;
      this.textContent = "";
      this.scrolled = false;
    }
    setAttribute(name, value) { this.attributes.set(name, String(value)); }
    getAttribute(name) { return this.attributes.get(name) ?? null; }
    removeAttribute(name) { this.attributes.delete(name); }
    focus() { document.activeElement = this; }
    scrollIntoView() { this.scrolled = true; }
    closest(selector) {
      if (selector === "[data-tree-node-key]") return this;
      if (selector === "details[data-tree-section-key]") return this.parentElement || null;
      if (selector === "[data-roving-tree=\"true\"]") return this.tree || null;
      return null;
    }
  }

  class MiniDetails {
    constructor(sectionIndex, leafId) {
      this.dataset = {};
      this.open = false;
      this.summary = new MiniNode({ sectionIndex: String(sectionIndex) });
      this.summary.textContent = "[section-" + sectionIndex + "]";
      this.summary.parentElement = this;
      this.summary.querySelector = selector => selector === "[data-tree-disclosure]" ? {} : null;
      this.leaf = new MiniNode({ actionId: leafId });
      this.leaf.textContent = leafId;
      this.leaf.parentElement = this;
    }
    querySelector(selector) {
      return selector.startsWith(":scope > summary") ? this.summary : null;
    }
    querySelectorAll(selector) { return selector === ".tree-item" ? [this.leaf] : []; }
  }

  class MiniTree {
    constructor() {
      this.dataset = {};
      this.attributes = new Map();
      this.details = [];
      this.scrollTop = 0;
    }
    replaceWith(details) {
      this.details = details;
      this.details.forEach(item => {
        item.parentElement = this;
        item.summary.tree = this;
        item.leaf.tree = this;
      });
    }
    querySelectorAll(selector) {
      if (selector === "details" || selector === "details[data-tree-section-key]") {
        return this.details;
      }
      if (selector === "summary[data-tree-root]") {
        return this.details.map(details => details.summary);
      }
      const nodes = this.details.flatMap(details => [details.summary, details.leaf]);
      if (selector === "[data-tree-node-key]") return nodes;
      if (selector === "[data-tree-node-key].tree-node-selected") {
        return nodes.filter(node => node.classList.contains("tree-node-selected"));
      }
      if (selector === "[data-roving-tree=\"true\"]") return [];
      return [];
    }
    querySelector(selector) {
      if (selector === "[data-tree-node-key].tree-node-selected") {
        return this.querySelectorAll(selector)[0] || null;
      }
      return null;
    }
    contains(node) {
      return this.details.some(details => details.summary === node || details.leaf === node);
    }
    setAttribute(name, value) { this.attributes.set(name, String(value)); }
    removeAttribute(name) { this.attributes.delete(name); }
  }

  const categoryTree = new MiniTree();
  const catalogExpandAll = { checked: false };
  const harness = new Function(
    "categoryTree", "catalogExpandAll", "document", "modalFocusManager",
    "configureRovingList", "setRovingListTabStop", `
    let renderedTreeTabId = null;
    let treeInteractionLocked = false;
    let treeFocusOwnedBeforeRender = false;
    let themeResultClickTimer = null;
    let themeResultPendingSelection = null;
    let comparisonTargetPendingSelection = null;
    const treeUiStateByTab = new Map();
    const pendingTreeResetTabs = new Set();
    function clearDelayedClick() {}
    function clearThemeResultPendingSelection() {}
    function clearComparisonTargetPendingSelection() {}
    ${app.slice(helperStart, helperEnd)}
    return { beginTreeRender, finishTreeRender, selectTreeNode };
  `)(
    categoryTree,
    catalogExpandAll,
    document,
    { getActiveModal() { return null; } },
    (list, rows, selected) => {
      list.dataset.rovingList = "true";
      rows.forEach(row => { row.dataset.rovingRow = "true"; });
      if (selected) selected.tabIndex = 0;
    },
    () => true);
  const state = tabId => ({
    isBusy: false,
    fixedSectionBatch: null,
    tabs: [{ id: tabId, isActive: true }]
  });
  const sections = () => [new MiniDetails(0, "fixed-1"), new MiniDetails(1, "fixed-2")];

  let current = sections();
  categoryTree.replaceWith(current);
  harness.finishTreeRender(harness.beginTreeRender(state("overseas")));
  assert.equal(catalogExpandAll.checked, true);
  assert.ok(current.every(details => details.open));
  assert.ok(current[0].summary.classList.contains("tree-node-selected"));
  assert.equal(document.activeElement, current[0].summary);
  assert.equal(current[0].summary.scrolled, true);

  current[0].open = false;
  harness.selectTreeNode(current[0].leaf, true);
  categoryTree.scrollTop = 42;
  const sameTab = harness.beginTreeRender(state("overseas"));
  current = sections();
  categoryTree.replaceWith(current);
  harness.finishTreeRender(sameTab);
  assert.equal(current[0].open, false);
  assert.equal(current[1].open, true);
  assert.ok(current[0].leaf.classList.contains("tree-node-selected"));
  assert.equal(document.activeElement, current[0].leaf);
  assert.equal(categoryTree.scrollTop, 42);

  catalogExpandAll.checked = false;
  const reentry = harness.beginTreeRender(state("exchange"));
  current = sections();
  categoryTree.replaceWith(current);
  harness.finishTreeRender(reentry);
  assert.equal(catalogExpandAll.checked, true);
  assert.ok(current.every(details => details.open));
  assert.ok(current[0].summary.classList.contains("tree-node-selected"));
  assert.equal(document.activeElement, current[0].summary);
});

test("UC1 Expand collapses all without clearing selection and is inert while locked", () => {
  const expandStart = app.indexOf('catalogExpandAll.addEventListener("change"');
  const doubleClickStart = app.indexOf(
    'categoryTree.addEventListener("dblclick"', expandStart + 1);
  assert.ok(expandStart >= 0 && doubleClickStart > expandStart);

  function createHarness(locked) {
    const selected = { value: true };
    const details = [{ open: true }, { open: true }];
    const categoryTree = {
      querySelectorAll(selector) { return selector === "details" ? details : []; }
    };
    const catalogExpandAll = {
      checked: false,
      addEventListener(type, callback) { this.change = callback; }
    };
    const expanded = [];
    new Function(
      "catalogExpandAll", "categoryTree", "syncTreeSectionExpanded",
      "treeInteractionLocked", app.slice(expandStart, doubleClickStart))(
      catalogExpandAll, categoryTree, row => expanded.push(row.open), locked);
    return { catalogExpandAll, details, expanded, selected };
  }

  const normal = createHarness(false);
  normal.catalogExpandAll.change();
  assert.deepEqual(normal.details.map(details => details.open), [false, false]);
  assert.deepEqual(normal.expanded, [false, false]);
  assert.equal(normal.selected.value, true);

  const locked = createHarness(true);
  locked.catalogExpandAll.change();
  assert.deepEqual(locked.details.map(details => details.open), [true, true]);
  assert.equal(locked.catalogExpandAll.checked, true);
  assert.deepEqual(locked.expanded, []);
  assert.equal(locked.selected.value, true);
});

test("fixed-262 ignores a single click and emits exactly one intent on double click", () => {
  const doubleClickStart = app.indexOf('categoryTree.addEventListener("dblclick"');
  const clickStart = app.indexOf(
    'categoryTree.addEventListener("click"', doubleClickStart + 1);
  const changeStart = app.indexOf(
    'categoryTree.addEventListener("change"', clickStart + 1);
  assert.ok(doubleClickStart >= 0 && clickStart > doubleClickStart && changeStart > clickStart);

  class FixedActionElement extends EventTarget {
    constructor() {
      super();
      this.dataset = { actionId: "fixed-262" };
      this.disabled = false;
    }

    closest(selector) {
      return selector === "button[data-action-id]" ? this : null;
    }
  }

  const categoryTree = new FixedActionElement();
  const sent = [];
  const send = (type, payload) => sent.push({ type, payload });
  new Function("categoryTree", "send", app.slice(doubleClickStart, clickStart))(
    categoryTree, send);
  new Function("categoryTree", "send", app.slice(clickStart, changeStart))(
    categoryTree, send);

  categoryTree.dispatchEvent(new Event("click", { bubbles: true, cancelable: true }));
  assert.deepEqual(sent, []);

  categoryTree.dispatchEvent(new Event("dblclick", { bubbles: true, cancelable: true }));
  assert.deepEqual(sent, [{
    type: "activate-fixed-action",
    payload: { actionId: "fixed-262" }
  }]);
});

test("industry workspace sends index and opaque action intents only", () => {
  const renderStart = app.indexOf("function renderIndustry");
  const renderEnd = app.indexOf("function renderOverseas", renderStart);
  const industryRender = app.slice(renderStart, renderEnd);
  assert.match(app, /state\.industry/);
  assert.match(app, /dataset\.industryIndex/);
  assert.match(app, /dataset\.industryActionId/);
  assert.match(app, /send\("select-industry"/);
  assert.match(app, /send\("double-click-industry"/);
  assert.match(app, /send\("swap-industries"/);
  assert.match(app, /send\("set-industry-comparison-text"/);
  assert.match(app, /send\("activate-industry-action"/);
  assert.doesNotMatch(app, /swap\.textContent = "↕ 교환"/);
  assert.match(app,
    /summary\.textContent = industry\.market === "kospi" \? "코스피" : "코스닥"/);
  assert.match(app, /summary\.dataset\.industrySectionIndex = "0"/);
  assert.match(industryRender, /markVisualCardList\(actionCards, "industry-actions"\)/);
  assert.match(industryRender, /setVisualCardContent\(button, \{/);
  assert.match(industryRender, /button\.dataset\.industryActionId = action\.actionId/);
  assert.doesNotMatch(industryRender, /meta:\s*action\.category/);
  assert.doesNotMatch(app, /const sections = new Map\(\)/);
  assert.doesNotMatch(industryRender, /first\.readOnly = true/);
  assert.doesNotMatch(industryRender, /second\.readOnly = true/);
  assert.match(industryRender, /first\.maxLength = 128/);
  assert.match(industryRender, /second\.maxLength = 128/);
});

test("visual card renderers stay text-first and exclude narrow overseas actions", () => {
  const helperStart = app.indexOf("function setVisualCardContent");
  const helperEnd = app.indexOf("function treeLeafIdentity", helperStart);
  assert.ok(helperStart >= 0 && helperEnd > helperStart);
  const helper = app.slice(helperStart, helperEnd);
  assert.match(helper, /button\.setAttribute\("aria-label", titleText\)/);
  assert.doesNotMatch(helper,
    /visual-card-(?:kicker|preview|state)|visualCardPreview|content\.preview/,
    "a card without unique media must not invent a repeated preview or state ornament");

  const cardRenderers = [
    ["fixed", "function renderFixedCatalog", "function markIndustryRowSelected"],
    ["industry", "function renderIndustry", "function updateOverseasWorkspace"],
    ["comparison", "function renderComparison", "function themeActionIsClickable"],
    ["theme", "function renderTheme", "function updateExpertWorkspace"],
    ["expert", "function renderExpert", "function updateTradingHaltWorkspace"],
    ["halt", "function renderTradingHalt", "const manualFinancialReferenceImages"]
  ];
  for (const [name, startMarker, endMarker] of cardRenderers) {
    const start = app.indexOf(startMarker);
    const end = app.indexOf(endMarker, start);
    assert.ok(start >= 0 && end > start, `${name} renderer slice must exist`);
    const renderer = app.slice(start, end);
    assert.match(renderer, /setVisualCardContent\(button, \{/,
      `${name} action buttons must use the visual-card helper`);
    assert.doesNotMatch(renderer, /\bkicker:|\bpreview:\s*true/,
      `${name} cards must not repeat their section label or a generic preview`);
  }

  const overseasStart = app.indexOf("function renderOverseas");
  const overseasEnd = app.indexOf("function updateComparisonWorkspace", overseasStart);
  const overseas = app.slice(overseasStart, overseasEnd);
  assert.match(overseas,
    /button\.dataset\.overseasActionId = action\.actionId;\s*button\.textContent = label;/);
  assert.doesNotMatch(overseas, /setVisualCardContent|markVisualCardList/);
});

test("UC2 row selection is immediate and its state receipt preserves the double-click target", () => {
  const helperStart = app.indexOf("function markIndustryRowSelected");
  const helperEnd = app.indexOf("function renderIndustry", helperStart);
  assert.ok(helperStart >= 0 && helperEnd > helperStart);

  function row(index) {
    return {
      id: `industry-row-${index}`,
      dataset: { industryIndex: String(index) },
      list: null,
      selected: false,
      classList: {
        toggle(_name, selected) { this.owner.selected = selected; },
        owner: null
      },
      setAttribute(name, value) {
        if (name === "aria-selected") this.ariaSelected = value;
      },
      closest(selector) {
        return selector === ".industry-list" ? this.list : null;
      }
    };
  }
  const first = row(0);
  const second = row(1);
  first.classList.owner = first;
  second.classList.owner = second;
  const categoryTree = {
    querySelectorAll() { return [first, second]; }
  };
  const industryList = {
    focus(options) { this.focusOptions = options; },
    setAttribute(name, value) {
      if (name === "aria-activedescendant") this.activeDescendant = value;
    }
  };
  first.list = industryList;
  second.list = industryList;
  const sent = [];
  const helpers = new Function(
    "categoryTree",
    "send",
    `
      ${app.slice(helperStart, helperEnd)}
      return { selectIndustryRow, activateIndustryRow };
    `)(
    categoryTree,
    (type, payload) => sent.push({ type, payload }));

  helpers.selectIndustryRow(second);
  assert.equal(second.selected, true);
  assert.equal(first.selected, false);
  assert.deepEqual(sent, [
    { type: "select-industry", payload: { index: 1 } }
  ]);
  helpers.activateIndustryRow(second);
  assert.deepEqual(sent[1], {
    type: "double-click-industry", payload: { index: 1 }
  });

  const renderStart = app.indexOf("function renderIndustry");
  const renderEnd = app.indexOf("function renderOverseas", renderStart);
  const render = app.slice(renderStart, renderEnd);
  assert.match(render, /structureKey === industryRenderStructureKey/);
  assert.match(render, /existingWorkspace\.querySelectorAll/);
  assert.match(render, /previousList\.scrollTop/);
  assert.match(render, /list\.scrollTop = previousScrollTop/);
  assert.match(render, /replacement\.focus\(\{ preventScroll: true \}\)/);
});

test("comparison workspace sends opaque target, pair and action intents", () => {
  for (const intent of [
    "search-comparison-domestic", "search-comparison-world",
    "select-comparison-domestic", "select-comparison-world",
    "select-comparison-fixed",
    "choose-comparison-domestic", "choose-comparison-world",
    "choose-comparison-fixed", "swap-comparison-targets",
    "clear-comparison-targets", "add-comparison-pair",
    "select-comparison-pair", "move-comparison-pair",
    "delete-selected-comparison-pair", "delete-all-comparison-pairs",
    "activate-comparison-action"
  ]) assert.match(app, new RegExp("\\\"" + intent + "\\\""));
  assert.match(app, /dataset\.comparisonSelectionId/);
  assert.match(app, /dataset\.comparisonPairId/);
  assert.match(app, /dataset\.comparisonActionId/);
  for (const label of ["◀▶", "추가", "UP", "DOWN", "DEL", "DEL ALL"]) {
    assert.match(app, new RegExp("\\\"" + label + "\\\""));
  }
  assert.doesNotMatch(app, /clear\.dataset\.comparisonClear/);
  assert.doesNotMatch(app, /window\.confirm\("저장된 종목 비교쌍을 모두 삭제하겠습니까\?"\)/);
  assert.match(app, /\? "confirm-native-dialog"/);
  assert.match(app, /send\("cancel-native-dialog", \{\}\)/);
  assert.doesNotMatch(app, /send\([^\n]*stockCode/);
  assert.match(app, /function comparisonUnavailableText\(reason\)/);
  assert.match(app, /국내 종목 두 개를 선택해야 합니다/);
  assert.match(app, /:\s*reason;\s*\n\s*}\s*\n\s*}\s*\n\s*\n\s*function renderComparison/);
  const comparisonStart = app.indexOf("function renderComparison");
  const comparisonEnd = app.indexOf("function renderTheme", comparisonStart);
  const comparisonRender = app.slice(comparisonStart, comparisonEnd);
  assert.doesNotMatch(comparisonRender, /meta:\s*action\.unavailableReason/);
  assert.match(comparisonRender,
    /comparisonUnavailableText\(action\.unavailableReason\)/);
});

test("theme workspace sends query, opaque selection, sort and action intents", () => {
  assert.match(app, /state\.theme/);
  assert.match(app, /dataset\.themeResultId/);
  assert.match(app, /dataset\.themeActionId/);
  assert.match(app, /send\("search-themes"/);
  assert.match(app, /send\("select-theme"/);
  assert.match(app, /send\("set-theme-sort"/);
  assert.match(app, /send\("activate-theme-action"/);
});

test("expert and trading halt workspaces send opaque selection and action intents", () => {
  for (const intent of [
    "search-experts", "select-expert", "activate-expert-action",
    "search-trading-halts", "select-trading-halt", "toggle-trading-halt-sort",
    "activate-trading-halt-action"
  ]) assert.match(app, new RegExp("send\\(\\\"" + intent + "\\\""));
  assert.match(app, /dataset\.expertSelectionId/);
  assert.match(app, /dataset\.haltSelectionId/);
});

test("theme expert and trading halt actions preserve original single click wiring", () => {
  const doubleClickStart = app.indexOf('categoryTree.addEventListener("dblclick"');
  const clickStart = app.indexOf('categoryTree.addEventListener("click"', doubleClickStart + 1);
  const changeStart = app.indexOf('categoryTree.addEventListener("change"', clickStart + 1);
  assert.ok(doubleClickStart >= 0 && clickStart > doubleClickStart && changeStart > clickStart);
  const doubleClickDelegate = app.slice(doubleClickStart, clickStart);
  const clickDelegate = app.slice(clickStart, changeStart);
  for (const intent of [
    "activate-theme-action",
    "activate-expert-action",
    "activate-trading-halt-action"
  ]) {
    assert.match(clickDelegate, new RegExp("send\\(\\\"" + intent + "\\\""));
    assert.doesNotMatch(doubleClickDelegate, new RegExp("send\\(\\\"" + intent + "\\\""));
    assert.equal(app.split('send("' + intent + '"').length - 1, 1);
  }
  assert.match(doubleClickDelegate, /activateIndustryRow\(industryRow\)/);
  for (const intent of [
    "choose-comparison-domestic", "choose-comparison-world", "choose-comparison-fixed"
  ]) {
    assert.match(doubleClickDelegate, new RegExp("send\\(.*\\\"" + intent + "\\\"", "s"));
    assert.doesNotMatch(clickDelegate, new RegExp("send\\(.*\\\"" + intent + "\\\"", "s"));
  }
  for (const intent of [
    "select-comparison-domestic", "select-comparison-world", "select-comparison-fixed"
  ]) {
    assert.match(clickDelegate, new RegExp("send\\(.*\\\"" + intent + "\\\"", "s"));
    assert.doesNotMatch(doubleClickDelegate, new RegExp("send\\(.*\\\"" + intent + "\\\"", "s"));
  }
  assert.match(clickDelegate, /send\("select-theme"/);
  assert.match(clickDelegate, /send\("select-expert"/);
  assert.match(clickDelegate, /send\("select-trading-halt"/);
});

test("visual card child spans preserve single and double click intent counts", () => {
  const doubleClickStart = app.indexOf('categoryTree.addEventListener("dblclick"');
  const clickStart = app.indexOf('categoryTree.addEventListener("click"', doubleClickStart + 1);
  const changeStart = app.indexOf('categoryTree.addEventListener("change"', clickStart + 1);
  assert.ok(doubleClickStart >= 0 && clickStart > doubleClickStart && changeStart > clickStart);

  const listeners = {};
  const selected = [];
  const sent = [];
  const categoryTree = {
    addEventListener(type, listener) { listeners[type] = listener; },
    querySelector() { return null; }
  };
  const send = (type, payload) => {
    sent.push({ type, payload });
    return true;
  };
  const install = new Function(
    "categoryTree", "send", "treeInteractionLocked", "selectTreeNode",
    app.slice(doubleClickStart, changeStart));
  install(categoryTree, send, false, node => selected.push(node));

  function nestedTarget(button, actionSelector, treeNode) {
    return {
      className: "visual-card-title",
      closest(selector) {
        if (treeNode && (selector === "[data-tree-node-key]" ||
            selector === ".tree-item[data-tree-node-key]")) return button;
        return selector === actionSelector ? button : null;
      }
    };
  }
  function inputEvent(target) {
    return {
      target,
      detail: 1,
      preventDefault() {}
    };
  }

  for (const action of [
    ["button[data-action-id]", "actionId", "fixed-card", "activate-fixed-action"],
    ["button[data-industry-action-id]", "industryActionId", "industry-card",
      "activate-industry-action"],
    ["button[data-comparison-action-id]", "comparisonActionId", "comparison-card",
      "activate-comparison-action"]
  ]) {
    const [selector, datasetKey, id, intent] = action;
    const button = {
      dataset: { [datasetKey]: id, treeNodeKey: id },
      disabled: false,
      getAttribute() { return null; }
    };
    const target = nestedTarget(button, selector, true);
    const beforeSelected = selected.length;
    const beforeSent = sent.length;
    listeners.click(inputEvent(target));
    assert.equal(selected.length, beforeSelected + 1);
    assert.equal(sent.length, beforeSent, `${id} single click must only select`);
    listeners.dblclick(inputEvent(target));
    assert.equal(sent.length, beforeSent + 1);
    assert.deepEqual(sent.at(-1), { type: intent, payload: { actionId: id } });
  }

  for (const action of [
    ["button[data-theme-action-id]", "themeActionId", "theme-card",
      "activate-theme-action"],
    ["button[data-expert-action-id]", "expertActionId", "expert-card",
      "activate-expert-action"],
    ["button[data-halt-action-id]", "haltActionId", "halt-card",
      "activate-trading-halt-action"]
  ]) {
    const [selector, datasetKey, id, intent] = action;
    const button = { dataset: { [datasetKey]: id }, disabled: false };
    const target = nestedTarget(button, selector, false);
    const before = sent.length;
    listeners.click(inputEvent(target));
    assert.equal(sent.length, before + 1);
    assert.deepEqual(sent.at(-1), { type: intent, payload: { actionId: id } });
    listeners.dblclick(inputEvent(target));
    assert.equal(sent.length, before + 1, `${id} double click must not duplicate its click intent`);
  }
});

test("named DB playlists and playout use separate closed native intents", () => {
  for (const intent of [
    "refresh-named-playlists", "select-named-playlist",
    "load-named-playlist-by-id", "create-named-playlist",
    "save-current-named-playlist-to", "delete-selected-named-playlist"
  ]) assert.match(app, new RegExp("\\\"" + intent + "\\\""));
  assert.match(app, /state\.namedPlaylist/);
  assert.match(app, /dataset\.namedPlaylistDefinitionId/);
  assert.match(app,
    /beginNamedPlaylistSelection\(definition\.dataset\.namedPlaylistDefinitionId\)/);
  assert.match(app,
    /dbLoadButton\.disabled = playlistStructureMutationLocked \|\| busy \|\| !named/);
  assert.match(app, /named\.isWriteQuarantined/);
  assert.doesNotMatch(app, /programCode|listText/i);

  for (const label of ["PREPARE", "TAKE IN", "NEXT", "TAKE OUT"]) {
    assert.match(markup, new RegExp("<button[^>]*disabled[^>]*>[^<]*" + label));
  }
  for (const intent of [
    "prepare-playout", "take-in", "next-playout", "take-out",
    "choose-background", "toggle-background"
  ]) assert.match(playout, new RegExp("\\\"" + intent + "\\\""));
  assert.doesNotMatch(playout, /post\("set-fade-duration"/);
  assert.doesNotMatch(playout, /fadeDuration\.addEventListener/);
  assert.match(playout, /fadeDuration\.disabled = true;/);
  assert.match(playout, /fadeDuration\.value = String\(playout\.fadeDuration\);/);
  assert.match(markup,
    /id="playout-fade-duration" disabled hidden aria-hidden="true" tabindex="-1"/);
  assert.match(playout, /takeOut\.disabled = busy \|\| !commandAvailable;/);
  assert.match(playout, /playout\.isPlayCompletionPending === true/);
  assert.match(playout, /playout\.outcomeUnknown !== true/);
  assert.doesNotMatch(playout, /fetch\s*\(|localStorage|sessionStorage|sceneCode|filePath/);
});

test("named playlist load and save send command-specific closed payloads", () => {
  const start = app.indexOf("function beginNamedPlaylistCommand");
  const end = app.indexOf("function isMatchingNamedPlaylistReceipt", start);
  assert.ok(start >= 0 && end > start);
  const sent = [];
  const begin = new Function("send", `
    let pendingNamedPlaylistCommand = null;
    let lastCommandSequence = 40;
    function isNamedPlaylistActionLocked() { return false; }
    function lockNamedPlaylistControls() {}
    ${app.slice(start, end)}
    return beginNamedPlaylistCommand;
  `)((type, payload) => {
    sent.push({ type, payload });
    return true;
  });

  assert.equal(begin(
    "load-named-playlist-by-id",
    "named-definition-AAAAAAAAAAEF",
    false), true);
  assert.deepEqual(sent.pop(), {
    type: "load-named-playlist-by-id",
    payload: { definitionId: "named-definition-AAAAAAAAAAEF" }
  });

  assert.equal(begin(
    "save-current-named-playlist-to",
    "named-definition-AAAAAAAAAAEF",
    true), true);
  assert.deepEqual(sent.pop(), {
    type: "save-current-named-playlist-to",
    payload: {
      definitionId: "named-definition-AAAAAAAAAAEF",
      showSavedAlert: true
    }
  });
});

test("GraphE preserves name header sorting and directional find in native authority", () => {
  for (const intent of [
    "find-manual-financial", "toggle-manual-financial-name-sort"
  ]) assert.match(app, new RegExp("\\\"" + intent + "\\\""));
  assert.match(app, /id = "manual-financial-find-query"/);
  assert.match(app, /dataset\.manualFindDirection/);
  assert.match(app, /dataset\.manualNameSort/);
  assert.match(app, /dataset\.manualGeneration/);
  assert.match(app, /row\.isDataReadable === false/);
  assert.match(app, /manual-financial-row-unreadable/);
  assert.match(app, /manual-financial-row-legacy/);
  assert.match(styles, /button\.manual-financial-row-unreadable/);
  assert.match(styles, /button\.manual-financial-row-legacy/);
  assert.match(app, /generation: generation/);
  assert.match(app, /manual-financial-find-query[\s\S]*down\.click\(\)/);
  assert.doesNotMatch(app, /(?:INPUT_PIE|INPUT_GROW|INPUT_SELL|INPUT_PROFIT)[\s\S]*manual-financial-find-query/);
});

test("GraphE raw editor preserves untouched legacy storage byte-for-byte", () => {
  const start = app.indexOf("function blankManualRecord");
  const end = app.indexOf("function cloneManualRecord", start);
  assert.ok(start >= 0 && end > start);
  const helpers = new Function(`
    ${app.slice(start, end)}
    return { manualRecordFromRaw, preserveManualRawStorageValue };
  `)();
  const originals = ["", "_", "label_value_extra", " 12 ", "\u0001legacy", "\u0000tail"];
  const record = helpers.manualRecordFromRaw({
    screen: "sales",
    stockName: "Alpha",
    storageValues: originals
  });

  originals.forEach(function (original, index) {
    assert.equal(
      helpers.preserveManualRawStorageValue(record, index, "normalized_0"),
      original);
  });
  record.rawDirtyStorageIndices.push(2);
  assert.equal(
    helpers.preserveManualRawStorageValue(record, 2, "label_changed"),
    "label_changed");
  assert.deepEqual(record.rawOriginalStorageValues, originals);

  assert.match(app, /manualRawStorageIndex/);
  assert.match(app, /send\("save-manual-financial-raw", \{ record: rawRecord \}\)/);
});

test("FSell and VI dialogs send opaque native intents and never own file or stock authority", () => {
  for (const id of ["manual-list-modal", "manual-list-workspace", "manual-list-close"]) {
    assert.match(markup, new RegExp("id=\\\"" + id + "\\\""));
  }
  for (const screen of ["individual", "foreign", "institution", "vi"]) {
    assert.match(markup, new RegExp("data-manual-list-screen=\\\"" + screen + "\\\""));
  }
  for (const intent of [
    "open-manual-list", "close-manual-list", "refresh-manual-list",
    "set-manual-net-sell-cell", "search-manual-vi", "select-manual-vi-result",
    "add-manual-vi-result",
    "move-manual-vi-item", "delete-manual-vi-item",
    "delete-all-manual-vi-items", "confirm-manual-list",
    "import-manual-lists"
  ]) assert.match(app, new RegExp("\\\"" + intent + "\\\""));
  for (const obsoleteIntent of ["save-manual-net-sell", "add-manual-net-sell-cut", "save-manual-vi"]) {
    assert.doesNotMatch(app, new RegExp("\\\"" + obsoleteIntent + "\\\""));
  }

  const start = app.indexOf("function renderManualLists");
  const end = app.indexOf("function openNamedPlaylist", start);
  const manualLists = app.slice(start, end);
  assert.match(manualLists, /rowId: row\.rowId/);
  assert.match(manualLists, /resultId: row\.resultId/);
  assert.match(manualLists, /itemId: row\.itemId/);
  assert.match(manualLists,
    /addEventListener\("click"[\s\S]*deferLegacySingleClick[\s\S]*send\("select-manual-vi-result"/);
  assert.match(manualLists, /addEventListener\("dblclick"[\s\S]*send\("add-manual-vi-result"/);
  assert.match(manualLists, /send\("search-manual-vi", \{ query: input\.value \}\)/);
  assert.match(manualLists, /manual\.searchIsTruncated === true/);
  assert.match(manualLists, /검색 결과 일부만 표시 중입니다\. 검색어를 더 구체적으로 입력해 주세요\./u);
  assert.match(styles, /\.manual-list-status\.warning\s*\{/);
  assert.doesNotMatch(manualLists, /if \(input\.value\.trim\(\)\)/);
  assert.doesNotMatch(manualLists, /(?:rowVersion|stockCode|item\.code|filePath|directory|localStorage|fetch\s*\()/i);
});

test("ThemeA create editor changes KRX or NXT without clearing the current name draft", () => {
  for (const id of [
    "operator-catalog-new-market",
    "operator-catalog-editor-market-field",
    "operator-catalog-editor-market"
  ]) assert.match(markup, new RegExp("id=\\\"" + id + "\\\""));

  const renderStart = app.indexOf("function renderOperatorCatalog");
  const renderEnd = app.indexOf("function renderDialog", renderStart);
  assert.ok(renderStart >= 0 && renderEnd > renderStart);
  const render = app.slice(renderStart, renderEnd);
  assert.match(render,
    /const canChangeCreateThemeMarket = isTheme && catalog\.mode === "create"/);
  assert.match(render,
    /operatorCatalogEditorMarketField\.hidden = !canChangeCreateThemeMarket/);
  assert.match(render,
    /operatorCatalogEditorMarket\.disabled = !canChangeCreateThemeMarket/);
  assert.match(app,
    /root\.querySelectorAll\("button, input, select, textarea"\)[\s\S]*control\.disabled = true/);
  assert.match(render,
    /operatorCatalogEditorMarket\.value = catalog\.newThemeMarket === "nxt" \? "nxt" : "krx"/);

  const changeStart = app.indexOf(
    'operatorCatalogEditorMarket.addEventListener("change"');
  const changeEnd = app.indexOf(
    "operatorCatalogResults.addEventListener", changeStart);
  assert.ok(changeStart >= 0 && changeEnd > changeStart);
  const marketChange = app.slice(changeStart, changeEnd);
  assert.match(marketChange, /operatorCatalogState\.mode !== "create"/);
  assert.match(marketChange,
    /operatorCatalogNameDraft = operatorCatalogName\.value/);
  assert.match(marketChange,
    /send\("change-create-theme-market", \{[\s\S]*market:[\s\S]*editorName: operatorCatalogName\.value\.trim\(\)/);
  assert.doesNotMatch(marketChange, /operatorCatalogDraftKey\s*=\s*""/);
  assert.doesNotMatch(marketChange, /operatorCatalogNameDraft\s*=\s*""/);
  assert.doesNotMatch(marketChange, /begin-create-theme-catalog/);

  const listCreateStart = app.indexOf(
    'operatorCatalogNew.addEventListener("click"');
  assert.ok(listCreateStart >= 0 && changeStart > listCreateStart);
  const listCreate = app.slice(listCreateStart, changeStart);
  assert.match(listCreate,
    /send\("begin-create-theme-catalog", \{[\s\S]*operatorCatalogNewMarket\.value/);
});

test("UC3 keeps deferred world rows visible but disables ambiguous selection", () => {
  const start = app.indexOf("function renderComparison");
  const end = app.indexOf("function renderTheme", start);
  const comparison = app.slice(start, end);

  assert.match(comparison, /result\.disabled = row\.canSelect === false/);
  assert.match(comparison, /row\.unavailableReason/);
  assert.match(comparison, /deferredUnsafeRowCount/);
  assert.match(comparison, /선택과 송출을 차단합니다/);
});

test("separator rows preserve the original drag gesture", () => {
  assert.match(app, /element\.dataset\.cutDragEnabled = cutSelectionEnabled && !cutDragLocked/);
  assert.match(app, /element\.draggable = false/);
  assert.doesNotMatch(app, /cutDragEnabled = !row\.isSeparator/);
});

test("a C# busy snapshot freezes controls instead of silently accepting queued input", () => {
  const styles = fs.readFileSync(path.join(webRoot, "styles.css"), "utf8");
  assert.match(app, /state\.isBusy === true/);
  assert.match(app, /classList\.toggle\("busy"/);
  assert.match(styles, /body\.busy \.operator-shell \{ pointer-events: none; \}/);
});
