"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");

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 workspaceHarness(source, keyVariable, renderFunction, workspaceSelector,
  inputs) {
  Object.entries(inputs).forEach(([selector, input]) => {
    if (selector.startsWith("#") && !input.id) input.id = selector.slice(1);
  });
  let replacements = 0;
  const workspace = {
    querySelector(selector) { return inputs[selector] || null; }
  };
  const categoryTree = {
    querySelector(selector) {
      return selector === workspaceSelector ? workspace : null;
    },
    replaceChildren() { replacements += 1; }
  };
  const documentObject = { activeElement: null };
  const draftSupport = section(
    "function rememberSearchDraft",
    "function isImeComposing");
  const harness = new Function("categoryTree", "document", "draftIds", `
    const searchDraftInputIds = new Set(draftIds);
    const searchDrafts = new Map();
    ${draftSupport}
    let ${keyVariable} = "";
    let comparisonTargetPendingSelection = null;
    function clearComparisonTargetPendingSelection() {}
    function applyPendingRowSelection() {}
    ${source}
    return {
      render: ${renderFunction},
      prime: function (value) { ${keyVariable} = value; },
      remember: rememberSearchDraft
    };
  `)(categoryTree, documentObject,
    Object.keys(inputs).filter(selector => selector.startsWith("#"))
      .map(selector => selector.slice(1)));
  return {
    ...harness,
    workspace,
    document: documentObject,
    replacements: () => replacements
  };
}

test("expert search keeps the same focused input across equivalent native states", () => {
  const input = { value: "ㄱㅈ" };
  const harness = workspaceHarness(
    section("function updateExpertWorkspace", "function updateTradingHaltWorkspace"),
    "expertRenderStructureKey",
    "renderExpert",
    ".expert-workspace",
    { "#expert-search": input });
  const state = {
    expert: {
      search: { query: "", results: [], isTruncated: false },
      selectedExpert: null,
      preview: { items: [] },
      actions: []
    }
  };
  const key = JSON.stringify({
    results: state.expert.search.results,
    isTruncated: state.expert.search.isTruncated,
    selectedExpert: state.expert.selectedExpert,
    preview: state.expert.preview,
    actions: state.expert.actions
  });
  harness.prime(key);
  harness.document.activeElement = input;

  harness.render(state);

  assert.equal(input.value, "ㄱㅈ");
  assert.equal(harness.replacements(), 0);

  harness.document.activeElement = null;
  state.expert.search.query = "김전문";
  harness.render(state);
  assert.equal(input.value, "김전문");
  assert.equal(harness.replacements(), 0);
});

test("an unsubmitted search draft survives blur and unrelated native renders", () => {
  const input = { value: "" };
  const harness = workspaceHarness(
    section("function updateExpertWorkspace", "function updateTradingHaltWorkspace"),
    "expertRenderStructureKey",
    "renderExpert",
    ".expert-workspace",
    { "#expert-search": input });
  const state = {
    expert: {
      search: { query: "", results: [], isTruncated: false },
      selectedExpert: null,
      preview: { items: [] },
      actions: []
    }
  };
  harness.prime(JSON.stringify({
    results: [], isTruncated: false, selectedExpert: null,
    preview: state.expert.preview, actions: []
  }));
  harness.document.activeElement = input;
  input.value = "김전문";
  harness.remember(input);
  harness.document.activeElement = null;

  harness.render(state);

  assert.equal(input.value, "김전문");

  state.expert.search.query = "김전문";
  harness.render(state);
  state.expert.search.query = "박전문";
  harness.render(state);
  assert.equal(input.value, "박전문",
    "after the submitted value is acknowledged, later native state may replace it");
});

test("modal search drafts survive recreation with Korean text, focus and caret", () => {
  const support = section(
    "function rememberSearchDraft",
    "function isImeComposing");
  const documentObject = { activeElement: null, getElementById: () => null };
  const root = {
    current: null,
    contains(input) { return input === this.current; }
  };
  documentObject.getElementById = function (id) {
    return root.current && root.current.id === id ? root.current : null;
  };
  function input(value) {
    return {
      id: "manual-financial-find-query",
      value,
      disabled: false,
      selectionStart: 1,
      selectionEnd: 2,
      selectionDirection: "forward",
      focus() { documentObject.activeElement = this; },
      setSelectionRange(start, end, direction) {
        this.selectionStart = start;
        this.selectionEnd = end;
        this.selectionDirection = direction;
      }
    };
  }
  const api = new Function("document", "searchDraftInputIds", `
    const searchDrafts = new Map();
    ${support}
    return {
      remember: rememberSearchDraft,
      sync: syncSearchDraft,
      capture: captureSearchDraftFocus,
      restore: restoreSearchDraftFocus
    };
  `)(documentObject, new Set(["manual-financial-find-query"]));

  const original = input("ㅎㄱ");
  root.current = original;
  documentObject.activeElement = original;
  api.sync(original, "", "manual-financial:sales");
  api.remember(original);
  const saved = api.capture(root);

  const recreated = input("");
  recreated.selectionStart = 0;
  recreated.selectionEnd = 0;
  root.current = recreated;
  documentObject.activeElement = null;
  api.sync(recreated, "", "manual-financial:sales");

  assert.equal(recreated.value, "ㅎㄱ", "an unsubmitted Korean draft must survive blur");
  assert.equal(api.restore(root, saved), true);
  assert.strictEqual(documentObject.activeElement, recreated);
  assert.deepEqual(
    [recreated.selectionStart, recreated.selectionEnd, recreated.selectionDirection],
    [1, 2, "forward"]);

  const otherScreen = input("");
  root.current = otherScreen;
  documentObject.activeElement = null;
  api.sync(otherScreen, "네이티브", "manual-financial:growth-metrics");
  assert.equal(otherScreen.value, "네이티브",
    "a draft from another modal scope must not leak into the new screen");
});

test("GraphE, VI and operator catalog searches use modal draft focus support", () => {
  const manualFinancial = section(
    "function renderManualFinancial",
    "function isCanonicalNamedPlaylistTitle");
  const manualLists = section(
    "function renderManualLists",
    "function openNamedPlaylist");
  const operatorCatalog = section(
    "function renderOperatorCatalog",
    "function renderDialog");

  assert.match(manualFinancial,
    /captureSearchDraftFocus\(manualWorkspace\)/);
  assert.match(manualFinancial,
    /syncSearchDraft\(listQuery, manual\.query, manualSearchScope\)/);
  assert.match(manualFinancial,
    /syncSearchDraft\(findQuery, manual\.findQuery, manualSearchScope\)/);
  assert.match(manualFinancial,
    /restoreSearchDraftFocus\(manualWorkspace, manualFinancialSearchFocus\)/);

  assert.match(manualLists, /input\.id = "manual-vi-search"/);
  assert.match(manualLists,
    /syncSearchDraft\(input, manual\.searchQuery, "manual-list:vi"\)/);
  assert.match(manualLists,
    /restoreSearchDraftFocus\(manualListWorkspace, manualListSearchFocus\)/);

  assert.match(operatorCatalog,
    /syncSearchDraft\([^]*?operatorCatalogQuery,[^]*?catalog\.query,[^]*?"operator-catalog:" \+ catalog\.entity\)/);
  assert.match(operatorCatalog,
    /syncSearchDraft\([^]*?operatorCatalogStockQuery,[^]*?catalog\.stockQuery,[^]*?"operator-catalog-stock:" \+ draftKey\)/);
  assert.match(operatorCatalog,
    /restoreSearchDraftFocus\(operatorCatalogDialog, operatorCatalogSearchFocus\)/);
});

test("comparison searches keep both focused editor identities across status polls", () => {
  const domestic = { value: "ㅅㅅ" };
  const world = { value: "apple" };
  const harness = workspaceHarness(
    section("function updateComparisonWorkspace", "function themeActionIsClickable"),
    "comparisonRenderStructureKey",
    "renderComparison",
    ".comparison-workspace",
    {
      "#comparison-domestic-search": domestic,
      "#comparison-world-search": world
    });
  const state = {
    comparison: {
      domesticSearch: { query: "", results: [], selectedResultId: null,
        isTruncated: false },
      worldSearch: { query: "", results: [], selectedResultId: null,
        isTruncated: false },
      fixedTargets: [],
      selectedFixedTargetId: null,
      firstTarget: null,
      secondTarget: null,
      savedPairs: [],
      actions: []
    }
  };
  const key = JSON.stringify({
    domestic: { results: [], selectedResultId: null, isTruncated: false },
    world: { results: [], selectedResultId: null, isTruncated: false },
    fixedTargets: [],
    selectedFixedTargetId: null,
    firstTarget: null,
    secondTarget: null,
    savedPairs: [],
    actions: []
  });
  harness.prime(key);
  harness.document.activeElement = domestic;

  harness.render(state);

  assert.equal(domestic.value, "ㅅㅅ");
  assert.equal(world.value, "");
  assert.equal(harness.replacements(), 0);
});

test("trading-halt search keeps its focused draft across equivalent status polls", () => {
  const input = { value: "ㅎㄱ" };
  const harness = workspaceHarness(
    section("function updateTradingHaltWorkspace", "function blankManualRecord"),
    "tradingHaltRenderStructureKey",
    "renderTradingHalt",
    ".halt-workspace",
    { "#halt-search": input });
  const state = {
    tradingHalt: { query: "", results: [], nameSort: "ascending", actions: [] }
  };
  harness.prime(JSON.stringify({
    results: [], nameSort: "ascending", actions: []
  }));
  harness.document.activeElement = input;

  harness.render(state);

  assert.equal(input.value, "ㅎㄱ");
  assert.equal(harness.replacements(), 0);
});

test("all recreated editors use stable state keys and transient busy locking", () => {
  const fixedCatalog = section("function renderFixedCatalog", "function markIndustryRowSelected");
  const manualFinancial = section("function renderManualFinancial", "function isCanonicalNamedPlaylistTitle");
  const manualLists = section("function renderManualLists", "function openNamedPlaylist");
  const operatorCatalog = section("function renderOperatorCatalog", "operatorCatalogClose.addEventListener");
  const treeFinish = section("function finishTreeRender", "function send(");

  assert.doesNotMatch(fixedCatalog,
    /if \(!catalog[^]*?categoryTree\.replaceChildren\(\)/);
  assert.match(manualFinancial,
    /renderStateKey = JSON\.stringify\(manual\)/);
  assert.match(manualFinancial,
    /setTransientControlsBusy\(manualDialog, state\.isBusy === true\)/);
  const manualStableReturn = manualFinancial.indexOf(
    "if (!manualModal.hidden && renderStateKey === manualFinancialRenderStateKey)");
  const manualDraftCapture = manualFinancial.indexOf(
    "captureManualDraftForChangedModel();",
    manualStableReturn);
  const manualWorkspaceReplace = manualFinancial.indexOf(
    "manualWorkspace.replaceChildren(toolbar, body);",
    manualStableReturn);
  assert.ok(manualStableReturn >= 0 && manualDraftCapture > manualStableReturn &&
    manualWorkspaceReplace > manualDraftCapture,
  "a changed GraphE model must capture the current editor before replacing it");
  assert.match(manualLists,
    /setTransientControlsBusy\(manualListDialog, state\.isBusy === true\)/);
  assert.match(operatorCatalog,
    /renderStateKey = JSON\.stringify\(catalog\)/);
  assert.match(operatorCatalog,
    /setTransientControlsBusy\(operatorCatalogDialog, busy\)/);
  assert.doesNotMatch(manualLists, /disabled\s*=\s*busy\s*\|\|/);
  assert.doesNotMatch(operatorCatalog, /disabled\s*=\s*busy\s*\|\|/);
  assert.doesNotMatch(manualLists,
    /querySelectorAll\("button, input"\)[^]*?control\.disabled = true/);
  assert.match(manualLists,
    /setTransientControlsBusy\(manualListDialog, true\)[^]*?set-manual-net-sell-cell/);
  const operatorStableReturn = operatorCatalog.indexOf(
    "if (!operatorCatalogModal.hidden && renderStateKey === operatorCatalogRenderStateKey)");
  const operatorModelReset = operatorCatalog.indexOf(
    "setTransientControlsBusy(operatorCatalogDialog, false);",
    operatorStableReturn);
  const operatorSemanticRender = operatorCatalog.indexOf(
    "operatorCatalogThemeTab.disabled = editing;",
    operatorStableReturn);
  assert.ok(operatorStableReturn >= 0 && operatorModelReset > operatorStableReturn &&
    operatorSemanticRender > operatorModelReset,
  "changed operator models must clear transient busy state before semantic rendering");
  assert.match(treeFinish, /!canRestoreControl && focused/);
  assert.match(treeFinish, /document\.activeElement !== control/);
});

test("transient busy snapshots can be cleared before a changed model is rendered", () => {
  const source = section("function setTransientControlsBusy", "function isOperatorMutationLocked");
  const setBusy = new Function(`${source}; return setTransientControlsBusy;`)();
  const control = { disabled: true, dataset: {} };
  const root = { querySelectorAll() { return [control]; } };

  setBusy(root, true);
  assert.equal(control.dataset.busyDisabled, "true");

  setBusy(root, false);
  assert.equal(control.disabled, true);
  assert.equal(control.dataset.busyDisabled, undefined);

  control.disabled = false;
  setBusy(root, true);
  setBusy(root, false);

  assert.equal(control.disabled, false);
  assert.equal(control.dataset.busyDisabled, undefined);
});

test("GraphE captures the current editor values before a changed model rebuild", () => {
  const source = section("function blankManualRecord", "function renderManualFinancial");
  const inputs = new Map([
    ["#manual-financial-stock-name", { value: " 삼성전자 " }],
    ["[data-manual-quarter-label=\"0\"]", { value: "2026Q1" }],
    ["[data-manual-quarter-value=\"0\"]", { value: "123" }]
  ]);
  const workspace = {
    querySelector(selector) { return inputs.get(selector) || null; }
  };
  const harness = new Function("manualWorkspace", `
    let manualDraft = {
      screen: "sales",
      stockName: "기존값",
      quarters: Array.from({ length: 6 }, function () {
        return { quarter: "", value: 0 };
      })
    };
    ${source}
    return {
      capture: rememberManualDraft,
      draft: function () { return manualDraft; }
    };
  `)(workspace);

  harness.capture();

  assert.equal(harness.draft().stockName, "삼성전자");
  assert.deepEqual(harness.draft().quarters[0], { quarter: "2026Q1", value: 123 });
});

test("GraphE preserves edits on model refresh but protects an intentional new-form reset", () => {
  const source = section(
    "function captureManualDraftForChangedModel",
    "function renderManualFinancial");
  let captures = 0;
  const harness = new Function("manualModal", "rememberManualDraft", `
    let manualDraftResetPending = false;
    ${source}
    return {
      capture: captureManualDraftForChangedModel,
      reset: function () { manualDraftResetPending = true; },
      pending: function () { return manualDraftResetPending; }
    };
  `)({ hidden: false }, function () { captures += 1; });

  harness.capture();
  assert.equal(captures, 1);

  harness.reset();
  harness.capture();
  assert.equal(captures, 1);
  assert.equal(harness.pending(), false);

  harness.capture();
  assert.equal(captures, 2);
});
