"use strict";

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

const versionA = "a".repeat(64);
const versionB = "b".repeat(64);

const netRows = () => Array.from({ length: 5 }, (_, index) => ({
  leftName: `좌측${index + 1}`,
  leftAmount: `${index + 1},000`,
  rightName: `우측${index + 1}`,
  rightAmount: `-${index + 1},000`
}));

const viItems = () => [
  { code: "P005930", name: "삼성전자" },
  { code: "P000660", name: "SK하이닉스" },
  { code: "D035720", name: "카카오" }
];

function fixture() {
  let sequence = 0;
  let externallyLocked = false;
  const posts = [];
  const entries = [];
  const toasts = [];
  const logs = [];
  const controller = ui.createController({
    postNative: (type, payload) => posts.push({ type, payload }),
    appendPlaylistEntry: entry => entries.push(entry),
    isLocked: () => externallyLocked,
    showToast: (message, kind) => toasts.push({ message, kind }),
    addLog: message => logs.push(message),
    createId: () => `manual_ui_${++sequence}`
  });
  return {
    controller,
    posts,
    entries,
    toasts,
    logs,
    setExternalLock: value => { externallyLocked = value; },
    latest: type => [...posts].reverse().find(item => item.type === type)
  };
}

function enableManualStore(context) {
  const requestId = context.controller.getState().pending.status;
  assert.equal(context.controller.handleMessage("manual-operator-data-status", {
    requestId,
    available: true,
    code: "READY",
    writeQuarantined: false,
    message: null
  }), true);
}

test("interrupted import status remains explicit and disables every manual store action", () => {
  const context = fixture();
  context.controller.openVi();
  const requestId = context.controller.getState().pending.status;

  assert.equal(context.controller.handleMessage("manual-operator-data-status", {
    requestId,
    available: false,
    code: "IMPORT_INTERRUPTED",
    writeQuarantined: false,
    message: "A prior import is unresolved; no FSell or VI file was opened."
  }), true);

  const state = context.controller.getState();
  assert.equal(state.available, false);
  assert.equal(state.unavailableCode, "IMPORT_INTERRUPTED");
  assert.equal(
    state.unavailableMessage,
    "A prior import is unresolved; no FSell or VI file was opened.");
  assert.equal(context.controller.actions.importLegacyManualData(), false);
  assert.equal(context.controller.actions.saveVi(), false);
});

test("FSell cut opens only after five saved rows are re-read unchanged", () => {
  const context = fixture();
  const { controller } = context;
  controller.openNetSell("FOREIGN");
  enableManualStore(context);
  const initialRead = controller.getState().pending.netRead;
  assert.equal(controller.handleMessage("manual-net-sell-data", {
    requestId: initialRead,
    audience: "FOREIGN",
    rows: netRows()
  }), true);
  assert.equal(controller.actions.addNetSellCut(), false);

  assert.equal(controller.actions.saveNetSell(), true);
  const save = context.latest("save-manual-net-sell-data");
  assert.equal(save.payload.rows.length, 5);
  assert.equal(controller.handleMessage("manual-net-sell-save-result", {
    requestId: save.payload.requestId,
    audience: "FOREIGN",
    saved: true,
    recovered: false
  }), true);
  assert.equal(controller.actions.addNetSellCut(), false);

  const verificationRead = controller.getState().pending.netRead;
  assert.equal(controller.handleMessage("manual-net-sell-data", {
    requestId: verificationRead,
    audience: "FOREIGN",
    rows: netRows()
  }), true);
  assert.equal(controller.getState().netVerified, true);
  assert.equal(controller.actions.addNetSellCut(), true);
  assert.equal(context.entries[0].builderKey, "s5025");

  controller.actions.setNetCell(0, "leftAmount", "변경");
  assert.equal(controller.actions.addNetSellCut(), false);
});

test("VI cut binds the latest version and reorder requires save plus matching reread", () => {
  const context = fixture();
  const { controller } = context;
  controller.openVi();
  enableManualStore(context);
  const read = controller.getState().pending.viRead;
  assert.equal(controller.handleMessage("vi-manual-list-data", {
    requestId: read,
    itemCount: 3,
    pageCount: 1,
    items: viItems(),
    version: versionA
  }), true);
  assert.equal(controller.actions.addViCut(), true);
  assert.equal(context.entries[0].selection.subject, "@MBN_VI_SNAPSHOT_V1");
  assert.equal(context.entries[0].selection.dataCode, versionA);

  assert.equal(controller.actions.moveViItem(2, -1), true);
  assert.deepEqual(controller.getState().viItems.map(item => item.name), [
    "삼성전자", "카카오", "SK하이닉스"
  ]);
  assert.equal(controller.getState().viBaseVersion, versionA);
  assert.equal(controller.getState().viVersion, null);
  assert.equal(controller.actions.addViCut(), false);

  assert.equal(controller.actions.saveVi(), true);
  const save = context.latest("save-vi-manual-list");
  assert.equal(save.payload.expectedVersion, versionA);
  assert.equal(controller.handleMessage("vi-manual-list-save-result", {
    requestId: save.payload.requestId,
    saved: true,
    recovered: false,
    persisted: true,
    itemCount: 3,
    pageCount: 1,
    version: versionB
  }), true);
  const verify = controller.getState().pending.viRead;
  assert.equal(controller.handleMessage("vi-manual-list-data", {
    requestId: verify,
    itemCount: 3,
    pageCount: 1,
    items: save.payload.items,
    version: versionB
  }), true);
  assert.equal(controller.getState().viVerified, true);
  assert.equal(controller.actions.addViCut(), true);
  assert.equal(context.entries[1].selection.dataCode, versionB);
  assert.deepEqual(context.entries[1].operator.items, save.payload.items);
});

test("VI mismatched post-save version never opens the playlist gate", () => {
  const context = fixture();
  const { controller } = context;
  controller.openVi();
  enableManualStore(context);
  controller.handleMessage("vi-manual-list-data", {
    requestId: controller.getState().pending.viRead,
    itemCount: 3,
    pageCount: 1,
    items: viItems(),
    version: versionA
  });
  controller.actions.moveViItem(2, -1);
  controller.actions.saveVi();
  const save = context.latest("save-vi-manual-list");
  controller.handleMessage("vi-manual-list-save-result", {
    requestId: save.payload.requestId,
    saved: true,
    recovered: false,
    persisted: true,
    itemCount: 3,
    pageCount: 1,
    version: versionB
  });
  controller.handleMessage("vi-manual-list-data", {
    requestId: controller.getState().pending.viRead,
    itemCount: 3,
    pageCount: 1,
    items: save.payload.items,
    version: versionA
  });
  assert.equal(controller.getState().viVerified, false);
  assert.equal(controller.actions.addViCut(), false);
});

test("VI stale-data conflict is terminal, preserves the draft, and requires reread", () => {
  const context = fixture();
  const { controller } = context;
  controller.openVi();
  enableManualStore(context);
  controller.handleMessage("vi-manual-list-data", {
    requestId: controller.getState().pending.viRead,
    itemCount: 3,
    pageCount: 1,
    items: viItems(),
    version: versionA
  });
  controller.actions.moveViItem(2, -1);
  const draft = controller.getState().viItems;
  assert.equal(controller.actions.saveVi(), true);
  const save = context.latest("save-vi-manual-list");
  const postCount = context.posts.length;

  assert.equal(controller.handleMessage("manual-operator-data-error", {
    requestId: save.payload.requestId,
    operation: "save-vi",
    code: "STALE_DATA",
    message: "The VI list changed. Reload before saving.",
    retryable: false,
    outcomeUnknown: false
  }), true);
  assert.deepEqual(controller.getState().viItems, draft);
  assert.equal(controller.getState().viBaseVersion, null);
  assert.equal(controller.getState().viVersion, null);
  assert.equal(controller.getState().viVerified, false);
  assert.equal(controller.getState().quarantined, false);
  assert.equal(controller.actions.saveVi(), false);
  assert.equal(context.posts.length, postCount);
});

test("empty VI save is a successful no-op and can never create an empty cut", () => {
  const context = fixture();
  const { controller } = context;
  controller.openVi();
  enableManualStore(context);
  controller.handleMessage("vi-manual-list-data", {
    requestId: controller.getState().pending.viRead,
    itemCount: 3,
    pageCount: 1,
    items: viItems(),
    version: versionA
  });
  controller.actions.clearViItems();
  assert.equal(controller.actions.addViCut(), false);
  assert.equal(controller.actions.saveVi(), true);
  const save = context.latest("save-vi-manual-list");
  assert.equal(save.payload.expectedVersion, versionA);
  assert.deepEqual(save.payload.items, []);
  assert.equal(controller.handleMessage("vi-manual-list-save-result", {
    requestId: save.payload.requestId,
    saved: true,
    recovered: false,
    persisted: false,
    itemCount: 3,
    pageCount: 1,
    version: versionA
  }), true);
  assert.ok(context.toasts.some(item => item.message.includes("변경하지 않았습니다")));

  controller.handleMessage("vi-manual-list-data", {
    requestId: controller.getState().pending.viRead,
    itemCount: 3,
    pageCount: 1,
    items: viItems(),
    version: versionA
  });
  assert.equal(controller.getState().viItems.length, 3);
  controller.actions.clearViItems();
  assert.equal(controller.actions.addViCut(), false);
});

test("stale correlations are ignored and OutcomeUnknown quarantines every later save", () => {
  const context = fixture();
  const { controller } = context;
  controller.openVi();
  enableManualStore(context);
  const staleRead = controller.getState().pending.viRead;
  controller.actions.requestViRead();
  const currentRead = controller.getState().pending.viRead;
  assert.notEqual(staleRead, currentRead);
  assert.equal(controller.handleMessage("vi-manual-list-data", {
    requestId: staleRead,
    itemCount: 3,
    pageCount: 1,
    items: viItems(),
    version: versionA
  }), false);
  assert.equal(controller.getState().viItems.length, 0);
  controller.handleMessage("vi-manual-list-data", {
    requestId: currentRead,
    itemCount: 3,
    pageCount: 1,
    items: viItems(),
    version: versionA
  });
  assert.equal(controller.actions.saveVi(), true);
  const save = context.latest("save-vi-manual-list");
  assert.equal(controller.handleMessage("manual-operator-data-error", {
    requestId: save.payload.requestId,
    operation: "save-vi",
    code: "OUTCOME_UNKNOWN",
    message: "저장 결과를 확인할 수 없습니다.",
    retryable: false,
    outcomeUnknown: true
  }), true);
  assert.equal(controller.getState().quarantined, true);
  const postCount = context.posts.length;
  assert.equal(controller.actions.saveVi(), false);
  assert.equal(context.posts.length, postCount);
});

test("stock search is exact and correlated before it can append a VI row", () => {
  const context = fixture();
  const { controller } = context;
  controller.openVi();
  assert.equal(controller.actions.searchStocks("삼성"), true);
  const search = context.latest("search-stocks");
  assert.equal(controller.handleMessage("stock-search-results", {
    requestId: "stale_request",
    query: "삼성",
    retrievedAt: "2026-07-12T00:00:00+09:00",
    totalRowCount: 1,
    truncated: false,
    results: [{ market: "kospi", source: "oracle", name: "삼성전자", code: "005930" }]
  }), false);
  assert.equal(controller.handleMessage("stock-search-results", {
    requestId: search.payload.requestId,
    query: "삼성",
    retrievedAt: "2026-07-12T00:00:00+09:00",
    totalRowCount: 1,
    truncated: false,
    results: [{ market: "kospi", source: "oracle", name: "삼성전자", code: "005930" }]
  }), true);
  assert.equal(controller.actions.addSearchResult(0), true);
  assert.deepEqual(controller.getState().viItems, [{ code: "P005930", name: "삼성전자" }]);
});

test("explicit and host locks block edits, saves, and cut creation", () => {
  const context = fixture();
  const { controller } = context;
  controller.openNetSell("INDIVIDUAL");
  enableManualStore(context);
  controller.handleMessage("manual-net-sell-data", {
    requestId: controller.getState().pending.netRead,
    audience: "INDIVIDUAL",
    rows: netRows()
  });
  controller.setLocked(true);
  assert.equal(controller.actions.setNetCell(0, "leftName", "변경"), false);
  assert.equal(controller.actions.saveNetSell(), false);
  controller.setLocked(false);
  context.setExternalLock(true);
  assert.equal(controller.actions.saveNetSell(), false);
  assert.equal(controller.getState().locked, true);
});

test("legacy FSell and VI import requires explicit confirmation and sends no path", () => {
  const context = fixture();
  const { controller } = context;
  const previousConfirm = globalThis.confirm;
  try {
    controller.openVi();
    enableManualStore(context);
    controller.handleMessage("vi-manual-list-data", {
      requestId: controller.getState().pending.viRead,
      itemCount: 0,
      pageCount: 0,
      items: [],
      version: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
    });

    globalThis.confirm = () => false;
    const before = context.posts.length;
    assert.equal(controller.actions.importLegacyManualData(), false);
    assert.equal(context.posts.length, before);

    let prompt = "";
    globalThis.confirm = message => {
      prompt = message;
      return true;
    };
    assert.equal(controller.actions.importLegacyManualData(), true);
    const request = context.latest("import-legacy-manual-operator-data");
    assert.deepEqual(Object.keys(request.payload), ["requestId"]);
    assert.equal(/path|directory|sourceRoot/iu.test(JSON.stringify(request)), false);
    assert.match(prompt, /기존 대상 파일이 하나라도 있으면/u);
    assert.match(prompt, /덮어쓰지 않고 중단/u);

    assert.equal(controller.handleMessage("legacy-manual-operator-import-result", {
      requestId: request.payload.requestId,
      markerVersion: 1,
      sourceSha256: "b".repeat(64),
      importedAt: "2026-07-12T01:02:03+00:00",
      netSellRowCount: 15,
      viItemCount: 9
    }), true);
    assert.ok(controller.getState().pending.viRead);
    assert.ok(context.toasts.some(item => item.message.includes("가져왔습니다")));
  } finally {
    if (previousConfirm === undefined) delete globalThis.confirm;
    else globalThis.confirm = previousConfirm;
  }
});

test("legacy import failure is terminal and never auto-retries", () => {
  const context = fixture();
  const { controller } = context;
  const previousConfirm = globalThis.confirm;
  try {
    globalThis.confirm = () => true;
    controller.openVi();
    enableManualStore(context);
    controller.handleMessage("vi-manual-list-data", {
      requestId: controller.getState().pending.viRead,
      itemCount: 0,
      pageCount: 0,
      items: [],
      version: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
    });
    controller.actions.importLegacyManualData();
    const request = context.latest("import-legacy-manual-operator-data");
    const before = context.posts.length;

    assert.equal(controller.handleMessage("manual-operator-data-error", {
      requestId: request.payload.requestId,
      operation: "import-legacy-manual-data",
      code: "DESTINATION_NOT_EMPTY",
      message: "기존 데이터가 있어 아무 파일도 덮어쓰지 않았습니다.",
      retryable: false,
      outcomeUnknown: false
    }), true);
    assert.equal(context.posts.length, before);
    assert.equal(controller.getState().pending.legacyImport, null);
  } finally {
    if (previousConfirm === undefined) delete globalThis.confirm;
    else globalThis.confirm = previousConfirm;
  }
});

test("controller source constructs DOM without HTML injection or filesystem paths", () => {
  const source = fs.readFileSync(
    path.resolve(__dirname, "..", "..", "Web", "manual-lists-ui.js"),
    "utf8");
  assert.equal(source.includes("innerHTML"), false);
  assert.equal(source.includes("insertAdjacentHTML"), false);
  assert.equal(/(?:[A-Z]:\\|file:\/\/)/u.test(source), false);
});
