"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 harness = fs.readFileSync(path.join(
  repositoryRoot, "scripts", "Test-LegacyPackageInput.mjs"), "utf8");
const runner = fs.readFileSync(path.join(
  repositoryRoot, "scripts", "Invoke-LegacyPackageInputSmoke.ps1"), "utf8");

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

function assertInOrder(source, markers) {
  let cursor = 0;
  for (const marker of markers) {
    const index = source.indexOf(marker, cursor);
    assert.ok(index >= cursor, "missing or out-of-order marker: " + marker);
    cursor = index + marker.length;
  }
}

test("DryRun PROGRAM uses the original all, mouse, Space, and NEXT split", () => {
  const sequence = section(
    harness,
    "async function executeDryRunPlayoutSequence",
    "function recordFullUiDbCheck");
  assertInOrder(sequence, [
    "PROGRAM clear every enabled row",
    "snapshot.state.playlist.every(row => row.isEnabled === false)",
    "snapshot.state.playout.nextKind === \"endOfPlaylist\"",
    "PROGRAM restore every enabled row",
    "snapshot.state.playlist.every(row => row.isEnabled === true)",
    "pointerClickDisabledPlaylistCheckbox(",
    "checkboxAriaDisabled === true",
    "pressKey(\"Space\"",
    "[\"toggle-active-playlist-enabled\"]",
    "DryRun NEXT skips the Space-disabled future row",
    "currentEntryId === nextEntryId"
  ]);
  assert.match(sequence, /\["set-all-playlist-enabled"\]/u);
  assert.match(sequence, /setAllIntentCount: 2/u);
  assert.match(sequence, /toggleActiveIntentCount: 1/u);
  assert.doesNotMatch(sequence, /send\("set-playlist-enabled"/u);
});

test("F8 and Escape are physical one-shot keys only in known DryRun phases", () => {
  const authorization = section(
    harness,
    "function authorizeDryRunPlayoutShortcut",
    "async function setSearchText");
  assert.match(authorization, /new Set\(\["F8", "Escape"\]\)/u);
  assert.match(authorization,
    /definitionName === "F8" \? "idle" : "program"/u);
  assert.match(authorization, /playout\.outcomeUnknown === true/u);
  assert.match(authorization, /playout\.isPlayCompletionPending === true/u);
  assert.match(authorization, /playout\.isTakeOutCompletionPending === true/u);
  assert.match(authorization, /authorization\.consumed === true/u);
  assert.match(authorization, /authorization\.consumed = true/u);

  const sequence = section(
    harness,
    "async function executeDryRunPlayoutSequence",
    "function recordFullUiDbCheck");
  assertInOrder(sequence, [
    "const f8Authorization = authorizeDryRunPlayoutShortcut(",
    "\"rapid DryRun F8 after row selection\"",
    "takeInIntents.length !== 1",
    "const escapeAuthorization = authorizeDryRunPlayoutShortcut(",
    "\"DryRun Escape TAKE OUT after NEXT\"",
    "takeOutIntents.length !== 1",
    "finalPhase = current.state.playout.phase"
  ]);
});

test("ThemeA button and existing UC6 Enter expose a bounded blank stock list", () => {
  const blankSearch = section(
    harness,
    "async function verifyBlankOperatorCatalogStockSearch",
    "async function addOperatorCatalogStock");
  assertInOrder(blankSearch, [
    "`${label} blank query`",
    "trigger === \"button\"",
    "pressKey(\"Enter\"",
    "currentCatalog?.stockQuery === \"\"",
    "resultCount > 1 && resultCount <= 10_000",
    "statusMessage === \"종목 검색을 완료했습니다.\"",
    "operatorCatalogStockResultCount === resultCount",
    "messages[0].emptyStockQuery !== true",
    "resultCount !== 10_000 || !truncationVisible",
    "draftRowsPreserved: true"
  ]);

  const theme = section(
    harness,
    "async function createReadEditDeleteTheme",
    "async function searchAndSelectMainExpert");
  assertInOrder(theme, [
    "`${label} ThemeA editor`",
    "addOperatorCatalogStockByDoubleClick("
  ]);

  const expert = section(
    harness,
    "async function createReadEditDeleteExpert",
    "async function executeOperatorCatalogCrud");
  assertInOrder(expert, [
    "snapshot.state.operatorCatalog?.mode === \"edit\"",
    "`${label} existing UC6 editor`",
    "addOperatorCatalogStockByDoubleClick("
  ]);
});

test("KRX ThemeA create physically round-trips NXT while preserving the name", () => {
  const allowed = section(
    harness,
    "const fullUiDbAllowedOutboundMessageTypes",
    "const playoutIntentMessageTypes");
  assert.match(allowed, /"change-create-theme-market"/u);

  const roundTrip = section(
    harness,
    "async function verifyKrxThemeCreateMarketRoundTrip",
    "async function createReadEditDeleteTheme");
  assertInOrder(roundTrip, [
    "operatorCatalogEditorMarketValue !== \"krx\"",
    "draftRows?.some(row => row.rowId === draftRowId)",
    "switch editor market to NXT",
    "newThemeMarket === \"nxt\"",
    "editorName === title",
    "draftRows?.length === 0",
    "statusKind === \"warning\"",
    "operatorCatalogNameValue === title",
    "change-create-theme-market",
    "switch editor market back to KRX",
    "newThemeMarket === \"krx\"",
    "operatorCatalogNameValue === title",
    "removedDraftRowId: draftRowId"
  ]);

  const create = section(
    harness,
    "async function createReadEditDeleteTheme",
    "async function searchAndSelectMainExpert");
  assertInOrder(create, [
    "let added = await addOperatorCatalogStockByDoubleClick(",
    "await verifyKrxThemeCreateMarketRoundTrip(",
    "duplicate recheck after market round-trip",
    "name available after market round-trip",
    "stock re-add after market round-trip",
    "let expectedSavedRows = 1"
  ]);
});

test("playlist Shift-click deletion is one physical Windows exchange and restores three rows", () => {
  const setup = section(
    harness,
    "async function executeSafeSequence",
    "async function executeDryRunPlayoutSequence");
  assertInOrder(setup, [
    '"stage first disposable Shift-delete row"',
    "await sleep(legacyCutDoubleClickWindowMilliseconds + 100)",
    '"stage second disposable Shift-delete row"'
  ]);
  assert.match(harness, /const legacyCutDoubleClickWindowMilliseconds = 500;/u);

  const request = section(
    harness,
    "async function requestWindowsInput(",
    "async function installProbe");
  assertInOrder(request, [
    "beforeOrder.length !== 5",
    "reorderedOrder.at(-2) !== rangeStartRowId",
    "const afterOrder = reorderedOrder.filter",
    'operation: "row-header-drag-home-shift-range-delete-restore"',
    "expectedRangeSelectedRowIds: [rangeStartRowId, rangeEndRowId]",
    "mouseDownCalls: 4",
    "shiftKeyDownCalls: 1",
    "deleteKeyCalls: 1",
    '"delete-selected-playlist-rows"',
    "selections[1].shift !== true",
    "shiftRangeDeletedRowIds = [rangeStartRowId, rangeEndRowId]"
  ]);
  assert.doesNotMatch(request, /Input\.dispatchMouseEvent/u);

  const native = section(
    runner,
    "public static void SendShiftClickRangeThenDeleteAndRestore(",
    "public static void SendApplicationClick(");
  assertInOrder(native, [
    "SendKeyboard(VirtualKeyShift, false)",
    "SendMouseButton(MouseLeftDown)",
    "SendKeyboard(VirtualKeyShift, true)",
    "SendKeyboard(VirtualKeyDelete, false)",
    "SendKeyboard(VirtualKeyDelete, true)",
    "SendMouseMove(restore.X, restore.Y)"
  ]);
});

test("PList fresh readback uses the second physical double-click exchange", () => {
  const refresh = section(
    harness,
    "function namedPlaylistCommandSequence",
    "async function requestNamedPlaylistLoadDoubleClick");
  assertInOrder(refresh, [
    "Number.isSafeInteger(sequence)",
    'result?.command === "refresh-named-playlists"',
    "result.succeeded === true",
    "result.sequence > baselineSequence",
    'listFreshness === "fresh"'
  ]);

  const request = section(
    harness,
    "async function requestNamedPlaylistLoadDoubleClick",
    "function namedPlaylistDeleteWriteCount");
  assertInOrder(request, [
    "configuration.windowsInputExchangeCount !== 2",
    'current.dom.namedTitle !== "DB 재생목록 불러오기"',
    "must start on the freshly read, unselected saved row",
    "const beforeContent = playlistContentProjection(current)",
    'operation: "named-playlist-load-double-click"',
    "mouseDownCalls: 2",
    "doubleClickIntervalMilliseconds: 100",
    'snapshot.state.commandResult?.command === "load-named-playlist-by-id"',
    "JSON.stringify(playlistContentProjection(snapshot))",
    '["select-named-playlist", "load-named-playlist-by-id"]',
    "loads.length !== 1",
    "selections.length !== 1",
    "approvedDatabaseWriteMessages.length !== beforeApprovedWriteCount",
    "startedUnselected: true",
    "selectionIntentCount: selections.length",
    "contentRestored: true"
  ]);
  assert.doesNotMatch(request, /pointerDoubleClick|Input\.dispatchMouseEvent/u);

  const crud = section(
    harness,
    "async function executeNamedPlaylistCrud",
    "const manualFinancialScreens");
  assertInOrder(crud, [
    "PList reopen for fresh readback",
    "completedNamedPlaylistRefreshAfter(snapshot, readbackRefreshBaseline)",
    "matches[0].isSelected !== true",
    "requestNamedPlaylistLoadDoubleClick(",
    '"PList physical saved-row double-click load"',
    '"PList physical double-click fresh readback"',
    "PList reopen for delete",
    "completedNamedPlaylistRefreshAfter(snapshot, deleteRefreshBaseline)"
  ]);
  assert.doesNotMatch(crud, /PList select fresh readback|PList load fresh rows/u);

  const native = section(
    runner,
    "public static void SendApplicationDoubleClick(",
    "public static IntPtr ReadForegroundWindow(");
  assert.match(native, /for \(int click = 0; click < 2; click\+\+\)/u);
  assert.match(native, /SendMouseButton\(MouseLeftDown\)/u);
  assert.match(native, /SendMouseButton\(MouseLeftUp\)/u);
  assert.doesNotMatch(native, /Input\.dispatchMouseEvent/u);
});
