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

function section(startMarker, endMarker) {
  const start = app.indexOf(startMarker);
  assert.notEqual(start, -1, "missing start marker: " + startMarker);
  const end = app.indexOf(endMarker, start + startMarker.length);
  assert.notEqual(end, -1, "missing end marker: " + endMarker);
  return app.slice(start, end);
}

function assertOrdered(source, markers) {
  let cursor = -1;
  for (const marker of markers) {
    const next = source.indexOf(marker, cursor + 1);
    assert.ok(next > cursor, "expected marker in order: " + marker);
    cursor = next;
  }
}

test("modal focus support loads before app.js and is started once", () => {
  const scripts = Array.from(markup.matchAll(/<script\s+src="([^"]+)"/g),
    match => match[1]);
  const focusIndex = scripts.indexOf("legacy-modal-focus.js");
  const namedSelectionIndex = scripts.indexOf("legacy-named-playlist-selection.js");
  const appIndex = scripts.indexOf("app.js");

  assert.ok(focusIndex >= 0, "legacy modal focus module must be loaded");
  assert.ok(namedSelectionIndex > focusIndex,
    "named-playlist selection support must load after modal support");
  assert.ok(appIndex > namedSelectionIndex,
    "named-playlist selection support must load before app.js");
  assert.ok(appIndex > focusIndex, "focus module must load before app.js");
  assert.equal(scripts.filter(name => name === "legacy-modal-focus.js").length, 1);
  assert.equal(scripts.filter(
    name => name === "legacy-named-playlist-selection.js").length, 1);
  assert.equal(scripts.filter(name => name === "app.js").length, 1);

  assert.match(app, /window\.LegacyModalFocus\.create\(\{ document: document \}\)/);
  assert.match(app, /window\.LegacyNamedPlaylistSelection\.create\(\)/);
  assert.equal((app.match(/modalFocusManager\.start\(\)/g) || []).length, 1);
});

test("the one-button MessageBox stays visually above every child dialog", () => {
  function zIndex(className) {
    const match = styles.match(new RegExp(
      "\\." + className + "\\s*\\{[^}]*z-index:\\s*(\\d+)", "s"));
    assert.ok(match, "missing z-index for " + className);
    return Number(match[1]);
  }

  const alertLayer = zIndex("dialog-backdrop");
  assert.ok(alertLayer > zIndex("operator-catalog-backdrop"));
  assert.ok(alertLayer > zIndex("named-playlist-confirmation-backdrop"));
});

test("modal open, render, and close paths preserve and restore focus", () => {
  const namedOpen = section("function openNamedPlaylist", "function closeNamedPlaylist");
  assertOrdered(namedOpen, [
    "rememberModalOpener(namedPlaylistDialog)",
    "namedPlaylistModal.hidden = false",
    "syncModalFocus()"
  ]);

  const namedClose = section("function closeNamedPlaylist", "function isNamedPlaylistModalLocked");
  assertOrdered(namedClose, [
    "namedPlaylistModal.hidden = true",
    "syncModalFocus()"
  ]);

  for (const [functionName, nextFunction, hiddenElement, dialogElement] of [
    ["renderManualFinancial", "isCanonicalNamedPlaylistTitle", "manualModal", "manualDialog"],
    ["renderManualLists", "openNamedPlaylist", "manualListModal", "manualListDialog"],
    ["renderOperatorCatalog", "renderDialog", "operatorCatalogModal", "operatorCatalogDialog"]
  ]) {
    const body = section("function " + functionName, "function " + nextFunction);
    assertOrdered(body, [
      "if (" + hiddenElement + ".hidden) rememberModalOpener(" + dialogElement + ")",
      hiddenElement + ".hidden = false"
    ]);
  }

  const dialogRender = section("function renderDialog", "function render(state)");
  assertOrdered(dialogRender, [
    "if (dialog.hidden) rememberModalOpener(legacyAlertDialog)",
    "dialog.hidden = false"
  ]);

  const rootRender = section("function render(state)", "searchButton.addEventListener");
  assertOrdered(rootRender, ["renderDialog(state)", "syncModalFocus()"]);
  assert.doesNotMatch(rootRender, /dialogOk\.focus\(/,
    "focus must be delegated to the modal manager so nested opener state is retained");
});

test("Escape dismisses only the one-button alert and is inert for child dialogs", () => {
  const keydown = section(
    'document.addEventListener("keydown", function (event)',
    'dialogOk.addEventListener("click"');
  assertOrdered(keydown, [
    'event.key === "Escape" && modalFocusManager.getActiveModal()',
    "event.preventDefault()",
    "modalFocusManager.getActiveModal() === legacyAlertDialog",
    'send("dismiss-dialog", {})'
  ]);
  assert.doesNotMatch(keydown,
    /(?:closeNamedPlaylist|close-manual|close-operator)\s*\(/);
  assert.match(markup,
    /id="named-playlist-confirmation"[\s\S]*role="alertdialog"[\s\S]*id="named-playlist-confirmation-yes"[\s\S]*id="named-playlist-confirmation-no"/);
});

test("category-tree opener survives rerender by resolving its stable id or node key", () => {
  const opener = section("function createModalOpenerReference", "function rememberModalOpener");
  assertOrdered(opener, [
    "categoryTree.contains(opener)",
    "captureTreeUiState(renderedTreeTabId)",
    "controlId: opener.id || null",
    "nodeKey: node ? node.dataset.treeNodeKey : null",
    "return function resolveTreeModalOpener()",
    "document.getElementById(descriptor.controlId)",
    "findTreeNodeByKey(descriptor.nodeKey)",
    "resolved.setSelectionRange(descriptor.selectionStart, descriptor.selectionEnd)",
    "return resolved"
  ]);
  assert.match(app,
    /modalFocusManager\.rememberOpener\(modal, createModalOpenerReference\(\)\)/);

  const capture = section("function captureTreeUiState", "function beginTreeRender");
  assertOrdered(capture, [
    "categoryTree.contains(document.activeElement)",
    "id: activeElement.id",
    'value: typeof activeElement.value === "string" ? activeElement.value : null',
    "selectionStart: Number.isInteger(activeElement.selectionStart)",
    "selectionEnd: Number.isInteger(activeElement.selectionEnd)",
    "focusKey: active ? active.dataset.treeNodeKey : previous.focusKey || null",
    "controlFocus: activeElement ? controlFocus : previous.controlFocus || null"
  ]);

  const finish = section("function finishTreeRender", "function send(type, payload)");
  assertOrdered(finish, [
    "const canRestoreFocus = treeFocusOwnedBeforeRender",
    "const control = canRestoreFocus && saved.controlFocus && saved.controlFocus.id",
    "document.getElementById(saved.controlFocus.id)",
    "const canRestoreControl = control && categoryTree.contains(control)",
    "if (canRestoreFocus && !canRestoreControl && focused",
    "const controlWasRecreated = document.activeElement !== control",
    'typeof saved.controlFocus.value === "string"',
    "control.value = saved.controlFocus.value",
    "if (controlWasRecreated) control.focus({ preventScroll: true })",
    "Number.isInteger(saved.controlFocus.selectionStart)",
    "Number.isInteger(saved.controlFocus.selectionEnd)",
    "control.setSelectionRange(",
    "saved.controlFocus.selectionStart",
    "saved.controlFocus.selectionEnd"
  ]);
});

test("named playlist save requests the native saved dialog then closes on exact success", () => {
  const confirm = section(
    'namedPlaylistConfirmButton.addEventListener("click"',
    "manualButtons.forEach");
  assertOrdered(confirm, [
    "openNamedPlaylistConfirmation({",
    'kind: "save"',
    "definitionId: definitionId",
    'message: "저장하겠습니까?"',
    "showSavedAlert: true",
    "closeOnNo: true"
  ]);
  assert.doesNotMatch(confirm, /window\.confirm/);

  const resolution = section(
    "function resolveNamedPlaylistConfirmation",
    "function renderNamedPlaylist");
  const confirmationOpen = section(
    "function openNamedPlaylistConfirmation",
    "function restoreNamedPlaylistAfterConfirmation");
  assertOrdered(confirmationOpen, [
    "rememberModalOpener(namedPlaylistConfirmationDialog)",
    "namedPlaylistConfirmationModal.hidden = false",
    "namedPlaylistConfirmationYes.disabled = false",
    "namedPlaylistConfirmationNo.disabled = false",
    "syncModalFocus()"
  ]);
  const confirmationLock = section(
    "namedPlaylistConfirmationYes.disabled = pendingNamedPlaylistCommand",
    "if (deferredSelectionAction)");
  assert.doesNotMatch(confirmationLock, /operatorMutationLocked|uiBusy/,
    "an unrelated PROGRAM refresh must not cancel a physical confirmation click");
  assertOrdered(resolution, [
    "namedPlaylistConfirmationModal.hidden = true",
    "syncModalFocus()",
    "if (!confirmed)",
    "confirmation.closeOnNo === true",
    "closeNamedPlaylist()",
    '"save-current-named-playlist-to"',
    "confirmation.showSavedAlert === true"
  ]);
  const commandStart = section(
    "function beginNamedPlaylistCommand",
    "function isMatchingNamedPlaylistReceipt");
  assertOrdered(commandStart, [
    "const payload = { definitionId: definitionId }",
    'if (command === "save-current-named-playlist-to")',
    "payload.showSavedAlert = showSavedAlert === true",
    "pendingNamedPlaylistCommand = {",
    "send(command, payload)"
  ]);
  assert.doesNotMatch(commandStart, /send\(command,\s*\{/,
    "load and save must not share an open inline payload");

  const renderNamed = section("function renderNamedPlaylist", "function clearOperatorCatalogDragVisuals");
  assertOrdered(renderNamed, [
    "isMatchingNamedPlaylistReceipt(",
    "pendingNamedPlaylistCommand",
    "commandResult",
    "pendingNamedPlaylistCommand = null",
    "closeNamedPlaylist()"
  ]);
  assert.doesNotMatch(renderNamed, /window\.alert/);
});

test("named playlist double-click save confirms but never requests a success alert", () => {
  const action = section(
    "function runNamedPlaylistDoubleClick",
    "function lockNamedPlaylistControls");
  assertOrdered(action, [
    'mode === "save"',
    "openNamedPlaylistConfirmation({",
    'kind: "save"',
    "definitionId: definitionId",
    'message: "저장하겠습니까?"',
    "showSavedAlert: false",
    "closeOnNo: true"
  ]);

  const doubleClick = section(
    'namedPlaylistDefinitions.addEventListener("dblclick"',
    'namedPlaylistDefinitions.addEventListener("click"');
  assertOrdered(doubleClick, [
    "namedPlaylistSelection.isPending()",
    "namedPlaylistSelection.defer(definitionId",
    "mode: namedPlaylistMode",
    "runNamedPlaylistDoubleClick(definitionId, namedPlaylistMode)"
  ]);
  assert.doesNotMatch(action + doubleClick,
    /window\.(?:alert|confirm)|showSavedAlert\s*:\s*true/);
});

test("named playlist row selection is immediate and blocks wrong-target actions", () => {
  const selectionStart = section(
    "function beginNamedPlaylistSelection",
    "function runNamedPlaylistDoubleClick");
  assertOrdered(selectionStart, [
    "namedPlaylistSelection.begin(",
    "markNamedPlaylistVisualSelection(definitionId)",
    "lockNamedPlaylistSelectionActions()",
    'send("select-named-playlist", {',
    "definitionId: definitionId",
    "requestId: started.requestId"
  ]);
  assert.doesNotMatch(selectionStart, /setTimeout|namedPlaylistResultClickTimer/);

  const click = section(
    'namedPlaylistDefinitions.addEventListener("click"',
    'namedPlaylistNewTitle.addEventListener("input"');
  assert.match(click,
    /beginNamedPlaylistSelection\(definition\.dataset\.namedPlaylistDefinitionId\)/);
  assert.doesNotMatch(click, /setTimeout|namedPlaylistResultClickTimer/);

  const renderNamed = section(
    "function renderNamedPlaylist",
    "function clearOperatorCatalogDragVisuals");
  assertOrdered(renderNamed, [
    "const commandResult = state.commandResult",
    "namedPlaylistSelection.reconcile(",
    "commandResult",
    "const deferredSelectionAction = selectionResult.deferredAction",
    "const definitionLocked = isNamedPlaylistDefinitionLocked()",
    "runNamedPlaylistDoubleClick("
  ]);
  assert.match(app,
    /function isNamedPlaylistActionLocked\(\)[\s\S]*namedPlaylistSelection\.isPending\(\)/);
});

test("named playlist delete uses the same confirmation but No keeps the child open", () => {
  const remove = section(
    'namedPlaylistDeleteButton.addEventListener("click"',
    'namedPlaylistConfirmButton.addEventListener("click"');
  assertOrdered(remove, [
    "openNamedPlaylistConfirmation({",
    'kind: "delete"',
    'message: "프로그램이 삭제됩니다. 계속 하시겠습니까?"',
    "closeOnNo: false"
  ]);
  assert.doesNotMatch(remove, /window\.confirm/);
});

test("named playlist requests terminate only on their exact correlated receipt", () => {
  const close = section("function closeNamedPlaylist", "function isNamedPlaylistModalLocked");
  assertOrdered(close, [
    "if (isNamedPlaylistModalLocked() || namedPlaylistSelection.isPending()) return false",
    "namedPlaylistModal.hidden = true"
  ]);

  const locking = section("function isNamedPlaylistModalLocked", "function renderNamedPlaylist");
  for (const lockSource of [
    "uiBusy",
    "pendingNamedPlaylistCommand !== null",
    "namedPlaylistConfirmation !== null",
    "namedPlaylistState.isWriteInProgress === true"
  ]) {
    assert.match(locking, new RegExp(lockSource.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
  }
  assertOrdered(locking, [
    'const requestId = "named-ui-" + String(nextNamedPlaylistRequestId++)',
    "pendingNamedPlaylistCommand = {",
    "command: type",
    "targetId: requestId",
    "minimumSequence: lastCommandSequence + 1",
    "closeOnSuccess: false",
    "Object.assign({}, payload || {}, { requestId: requestId })",
    "send(type, correlatedPayload)",
    "lockNamedPlaylistControls()"
  ]);
  assertOrdered(locking, [
    "const payload = { definitionId: definitionId }",
    'if (command === "save-current-named-playlist-to")',
    "payload.showSavedAlert = showSavedAlert === true",
    "pendingNamedPlaylistCommand = {",
    "command: command",
    "targetId: definitionId",
    "definitionId: definitionId",
    "minimumSequence: lastCommandSequence + 1",
    "closeOnSuccess: true",
    "send(command, payload)",
    "lockNamedPlaylistControls()"
  ]);
  assert.doesNotMatch(locking, /send\(command,\s*\{/,
    "load and save must dispatch their closed command-specific payload");

  const renderNamed = section("function renderNamedPlaylist", "function clearOperatorCatalogDragVisuals");
  assertOrdered(renderNamed, [
    "isMatchingNamedPlaylistReceipt(",
    "pendingNamedPlaylistCommand",
    "commandResult",
    "pendingNamedPlaylistCommand = null",
    "succeeded && completed.closeOnSuccess"
  ]);
  assertOrdered(renderNamed, [
    "const modalLocked = isNamedPlaylistActionLocked()",
    "const closeLocked = isNamedPlaylistModalLocked() ||",
    "namedPlaylistSelection.isPending()",
    "namedPlaylistClose.disabled = closeLocked",
    "namedPlaylistCancelButton.disabled = closeLocked"
  ]);
  assert.match(renderNamed, /dbLoadButton\.disabled = playlistStructureMutationLocked/);
  assert.match(renderNamed, /dbSaveButton\.disabled = operatorMutationLocked/);
  assert.match(locking,
    /function isNamedPlaylistActionLocked\(\)[\s\S]*operatorMutationLocked/);
  assert.doesNotMatch(app,
    /isNamedPlaylistTerminalRejection|minimumRevision|namedPlaylistBusyCycleObserved/,
    "uncorrelated state changes must never unlock a DB request");
});

test("named playlist receipt matcher consumes busy rejection but defers busy success", () => {
  const helperSource = section(
    "function isMatchingNamedPlaylistReceipt",
    "function openNamedPlaylistConfirmation");
  const matches = new Function(
    helperSource + "\nreturn isMatchingNamedPlaylistReceipt;")();
  const pending = {
    minimumSequence: 7,
    command: "create-named-playlist",
    targetId: "named-ui-4"
  };
  const exact = {
    sequence: 7,
    command: "create-named-playlist",
    targetId: "named-ui-4",
    succeeded: true
  };

  assert.equal(matches(false, pending, exact), true);
  assert.equal(matches(true, pending, exact), false);
  assert.equal(matches(true, pending, { ...exact, succeeded: false }), true,
    "an exact gate rejection must be consumed before another busy request overwrites it");
  assert.equal(matches(false, pending, { ...exact, sequence: 6 }), false);
  assert.equal(matches(false, pending, { ...exact, command: "refresh-named-playlists" }), false);
  assert.equal(matches(false, pending, { ...exact, targetId: "named-ui-3" }), false);
});
