"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 app = fs.readFileSync(path.join(
  repositoryRoot,
  "src",
  "MBN_STOCK_WEBVIEW.LegacyParityApp",
  "Web",
  "app.js"), "utf8");
const styles = fs.readFileSync(path.join(
  repositoryRoot,
  "src",
  "MBN_STOCK_WEBVIEW.LegacyParityApp",
  "Web",
  "styles.css"), "utf8");

function section(startMarker, endMarker) {
  const start = app.indexOf(startMarker);
  const end = app.indexOf(endMarker, start + startMarker.length);
  assert.ok(start >= 0 && end > start,
    `Unable to extract ${startMarker} through ${endMarker}`);
  return app.slice(start, end);
}

function listRow(documentObject, selected = false, disabled = false) {
  const attributes = new Map([["aria-selected", selected ? "true" : "false"]]);
  const classes = new Set(selected ? ["selected"] : []);
  return {
    dataset: {},
    disabled,
    tabIndex: -1,
    focused: false,
    scrolled: false,
    classList: {
      toggle(name, enabled) {
        if (enabled) classes.add(name);
        else classes.delete(name);
      },
      contains(name) { return classes.has(name); }
    },
    getAttribute(name) { return attributes.get(name) || null; },
    setAttribute(name, value) { attributes.set(name, String(value)); },
    focus() {
      this.focused = true;
      documentObject.activeElement = this;
    },
    scrollIntoView() { this.scrolled = true; }
  };
}

function listBox(rows) {
  const attributes = new Map();
  return {
    dataset: {},
    tabIndex: -1,
    querySelectorAll() { return rows; },
    setAttribute(name, value) { attributes.set(name, String(value)); },
    getAttribute(name) { return attributes.get(name) || null; }
  };
}

function rovingHarness() {
  const documentObject = { activeElement: null };
  const source = section(
    "function isRovingListNavigationKey",
    "function stockResultPageOffset");
  const api = new Function("document", `
    ${source}
    return {
      configureRovingList,
      rovingListKeyTarget,
      focusRovingListRow,
      moveRovingListSelection
    };
  `)(documentObject);
  return { documentObject, ...api };
}

test("restored WinForms lists expose one roving tab stop and skip disabled rows", () => {
  const harness = rovingHarness();
  const rows = [
    listRow(harness.documentObject),
    listRow(harness.documentObject, true),
    listRow(harness.documentObject, false, true)
  ];
  const list = listBox(rows);

  harness.configureRovingList(list, rows, rows[1]);

  assert.deepEqual(rows.map(row => row.tabIndex), [-1, 0, -1]);
  assert.equal(list.tabIndex, -1);
  assert.equal(list.getAttribute("role"), "listbox");
  assert.deepEqual(rows.map(row => row.getAttribute("role")),
    ["option", "option", "option"]);
  assert.deepEqual(rows.map(row => row.dataset.rovingRow),
    ["true", "true", "true"]);
  assert.equal(list.dataset.rovingPageSize, "9");
  assert.equal(list.dataset.rovingSelectionFollowsFocus, "true");

  const empty = listBox([]);
  harness.configureRovingList(empty, [], null);
  assert.equal(empty.tabIndex, 0,
    "an empty native-style ListBox remains one tab stop");
});

test("Arrow and boundary keys select, focus and scroll one row through native intent", () => {
  const harness = rovingHarness();
  const rows = [
    listRow(harness.documentObject),
    listRow(harness.documentObject, true),
    listRow(harness.documentObject),
    listRow(harness.documentObject, false, true)
  ];
  const list = listBox(rows);
  harness.configureRovingList(list, rows, rows[1]);
  const activated = [];
  const activate = row => {
    activated.push(row);
    return true;
  };

  assert.equal(harness.moveRovingListSelection(
    list, rows[1], "ArrowDown", activate), true);
  assert.deepEqual(rows.map(row => row.getAttribute("aria-selected")),
    ["false", "false", "true", "false"]);
  assert.deepEqual(rows.map(row => row.tabIndex), [-1, -1, 0, -1]);
  assert.equal(rows[2].focused, true);
  assert.equal(rows[2].scrolled, true);
  assert.deepEqual(activated, [rows[2]]);

  harness.moveRovingListSelection(list, rows[2], "Home", activate);
  assert.equal(rows[0].getAttribute("aria-selected"), "true");
  assert.equal(rows[0].tabIndex, 0);
  assert.deepEqual(activated, [rows[2], rows[0]]);

  harness.moveRovingListSelection(list, rows[0], "End", activate);
  assert.equal(rows[2].getAttribute("aria-selected"), "true",
    "End targets the last enabled row");
  assert.deepEqual(activated, [rows[2], rows[0], rows[2]]);

  harness.moveRovingListSelection(list, rows[2], "ArrowDown", activate);
  assert.equal(activated.length, 3,
    "a boundary key on the current row does not duplicate native selection");
});

test("PageUp and PageDown move by a bounded nine-row native page", () => {
  const harness = rovingHarness();
  const rows = Array.from({ length: 25 }, (_, index) =>
    listRow(harness.documentObject, index === 12));
  const list = listBox(rows);
  harness.configureRovingList(list, rows, rows[12]);
  const activated = [];

  harness.moveRovingListSelection(list, rows[12], "PageUp", row => {
    activated.push(row);
    return true;
  });
  assert.equal(rows[3].getAttribute("aria-selected"), "true");

  harness.moveRovingListSelection(list, rows[3], "PageDown", row => {
    activated.push(row);
    return true;
  });
  assert.equal(rows[12].getAttribute("aria-selected"), "true");
  assert.deepEqual(activated, [rows[3], rows[12]]);
});

test("focus-only candidate lists move their sole tab stop without selecting", () => {
  const harness = rovingHarness();
  const rows = [
    listRow(harness.documentObject, true),
    listRow(harness.documentObject),
    listRow(harness.documentObject)
  ];
  const list = listBox(rows);
  harness.configureRovingList(list, rows, rows[0], {
    activation: "focus",
    selectionFollowsFocus: false
  });
  let activations = 0;

  assert.equal(harness.moveRovingListSelection(
    list, rows[0], "ArrowDown", () => { activations += 1; }), true);
  assert.equal(activations, 0);
  assert.deepEqual(rows.map(row => row.tabIndex), [-1, 0, -1]);
  assert.deepEqual(rows.map(row => row.getAttribute("aria-selected")),
    ["true", "false", "false"]);
});

test("graphics trees keep one treeitem tab stop and skip collapsed leaves", () => {
  const harness = rovingHarness();
  const root = listRow(harness.documentObject);
  root.dataset.treeNodeKey = "root|section-a";
  root.dataset.treeRoot = "true";
  const openLeaf = listRow(harness.documentObject);
  openLeaf.dataset.treeNodeKey = "leaf|section-a|one";
  openLeaf.closest = () => ({ open: true });
  const collapsedLeaf = listRow(harness.documentObject, true);
  collapsedLeaf.dataset.treeNodeKey = "leaf|section-b|two";
  collapsedLeaf.closest = () => ({ open: false });
  const rows = [root, openLeaf, collapsedLeaf];
  const tree = listBox(rows);

  harness.configureRovingList(tree, rows, collapsedLeaf, {
    listRole: "tree",
    rowRole: "treeitem",
    selectionFollowsFocus: false
  });

  assert.equal(tree.getAttribute("role"), "tree");
  assert.deepEqual(rows.map(row => row.getAttribute("role")),
    ["treeitem", "treeitem", "treeitem"]);
  assert.deepEqual(rows.map(row => row.tabIndex), [0, -1, -1]);
  assert.strictEqual(
    harness.rovingListKeyTarget(tree, root, "ArrowDown"),
    openLeaf);
  assert.strictEqual(
    harness.rovingListKeyTarget(tree, openLeaf, "End"),
    openLeaf);
});

test("all restored large lists use the shared IME-safe roving contract", () => {
  for (const selector of [
    "data-comparison-selection-id",
    "data-comparison-target-id",
    "data-comparison-pair-id",
    "data-theme-result-id",
    "data-expert-selection-id",
    "data-halt-selection-id",
    "data-manual-result-id",
    "data-manual-stock-result-id",
    "data-manual-vi-result-id",
    "data-named-playlist-definition-id",
    "data-operator-catalog-result-id",
    "data-operator-catalog-stock-result-id"
  ]) {
    assert.match(app, new RegExp(`button\\[${selector}\\]`));
  }
  assert.match(app, /configureTreeRovingLists/);
  assert.match(app, /listRole: "tree"/);
  assert.match(app, /rowRole: "treeitem"/);
  assert.match(app,
    /rovingList && isRovingListNavigationKey\(event\.key\)[^]*?!isImeComposing\(event\)/);
  assert.match(app, /function activateRovingListRow\(row\)/);
  assert.match(app, /key === "PageUp"/);
  assert.match(app, /key === "PageDown"/);
  assert.match(app, /row\.dataset\.themeResultId/);
  assert.match(app, /row\.dataset\.namedPlaylistDefinitionId/);
  assert.match(app, /row\.dataset\.operatorCatalogResultId/);
  assert.match(app, /row\.dataset\.operatorCatalogStockResultId/);
  assert.match(app, /send\("load-manual-financial", \{ resultId: row\.dataset\.manualResultId \}\)/);
  assert.match(app, /send\("select-manual-vi-result", \{ resultId: row\.dataset\.manualViResultId \}\)/);
});

test("GraphE previews use only four same-origin packaged images and bounded safe values", () => {
  const source = section(
    "const manualFinancialReferenceImages",
    "function blankManualRecord");
  const api = new Function(`
    ${source}
    return {
      images: manualFinancialReferenceImages,
      columns: manualFinancialPreviewColumns,
      values: manualFinancialPreviewValues
    };
  `)();

  assert.deepEqual({ ...api.images }, {
    "revenue-composition": "./Previews/pie.bmp",
    "growth-metrics": "./Previews/Grow.bmp",
    sales: "./Previews/sell.bmp",
    "operating-profit": "./Previews/profit.bmp"
  });
  assert.equal(api.columns("revenue-composition").length, 7);
  assert.equal(api.columns("growth-metrics").length, 6);
  assert.equal(api.columns("sales").length, 7);
  assert.deepEqual(api.values({
    stockName: "fallback",
    previewValues: ["삼성전자", "2025/12", "반도체_70"]
  }, 7), ["삼성전자", "2025/12", "반도체_70", "", "", "", ""]);
  assert.deepEqual(api.values({ stockName: "이름만", previewValues: [] }, 3),
    ["이름만", "", ""]);
  assert.match(source, /image\.alt = label \+ " 기존 송출 화면 참고 이미지"/);
  assert.match(styles, /\.manual-financial-reference img/);
  assert.match(styles, /\.manual-financial-preview-cell/);
  assert.doesNotMatch(source, /ROWID|ROW_HANDLE|rowVersion|stockCode|MmoneyCoder\.ini/);
});

test("choosing a different create-mode stock clears fields without touching rerenders or edits", () => {
  const source = section("function cloneManualRecord", "function manualElement");
  const shouldReset = new Function(`
    ${source}
    return shouldResetManualStockDraft;
  `)();

  assert.equal(shouldReset(false, false, { screen: "sales" }), true);
  assert.equal(shouldReset(true, false, { screen: "sales" }), false,
    "reselecting the current candidate keeps the draft");
  assert.equal(shouldReset(false, true, { screen: "sales" }), false,
    "stored-row edit mode keeps the loaded record");
  assert.equal(shouldReset(false, false, { screen: "sales", rawMode: true }), false,
    "legacy raw editing retains its byte-preserving draft");
  assert.equal(shouldReset(false, false, null), false);

  const selectionHelper = section(
    "function selectManualFinancialStock",
    "function captureManualDraftForChangedModel");
  assert.match(selectionHelper, /rememberManualDraft\(\)/);
  assert.match(selectionHelper, /shouldResetManualStockDraft/);
  assert.match(selectionHelper, /manualDraft = blankManualRecord\(manualDraft\.screen\)/);
  assert.match(selectionHelper, /manualDraftResetPending = true/);
  assert.match(selectionHelper, /manualDraft = previousDraft/,
    "a rejected send restores the operator's original draft");

  const clickBranch = section(
    "const stock = event.target.closest(\"button[data-manual-stock-result-id]\")",
    "if (event.target.closest(\"button[data-manual-save]\"))");
  assert.match(clickBranch, /selectManualFinancialStock\(stock\)/);
  assert.match(clickBranch, /focusManualFinancialStock\(stock\)/);
});

function manualStockSelectionHarness() {
  const documentObject = { activeElement: null };
  const stockName = { readOnly: false };
  const manualWorkspace = {
    contains() { return true; },
    querySelector(selector) {
      return selector === "#manual-financial-stock-name" ? stockName : null;
    }
  };
  const sent = [];
  const source = section(
    "function focusManualFinancialStock",
    "function captureManualDraftForChangedModel");
  const api = new Function(
    "document",
    "manualWorkspace",
    "rememberManualDraft",
    "cloneManualRecord",
    "shouldResetManualStockDraft",
    "blankManualRecord",
    "send",
    `
      let manualDraft = { screen: "sales", rawMode: false, retained: "operator input" };
      let manualDraftKey = "sales|new";
      let manualDraftResetPending = false;
      ${source}
      return {
        focus: focusManualFinancialStock,
        select: selectManualFinancialStock,
        draft: () => manualDraft,
        resetPending: () => manualDraftResetPending
      };
    `)(
      documentObject,
      manualWorkspace,
      () => {},
      record => JSON.parse(JSON.stringify(record)),
      (selected, readOnly, draft) => !selected && !readOnly && !!draft && !draft.rawMode,
      screen => ({ screen, rawMode: false, cleared: true }),
      (type, payload) => {
        sent.push({ type, payload });
        return true;
      });
  const stock = {
    dataset: { manualStockResultId: "opaque-stock-1" },
    disabled: false,
    focused: false,
    getAttribute(name) {
      return name === "aria-selected" ? "false" : null;
    },
    focus() {
      this.focused = true;
      documentObject.activeElement = this;
    }
  };
  return {
    ...api,
    stock,
    sent
  };
}

test("GraphE candidate browser double-click emits one authoritative selection", () => {
  const harness = manualStockSelectionHarness();

  // Browser order is click, click, dblclick. The click gestures only focus;
  // the dblclick is the one authoritative legacy GraphE selection.
  harness.focus(harness.stock);
  harness.focus(harness.stock);
  harness.select(harness.stock);

  assert.deepEqual(harness.sent, [{
    type: "select-manual-financial-stock",
    payload: { resultId: "opaque-stock-1" }
  }]);
  assert.equal(harness.stock.focused, true);
  assert.equal(harness.draft().cleared, true);
  assert.equal(harness.resetPending(), true);
});

test("GraphE candidate single-click only focuses and preserves the editor draft", () => {
  const harness = manualStockSelectionHarness();
  harness.focus(harness.stock);

  assert.equal(harness.stock.focused, true);
  assert.equal(harness.sent.length, 0);
  assert.equal(harness.draft().retained, "operator input");
  assert.equal(harness.resetPending(), false);
});
