"use strict";

const fs = require("node:fs");
const path = require("node:path");
const test = require("node:test");
const assert = require("node:assert/strict");
const workflow = require("../../Web/manual-financial-workflow.js");
const ui = require("../../Web/manual-financial-ui.js");

const rowVersion = "A".repeat(64);
const STOCK_RESULT = "stock-search-results";

function quarters() {
  return [
    { quarter: "1Q", value: 10 },
    { quarter: "2Q", value: -20 },
    { quarter: "3Q", value: 30 },
    { quarter: "4Q", value: 40 },
    { quarter: "5Q", value: 50 },
    { quarter: "6Q", value: 60 }
  ];
}

function records() {
  return {
    "revenue-composition": {
      kind: "revenue-composition",
      stockName: "Alpha",
      baseDate: "2026-06",
      slices: [
        { label: "Semiconductor", percentageText: "50.0", percentage: 50 },
        { label: "Service", percentageText: "-", percentage: 0 },
        null,
        { label: "Overseas", percentageText: "25", percentage: 25 },
        null
      ]
    },
    "growth-metrics": {
      kind: "growth-metrics",
      stockName: "Alpha",
      salesGrowth: [1, 2.5, null, -4],
      operatingProfitGrowth: [5, 6, 7, 8],
      netAssetGrowth: [9, null, 11, 12],
      netIncomeGrowth: [13, 14, 15, 16],
      periods: ["1Q", "2Q", "3Q", "4Q"]
    },
    sales: {
      kind: "sales",
      stockName: "Alpha",
      quarters: quarters()
    },
    "operating-profit": {
      kind: "operating-profit",
      stockName: "Alpha",
      quarters: quarters().map(item => ({ ...item, value: item.value * 2 }))
    }
  };
}

function createHarness(options = {}) {
  const posted = [];
  const entries = [];
  const toasts = [];
  const logs = [];
  let ordinal = 0;
  const controller = ui.createController({
    postNative(type, payload) {
      posted.push({ type, payload });
    },
    appendPlaylistEntry(entry) {
      entries.push(entry);
      return true;
    },
    isLocked() {
      return typeof options.isLocked === "function" ? options.isLocked() : false;
    },
    getSelectedStock() {
      return { market: "kospi", source: "oracle", name: "Alpha", code: "000001" };
    },
    showToast(message) {
      toasts.push(message);
    },
    addLog(message) {
      logs.push(message);
    },
    createId() {
      ordinal += 1;
      return `manual-ui-${ordinal}`;
    }
  });
  return { controller, posted, entries, toasts, logs };
}

test("all four GraphE records round-trip through pure form conversion", () => {
  for (const [screen, record] of Object.entries(records())) {
    const form = ui.recordToForm(screen, record);
    assert.ok(form, screen);
    assert.deepEqual(ui.formToRecord(screen, form), workflow.normalizeRecord(screen, record));
  }

  const revenue = ui.recordToForm("revenue-composition", records()["revenue-composition"]);
  assert.equal(ui.formToRecord("revenue-composition", {
    ...revenue,
    slice3Label: "Only label",
    slice3Percentage: ""
  }), null);

  const growth = ui.recordToForm("growth-metrics", records()["growth-metrics"]);
  assert.equal(ui.formToRecord("growth-metrics", {
    ...growth,
    salesGrowth2: "not-a-number"
  }), null);

  const sales = ui.recordToForm("sales", records().sales);
  assert.equal(ui.formToRecord("sales", { ...sales, value4: "10.5" }), null);
  assert.equal(ui.resolveScreen("manual-operating-profit"), "operating-profit");
  assert.equal(ui.resolveScreen({ id: "manual-revenue-mix" }), "revenue-composition");
  assert.equal(ui.resolveScreen({ screen: "growth-metrics" }), "growth-metrics");
});

test("GraphE display sorting is deterministic for ASCII and Korean with stable ties", () => {
  const rows = [
    { stockName: "나다", id: 1 },
    { stockName: "beta", id: 2 },
    { stockName: "가나", id: 3 },
    { stockName: "Alpha", id: 4 }
  ];
  assert.deepEqual(
    ui.sortDisplaySnapshots(rows, "ascending").map(row => row.stockName),
    ["Alpha", "beta", "가나", "나다"]);
  assert.deepEqual(
    ui.sortDisplaySnapshots(rows, "descending").map(row => row.stockName),
    ["나다", "가나", "beta", "Alpha"]);
  assert.deepEqual(rows.map(row => row.id), [1, 2, 3, 4]);

  const ties = [
    { stockName: "동일", id: "first" },
    { stockName: "동일", id: "second" }
  ];
  assert.deepEqual(
    ui.sortDisplaySnapshots(ties, "descending").map(row => row.id),
    ["first", "second"]);
  assert.deepEqual(ui.sortDisplaySnapshots([], "ascending"), []);
  assert.deepEqual(ui.sortDisplaySnapshots([{ stockName: "Only" }], "descending"), [
    { stockName: "Only" }
  ]);
  assert.throws(() => ui.sortDisplaySnapshots(rows, "sideways"), /sort direction/i);
});

test("controller accepts only the pending list correlation and ignores shared stock traffic", () => {
  const harness = createHarness();
  assert.equal(harness.controller.open({ id: "manual-sales" }), true);
  assert.equal(harness.posted.length, 1);
  assert.equal(harness.posted[0].type, workflow.bridgeContract.listRequestType);
  const request = harness.posted[0].payload;
  assert.deepEqual(request, {
    requestId: "manual-ui-1",
    screen: "sales",
    query: "",
    maximumResults: workflow.bridgeContract.defaultMaximumResults
  });
  assert.equal(harness.controller.render().pending.list, request.requestId);

  const payload = {
    requestId: request.requestId,
    screen: "sales",
    query: "",
    retrievedAt: "2026-07-12T10:00:00+09:00",
    totalRowCount: 1,
    truncated: false,
    items: [{
      stockName: "Alpha",
      rowVersion,
      record: records().sales
    }]
  };
  assert.equal(harness.controller.handleMessage(
    workflow.bridgeContract.listResultType,
    { ...payload, requestId: "stale-list" }), false);
  assert.equal(harness.controller.render().pending.list, request.requestId);
  assert.equal(harness.controller.render().itemCount, 0);

  assert.equal(harness.controller.handleMessage(workflow.bridgeContract.listResultType, payload), true);
  assert.equal(harness.controller.render().pending.list, undefined);
  assert.equal(harness.controller.render().itemCount, 1);
  assert.equal(harness.controller.render().status, "ready");

  assert.equal(harness.controller.handleMessage(STOCK_RESULT, {
    requestId: "another-component",
    query: "Alpha",
    retrievedAt: "2026-07-12T10:01:00+09:00",
    totalRowCount: 0,
    truncated: false,
    results: []
  }), false);
  assert.equal(harness.controller.setLocked(true).locked, true);
  assert.equal(harness.controller.setLocked(false).locked, false);
});

class FakeElement {
  constructor(tagName, ownerDocument) {
    this.tagName = tagName.toUpperCase();
    this.ownerDocument = ownerDocument;
    this.children = [];
    this.attributes = new Map();
    this.listeners = new Map();
    this.dataset = {};
    this.textContent = "";
    this.value = "";
    this.hidden = false;
    this.disabled = false;
    this.readOnly = false;
    this.className = "";
  }

  append(...values) {
    for (const value of values) this.appendChild(value);
  }

  appendChild(value) {
    this.children.push(value);
    return value;
  }

  replaceChildren(...values) {
    this.children = [];
    this.append(...values);
  }

  setAttribute(name, value) {
    this.attributes.set(name, String(value));
  }

  addEventListener(name, callback) {
    this.listeners.set(name, callback);
  }
}

class FakeDocument {
  constructor() {
    this.body = new FakeElement("body", this);
  }

  createElement(tagName) {
    return new FakeElement(tagName, this);
  }
}

function findByClass(node, className) {
  if (String(node.className || "").split(/\s+/).includes(className)) return node;
  for (const child of node.children || []) {
    const found = findByClass(child, className);
    if (found) return found;
  }
  return null;
}

function findAllByClass(node, className, found = []) {
  if (String(node.className || "").split(/\s+/).includes(className)) found.push(node);
  for (const child of node.children || []) findAllByClass(child, className, found);
  return found;
}

function findByText(node, textContent) {
  if (node.textContent === textContent) return node;
  for (const child of node.children || []) {
    const found = findByText(child, textContent);
    if (found) return found;
  }
  return null;
}

function keyboardEvent(key, options = {}) {
  return {
    key,
    isComposing: options.isComposing === true,
    defaultPrevented: false,
    preventDefault() {
      this.defaultPrevented = true;
    }
  };
}

function recordFor(screen, stockName) {
  return { ...JSON.parse(JSON.stringify(records()[screen])), stockName };
}

function acceptList(harness, screen, stockNames, query = "") {
  const request = harness.posted.at(-1).payload;
  assert.equal(request.screen, screen);
  assert.equal(request.query, query);
  return harness.controller.handleMessage(workflow.bridgeContract.listResultType, {
    requestId: request.requestId,
    screen,
    query,
    retrievedAt: "2026-07-12T10:00:00+09:00",
    totalRowCount: stockNames.length,
    truncated: false,
    items: stockNames.map((stockName, index) => ({
      stockName,
      rowVersion: String.fromCharCode(65 + index).repeat(64),
      record: recordFor(screen, stockName)
    }))
  });
}

function acceptLoad(harness, screen, stockName) {
  const request = harness.posted.at(-1).payload;
  assert.equal(request.screen, screen);
  assert.equal(request.stockName, stockName);
  const itemIndex = ["Alpha", "Alpine", "Beta", "Gamma"].indexOf(stockName);
  return harness.controller.handleMessage(workflow.bridgeContract.loadResultType, {
    requestId: request.requestId,
    screen,
    retrievedAt: "2026-07-12T10:01:00+09:00",
    snapshot: {
      stockName,
      rowVersion: String.fromCharCode(65 + Math.max(itemIndex, 0)).repeat(64),
      record: recordFor(screen, stockName)
    }
  });
}

function acceptExactStockSearch(harness, stockName = "Alpha") {
  const request = harness.posted.at(-1);
  assert.equal(request.type, "search-stocks");
  assert.equal(request.payload.query, stockName);
  return harness.controller.handleMessage(STOCK_RESULT, {
    requestId: request.payload.requestId,
    query: stockName,
    retrievedAt: "2026-07-12T10:02:00+09:00",
    totalRowCount: 1,
    truncated: false,
    results: [{ market: "kospi", source: "oracle", name: stockName, code: "000001" }]
  });
}

function setFindQuery(overlay, value) {
  const query = findByClass(overlay, "mfui-search-input");
  query.value = value;
  query.listeners.get("input")();
  return query;
}

function assertFindControlsDoNotPost(harness, overlay) {
  const before = harness.posted.length;
  findByClass(overlay, "mfui-find-previous").listeners.get("click")();
  findByClass(overlay, "mfui-find-next").listeners.get("click")();
  assert.equal(harness.posted.length, before);
}

function displayedStockNames(overlay) {
  return findAllByClass(overlay, "mfui-list-row").map(row => row.textContent);
}

function assertLocalSortOnly(harness, overlay) {
  const before = harness.controller.render();
  const postedCount = harness.posted.length;
  const playlistCount = harness.entries.length;
  findByClass(overlay, "mfui-list-sort").listeners.get("click")();
  const after = harness.controller.render();
  assert.equal(
    after.sortDirection,
    before.sortDirection === "ascending" ? "descending" : "ascending");
  assert.equal(after.selectedStockName, before.selectedStockName);
  assert.equal(harness.posted.length, postedCount);
  assert.equal(harness.entries.length, playlistCount);
}

test("mount creates the isolated text-only dialog and exposes exactly the integration methods", () => {
  const harness = createHarness();
  const document = new FakeDocument();
  const overlay = harness.controller.mount(document.body);
  assert.equal(overlay.tagName, "DIV");
  assert.equal(overlay.className, "mfui-overlay");
  assert.equal(document.body.children.length, 1);
  assert.deepEqual(Object.keys(harness.controller), [
    "mount", "open", "handleMessage", "render", "setLocked"
  ]);
  assert.equal(harness.controller.open("revenue-composition"), true);
  assert.equal(overlay.hidden, false);
  assert.equal(harness.posted[0].type, workflow.bridgeContract.listRequestType);
});

test("GraphE stock-name header toggles descending first and keeps selected identity local", () => {
  const harness = createHarness();
  const overlay = harness.controller.mount(new FakeDocument().body);
  harness.controller.open("sales");
  acceptList(harness, "sales", ["Alpha", "beta", "가나", "나다"]);

  const header = findByClass(overlay, "mfui-list-sort");
  assert.ok(header);
  assert.equal(header.textContent, "종목명 ↕");
  assert.equal(header.dataset.direction, "ascending");
  assert.equal(header.attributes.get("aria-pressed"), "false");
  assert.deepEqual(displayedStockNames(overlay), ["Alpha", "beta", "가나", "나다"]);

  const beforeSort = harness.posted.length;
  header.listeners.get("click")();
  assert.equal(harness.controller.render().sortDirection, "descending");
  assert.deepEqual(displayedStockNames(overlay), ["나다", "가나", "beta", "Alpha"]);
  assert.equal(harness.posted.length, beforeSort);

  findByText(overlay, "beta").listeners.get("click")();
  acceptLoad(harness, "sales", "beta");
  const beforeSelectedSort = harness.posted.length;
  header.listeners.get("click")();
  assert.equal(harness.controller.render().sortDirection, "ascending");
  assert.equal(harness.controller.render().selectedStockName, "beta");
  assert.equal(harness.posted.length, beforeSelectedSort);
  const selectedRows = findAllByClass(overlay, "mfui-list-row")
    .filter(row => row.attributes.get("aria-selected") === "true");
  assert.deepEqual(selectedRows.map(row => row.textContent), ["beta"]);
});

test("GraphE previous and next find follow the current displayed sort order", () => {
  const harness = createHarness();
  const overlay = harness.controller.mount(new FakeDocument().body);
  harness.controller.open("operating-profit");
  acceptList(harness, "operating-profit", ["Alpha", "Beta", "Gamma"]);
  findByClass(overlay, "mfui-list-sort").listeners.get("click")();
  assert.deepEqual(displayedStockNames(overlay), ["Gamma", "Beta", "Alpha"]);
  setFindQuery(overlay, "a");

  findByClass(overlay, "mfui-find-next").listeners.get("click")();
  assert.equal(harness.posted.at(-1).payload.stockName, "Gamma");
  acceptLoad(harness, "operating-profit", "Gamma");
  findByClass(overlay, "mfui-find-next").listeners.get("click")();
  assert.equal(harness.posted.at(-1).payload.stockName, "Beta");
  acceptLoad(harness, "operating-profit", "Beta");
  findByClass(overlay, "mfui-find-previous").listeners.get("click")();
  assert.equal(harness.posted.at(-1).payload.stockName, "Gamma");
});

test("GraphE list result and each mode open reset display sorting to ascending", () => {
  for (const screen of ["revenue-composition", "growth-metrics", "sales", "operating-profit"]) {
    const harness = createHarness();
    const overlay = harness.controller.mount(new FakeDocument().body);
    harness.controller.open(screen);
    acceptList(harness, screen, ["Alpha", "Beta"]);
    assertLocalSortOnly(harness, overlay);
    assert.equal(harness.controller.render().sortDirection, "descending", screen);

    const searchForm = findByClass(overlay, "mfui-search");
    const query = setFindQuery(overlay, "Alpha");
    const submitEvent = keyboardEvent("Enter");
    searchForm.listeners.get("submit")(submitEvent);
    assert.equal(submitEvent.defaultPrevented, true);
    assert.equal(harness.controller.render().sortDirection, "descending", screen);
    acceptList(harness, screen, ["Alpha"], "Alpha");
    assert.equal(harness.controller.render().sortDirection, "ascending", screen);
    assert.deepEqual(displayedStockNames(overlay), ["Alpha"], screen);

    findByClass(overlay, "mfui-list-sort").listeners.get("click")();
    assert.equal(harness.controller.render().sortDirection, "descending", screen);
    harness.controller.open(screen);
    assert.equal(harness.controller.render().sortDirection, "ascending", screen);
    assert.deepEqual(displayedStockNames(overlay), [], screen);
    assert.equal(query.value, "", screen);
  }
});

test("GraphE empty and single-row lists still toggle locally without native traffic", () => {
  const emptyHarness = createHarness();
  const emptyOverlay = emptyHarness.controller.mount(new FakeDocument().body);
  emptyHarness.controller.open("revenue-composition");
  acceptList(emptyHarness, "revenue-composition", []);
  assertLocalSortOnly(emptyHarness, emptyOverlay);
  assert.deepEqual(displayedStockNames(emptyOverlay), []);

  const singleHarness = createHarness();
  const singleOverlay = singleHarness.controller.mount(new FakeDocument().body);
  singleHarness.controller.open("growth-metrics");
  acceptList(singleHarness, "growth-metrics", ["Alpha"]);
  assertLocalSortOnly(singleHarness, singleOverlay);
  assert.deepEqual(displayedStockNames(singleOverlay), ["Alpha"]);
});

test("GraphE display sorting stays local during list, load, stock, selection, and write pending", () => {
  const readHarness = createHarness();
  const readOverlay = readHarness.controller.mount(new FakeDocument().body);
  readHarness.controller.open("sales");
  assert.ok(readHarness.controller.render().pending.list);
  assertLocalSortOnly(readHarness, readOverlay);
  acceptList(readHarness, "sales", ["Alpha", "Beta"]);

  setFindQuery(readOverlay, "a");
  findByClass(readOverlay, "mfui-find-next").listeners.get("click")();
  assert.ok(readHarness.controller.render().pending.load);
  assertLocalSortOnly(readHarness, readOverlay);
  acceptLoad(readHarness, "sales", "Alpha");

  findByClass(readOverlay, "mfui-accent").listeners.get("click")();
  assert.ok(readHarness.controller.render().pending.stock);
  assertLocalSortOnly(readHarness, readOverlay);
  acceptExactStockSearch(readHarness);
  assert.ok(readHarness.controller.render().pending.selection);
  assertLocalSortOnly(readHarness, readOverlay);

  const writeHarness = createHarness();
  const writeOverlay = writeHarness.controller.mount(new FakeDocument().body);
  writeHarness.controller.open("sales");
  acceptList(writeHarness, "sales", ["Alpha", "Beta"]);
  setFindQuery(writeOverlay, "Alpha");
  findByClass(writeOverlay, "mfui-find-next").listeners.get("click")();
  acceptLoad(writeHarness, "sales", "Alpha");
  findByText(writeOverlay, "수정 저장").listeners.get("click")();
  acceptExactStockSearch(writeHarness);
  assert.ok(writeHarness.controller.render().pending.write);
  assertLocalSortOnly(writeHarness, writeOverlay);
});

test("loaded GraphE list Enter finds downward from the current row without wrapping", () => {
  const harness = createHarness();
  const document = new FakeDocument();
  const overlay = harness.controller.mount(document.body);
  harness.controller.open("sales");
  assert.equal(acceptList(harness, "sales", ["Alpha", "Alpine", "Beta", "Gamma"]), true);

  const query = findByClass(overlay, "mfui-search-input");
  query.value = " al ";
  query.listeners.get("input")();
  let event = keyboardEvent("Enter");
  query.listeners.get("keydown")(event);
  assert.equal(event.defaultPrevented, true);
  assert.equal(harness.posted.at(-1).type, workflow.bridgeContract.loadRequestType);
  assert.equal(harness.posted.at(-1).payload.stockName, "Alpha");
  assert.equal(acceptLoad(harness, "sales", "Alpha"), true);

  event = keyboardEvent("Enter");
  query.listeners.get("keydown")(event);
  assert.equal(harness.posted.at(-1).payload.stockName, "Alpine");
  assert.equal(acceptLoad(harness, "sales", "Alpine"), true);

  const countAtLastMatch = harness.posted.length;
  event = keyboardEvent("Enter");
  query.listeners.get("keydown")(event);
  assert.equal(event.defaultPrevented, true);
  assert.equal(harness.posted.length, countAtLastMatch);
  assert.equal(harness.controller.render().selectedStockName, "Alpine");
});

test("visible GraphE find buttons move around the current row without wrapping", () => {
  const harness = createHarness();
  const document = new FakeDocument();
  const overlay = harness.controller.mount(document.body);
  harness.controller.open("sales");
  acceptList(harness, "sales", ["Alpha", "Alpine", "Beta", "Gamma"]);

  const previous = findByClass(overlay, "mfui-find-previous");
  const next = findByClass(overlay, "mfui-find-next");
  assert.ok(previous);
  assert.ok(next);
  assert.equal(previous.textContent, "위로 찾기");
  assert.equal(next.textContent, "아래로 찾기");
  assert.equal(previous.attributes.get("aria-label"), "현재 선택 이전의 일치 종목 찾기");
  assert.equal(next.attributes.get("aria-label"), "현재 선택 다음의 일치 종목 찾기");

  setFindQuery(overlay, " al ");
  const beforeNoSelectionPrevious = harness.posted.length;
  previous.listeners.get("click")();
  assert.equal(harness.posted.length, beforeNoSelectionPrevious);

  next.listeners.get("click")();
  assert.equal(harness.posted.at(-1).payload.stockName, "Alpha");
  acceptLoad(harness, "sales", "Alpha");
  next.listeners.get("click")();
  assert.equal(harness.posted.at(-1).payload.stockName, "Alpine");
  acceptLoad(harness, "sales", "Alpine");

  // The WinForms loop used i > 0 and accidentally skipped row 0. The Web port
  // intentionally includes the first row as a valid previous match.
  previous.listeners.get("click")();
  assert.equal(harness.posted.at(-1).payload.stockName, "Alpha");
  acceptLoad(harness, "sales", "Alpha");
  const beforeTop = harness.posted.length;
  previous.listeners.get("click")();
  assert.equal(harness.posted.length, beforeTop);
});

test("GraphE below-find button and Enter use the same next-match request path", () => {
  const buttonHarness = createHarness();
  const buttonOverlay = buttonHarness.controller.mount(new FakeDocument().body);
  buttonHarness.controller.open("growth-metrics");
  acceptList(buttonHarness, "growth-metrics", ["Alpha", "Beta"]);
  setFindQuery(buttonOverlay, " beta ");
  findByClass(buttonOverlay, "mfui-find-next").listeners.get("click")();
  const buttonRequest = buttonHarness.posted.at(-1);

  const enterHarness = createHarness();
  const enterOverlay = enterHarness.controller.mount(new FakeDocument().body);
  enterHarness.controller.open("growth-metrics");
  acceptList(enterHarness, "growth-metrics", ["Alpha", "Beta"]);
  const enterQuery = setFindQuery(enterOverlay, " beta ");
  const enterEvent = keyboardEvent("Enter");
  enterQuery.listeners.get("keydown")(enterEvent);
  const enterRequest = enterHarness.posted.at(-1);

  assert.equal(enterEvent.defaultPrevented, true);
  assert.equal(buttonRequest.type, workflow.bridgeContract.loadRequestType);
  assert.equal(enterRequest.type, workflow.bridgeContract.loadRequestType);
  assert.deepEqual(
    { screen: buttonRequest.payload.screen, stockName: buttonRequest.payload.stockName },
    { screen: enterRequest.payload.screen, stockName: enterRequest.payload.stockName });
});

test("GraphE next-find supports query changes, no match, and a single match", () => {
  const harness = createHarness();
  const document = new FakeDocument();
  const overlay = harness.controller.mount(document.body);
  harness.controller.open("revenue-composition");
  acceptList(harness, "revenue-composition", ["Alpha", "Alpine", "Beta", "Gamma"]);
  const query = findByClass(overlay, "mfui-search-input");

  query.value = "Alpha";
  query.listeners.get("input")();
  query.listeners.get("keydown")(keyboardEvent("Enter"));
  acceptLoad(harness, "revenue-composition", "Alpha");

  query.value = "beta";
  query.listeners.get("input")();
  query.listeners.get("keydown")(keyboardEvent("Enter"));
  assert.equal(harness.posted.at(-1).payload.stockName, "Beta");
  acceptLoad(harness, "revenue-composition", "Beta");

  const beforeNoMatch = harness.posted.length;
  query.value = "missing";
  query.listeners.get("input")();
  query.listeners.get("keydown")(keyboardEvent("Enter"));
  assert.equal(harness.posted.length, beforeNoMatch);

  query.value = "beta";
  query.listeners.get("input")();
  query.listeners.get("keydown")(keyboardEvent("Enter"));
  assert.equal(harness.posted.length, beforeNoMatch);
});

test("all four GraphE modes share the loaded-list Enter next-find path", () => {
  for (const screen of ["revenue-composition", "growth-metrics", "sales", "operating-profit"]) {
    const harness = createHarness();
    const document = new FakeDocument();
    const overlay = harness.controller.mount(document.body);
    harness.controller.open(screen);
    acceptList(harness, screen, ["Alpha", "Beta"]);
    const query = findByClass(overlay, "mfui-search-input");
    query.value = "beta";
    query.listeners.get("input")();
    query.listeners.get("keydown")(keyboardEvent("Enter"));
    assert.equal(harness.posted.at(-1).type, workflow.bridgeContract.loadRequestType, screen);
    assert.equal(harness.posted.at(-1).payload.stockName, "Beta", screen);
  }
});

test("all four GraphE modes share visible previous and next find controls", () => {
  for (const screen of ["revenue-composition", "growth-metrics", "sales", "operating-profit"]) {
    const harness = createHarness();
    const document = new FakeDocument();
    const overlay = harness.controller.mount(document.body);
    harness.controller.open(screen);
    acceptList(harness, screen, ["Alpha", "Beta"]);
    setFindQuery(overlay, "a");

    findByClass(overlay, "mfui-find-next").listeners.get("click")();
    assert.equal(harness.posted.at(-1).payload.stockName, "Alpha", screen);
    acceptLoad(harness, screen, "Alpha");
    findByClass(overlay, "mfui-find-next").listeners.get("click")();
    assert.equal(harness.posted.at(-1).payload.stockName, "Beta", screen);
    acceptLoad(harness, screen, "Beta");
    findByClass(overlay, "mfui-find-previous").listeners.get("click")();
    assert.equal(harness.posted.at(-1).payload.stockName, "Alpha", screen);
  }
});

test("GraphE find buttons no-op while hidden, locked, or any request class is pending", () => {
  let externalLocked = false;
  const listHarness = createHarness({ isLocked: () => externalLocked });
  const listOverlay = listHarness.controller.mount(new FakeDocument().body);
  listHarness.controller.open("sales");
  setFindQuery(listOverlay, "Alpha");
  assert.equal(findByClass(listOverlay, "mfui-find-previous").disabled, true);
  assert.equal(findByClass(listOverlay, "mfui-find-next").disabled, true);
  assertFindControlsDoNotPost(listHarness, listOverlay);

  acceptList(listHarness, "sales", ["Alpha", "Beta"]);
  findByClass(listOverlay, "mfui-find-next").listeners.get("click")();
  assert.equal(listHarness.controller.render().pending.load, listHarness.posted.at(-1).payload.requestId);
  assertFindControlsDoNotPost(listHarness, listOverlay);
  acceptLoad(listHarness, "sales", "Alpha");

  externalLocked = true;
  listHarness.controller.render();
  assertFindControlsDoNotPost(listHarness, listOverlay);
  externalLocked = false;
  listHarness.controller.render();
  findByClass(listOverlay, "mfui-close").listeners.get("click")();
  assertFindControlsDoNotPost(listHarness, listOverlay);

  const selectionHarness = createHarness();
  const selectionOverlay = selectionHarness.controller.mount(new FakeDocument().body);
  selectionHarness.controller.open("sales");
  acceptList(selectionHarness, "sales", ["Alpha", "Beta"]);
  setFindQuery(selectionOverlay, "Alpha");
  findByClass(selectionOverlay, "mfui-find-next").listeners.get("click")();
  acceptLoad(selectionHarness, "sales", "Alpha");
  findByClass(selectionOverlay, "mfui-accent").listeners.get("click")();
  assert.ok(selectionHarness.controller.render().pending.stock);
  assertFindControlsDoNotPost(selectionHarness, selectionOverlay);
  acceptExactStockSearch(selectionHarness);
  assert.ok(selectionHarness.controller.render().pending.selection);
  assertFindControlsDoNotPost(selectionHarness, selectionOverlay);

  const writeHarness = createHarness();
  const writeOverlay = writeHarness.controller.mount(new FakeDocument().body);
  writeHarness.controller.open("sales");
  acceptList(writeHarness, "sales", ["Alpha", "Beta"]);
  setFindQuery(writeOverlay, "Alpha");
  findByClass(writeOverlay, "mfui-find-next").listeners.get("click")();
  acceptLoad(writeHarness, "sales", "Alpha");
  findByText(writeOverlay, "수정 저장").listeners.get("click")();
  assert.ok(writeHarness.controller.render().pending.stock);
  acceptExactStockSearch(writeHarness);
  assert.ok(writeHarness.controller.render().pending.write);
  assertFindControlsDoNotPost(writeHarness, writeOverlay);
});

test("GraphE Enter preserves initial DB submit and blocks pending, lock, and hidden modal", () => {
  const harness = createHarness();
  const document = new FakeDocument();
  const overlay = harness.controller.mount(document.body);
  harness.controller.open("operating-profit");
  const query = findByClass(overlay, "mfui-search-input");
  const searchForm = findByClass(overlay, "mfui-search");

  query.value = "Alpha";
  query.listeners.get("input")();
  let event = keyboardEvent("Enter");
  query.listeners.get("keydown")(event);
  assert.equal(event.defaultPrevented, false);
  searchForm.listeners.get("submit")(event);
  assert.equal(event.defaultPrevented, true);
  assert.equal(harness.posted.at(-1).type, workflow.bridgeContract.listRequestType);
  assert.equal(harness.posted.at(-1).payload.query, "Alpha");
  acceptList(harness, "operating-profit", ["Alpha"], "Alpha");

  event = keyboardEvent("Enter");
  query.listeners.get("keydown")(event);
  assert.equal(harness.posted.at(-1).type, workflow.bridgeContract.loadRequestType);
  const pendingCount = harness.posted.length;
  query.listeners.get("keydown")(keyboardEvent("Enter"));
  assert.equal(harness.posted.length, pendingCount);
  acceptLoad(harness, "operating-profit", "Alpha");

  harness.controller.setLocked(true);
  query.value = "Alpha";
  query.listeners.get("input")();
  query.listeners.get("keydown")(keyboardEvent("Enter"));
  assert.equal(harness.posted.length, pendingCount);
  harness.controller.setLocked(false);

  const close = findByClass(overlay, "mfui-close");
  close.listeners.get("click")();
  query.listeners.get("keydown")(keyboardEvent("Enter"));
  assert.equal(harness.posted.length, pendingCount);
});

test("fresh load, exact stock revalidation and native selection create one playable entry", () => {
  const harness = createHarness();
  const document = new FakeDocument();
  const overlay = harness.controller.mount(document.body);
  harness.controller.open("sales");
  const listRequest = harness.posted.at(-1).payload;
  assert.equal(harness.controller.handleMessage(workflow.bridgeContract.listResultType, {
    requestId: listRequest.requestId,
    screen: "sales",
    query: "",
    retrievedAt: "2026-07-12T10:00:00+09:00",
    totalRowCount: 1,
    truncated: false,
    items: [{ stockName: "Alpha", rowVersion, record: records().sales }]
  }), true);

  const listRow = findByClass(overlay, "mfui-list-row");
  assert.ok(listRow);
  listRow.listeners.get("click")();
  const loadCall = harness.posted.at(-1);
  assert.equal(loadCall.type, workflow.bridgeContract.loadRequestType);
  assert.equal(harness.controller.handleMessage(workflow.bridgeContract.loadResultType, {
    requestId: loadCall.payload.requestId,
    screen: "sales",
    retrievedAt: "2026-07-12T10:01:00+09:00",
    snapshot: { stockName: "Alpha", rowVersion, record: records().sales }
  }), true);
  assert.equal(harness.controller.render().hasSnapshot, true);

  const playlistButton = findByClass(overlay, "mfui-accent");
  assert.ok(playlistButton);
  playlistButton.listeners.get("click")();
  const truncatedSearch = harness.posted.at(-1);
  assert.equal(truncatedSearch.type, "search-stocks");
  harness.controller.handleMessage(STOCK_RESULT, {
    requestId: truncatedSearch.payload.requestId,
    query: "Alpha",
    retrievedAt: "2026-07-12T10:02:00+09:00",
    totalRowCount: 1,
    truncated: true,
    results: [{ market: "kospi", source: "oracle", name: "Alpha", code: "000001" }]
  });
  assert.equal(harness.entries.length, 0);
  assert.equal(harness.controller.render().stockStatus, "error");

  playlistButton.listeners.get("click")();
  const stockCall = harness.posted.at(-1);
  harness.controller.handleMessage(STOCK_RESULT, {
    requestId: stockCall.payload.requestId,
    query: "Alpha",
    retrievedAt: "2026-07-12T10:03:00+09:00",
    totalRowCount: 1,
    truncated: false,
    results: [{ market: "kospi", source: "oracle", name: "Alpha", code: "000001" }]
  });
  const selectionCall = harness.posted.at(-1);
  assert.equal(selectionCall.type, workflow.bridgeContract.selectionRequestType);
  const profile = workflow.getScreenDefinition("sales");
  harness.controller.handleMessage(workflow.bridgeContract.selectionResultType, {
    requestId: selectionCall.payload.requestId,
    screen: "sales",
    stockName: "Alpha",
    rowVersion,
    builderKey: profile.builderKey,
    code: profile.code,
    aliases: [profile.code],
    selection: {
      groupCode: "KOSPI",
      subject: "Alpha",
      graphicType: profile.graphicType,
      subtype: "",
      dataCode: ""
    },
    currentPage: 1,
    totalPages: 1
  });
  assert.equal(harness.entries.length, 1);
  assert.equal(workflow.isPlaylistEntryPlayable(harness.entries[0]), true);
  assert.equal(harness.entries[0].code, "5080");
});

test("UI source uses DOM construction and documents the isolated app integration hook", () => {
  const source = fs.readFileSync(
    path.join(__dirname, "..", "..", "Web", "manual-financial-ui.js"),
    "utf8");
  assert.match(source, /App integration hook/);
  assert.match(source, /createElement/);
  assert.doesNotMatch(source, /\.innerHTML\s*=/);
  assert.doesNotMatch(source, /insertAdjacentHTML/);
});

test("pending writes correlate exactly and OutcomeUnknown latches process quarantine", () => {
  const coordinator = ui.createPendingCoordinator();
  const expected = {
    requestId: "write-1",
    screen: "sales",
    stockName: "Alpha",
    expectedRowVersion: rowVersion
  };
  coordinator.set("write", expected, { operation: "delete-one" });

  const unknown = {
    requestId: "write-1",
    operation: "delete-one",
    screen: "sales",
    stockName: "Alpha",
    code: "OUTCOME_UNKNOWN",
    message: "Commit result is unknown",
    retryable: false,
    outcomeUnknown: true
  };
  assert.equal(coordinator.acceptMutationError({
    ...unknown,
    requestId: "stale-write"
  }).status, "stale");
  assert.equal(coordinator.snapshot().write, "write-1");
  assert.equal(ui.getProcessWriteQuarantine().active, false);

  const accepted = coordinator.acceptMutationError(unknown);
  assert.equal(accepted.status, "accepted");
  assert.equal(accepted.value.outcomeUnknown, true);
  assert.equal(coordinator.snapshot().write, undefined);
  assert.equal(ui.getProcessWriteQuarantine().active, true);
  assert.match(ui.getProcessWriteQuarantine().message, /unknown/i);

  const laterController = createHarness().controller;
  assert.equal(laterController.render().writeQuarantined, true);
  assert.equal(laterController.render().canWrite, false);
});
