"use strict";

const test = require("node:test");
const assert = require("node:assert/strict");
const workflow = require("../../Web/named-foreign-stock-restore-workflow.js");

const actions = [
  ["1열판기본", "", "stock-current", "s5001", "5001", "CURRENT", null],
  ["캔들그래프", "5일", "stock-candle-5d", "s8010", "8061", "PRICE", 5],
  ["캔들그래프", "20일", "stock-candle-20d", "s8010", "8040", "PRICE", 20],
  ["캔들그래프", "60일", "stock-candle-60d", "s8010", "8046", "PRICE", 60],
  ["캔들그래프", "120일", "stock-candle-120d", "s8010", "8051", "PRICE", 120],
  ["캔들그래프", "240일", "stock-candle-240d", "s8010", "8056", "PRICE", 240]
];

function raw(graphicType = "1열판기본", subtype = "", itemIndex = 0, enabled = true) {
  return {
    itemIndex,
    enabled,
    groupCode: "해외종목",
    subject: "NVIDIA",
    graphicType,
    subtype,
    pageText: "1/1",
    dataCode: ""
  };
}

function options(id = "foreign-stock-entry") {
  return { id, fadeDuration: 6, ma5: true, ma20: false };
}

function outcome(pending, overrides = {}) {
  return {
    itemIndex: pending.itemIndex,
    status: "resolved",
    actionId: pending.actionId,
    inputName: pending.inputName,
    symbol: "NAS@NVDA",
    nationCode: "US",
    foreignDomesticCode: "1",
    builderKey: pending.builderKey,
    cutCode: pending.cutCode,
    valueType: pending.valueType,
    periodDays: pending.periodDays,
    ...overrides
  };
}

function response(requestId, pendingRows, rows = pendingRows.map(value => outcome(value))) {
  return {
    requestId,
    retrievedAt: "2026-07-12T12:34:56+09:00",
    totalRowCount: rows.length,
    rows
  };
}

test("only the exact legacy overseas-stock grammar classifies all six UC5 actions", () => {
  actions.forEach(([graphicType, subtype, actionId, builderKey, cutCode, valueType, periodDays], index) => {
    const pending = workflow.classify(raw(graphicType, subtype, index, index !== 5), options(`foreign-${index}`));
    assert.ok(pending);
    assert.equal(workflow.isPending(pending), true);
    assert.equal(pending.actionId, actionId);
    assert.equal(pending.builderKey, builderKey);
    assert.equal(pending.cutCode, cutCode);
    assert.equal(pending.valueType, valueType);
    assert.equal(pending.periodDays, periodDays);
    assert.equal(pending.enabled, index !== 5);
    assert.deepEqual(pending.movingAverages,
      builderKey === "s8010" ? { ma5: true, ma20: false } : { ma5: false, ma20: false });
  });

  for (const invalid of [
    { ...raw(), groupCode: "FOREIGN_STOCK", dataCode: "NAS@NVDA|US|1" },
    { ...raw(), groupCode: " 해외종목" },
    { ...raw(), subject: " NVIDIA" },
    { ...raw(), graphicType: "CURRENT" },
    { ...raw("캔들그래프", "5일"), subtype: "1년" },
    { ...raw(), pageText: "1/2" },
    { ...raw(), dataCode: "NAS@NVDA" },
    { ...raw(), itemIndex: 1000 }
  ]) {
    assert.equal(workflow.classify(invalid, options("invalid-row")), null);
  }
  assert.equal(workflow.classify(raw(), { ...options(), extra: true }), null);
});

test("a correlated unique Oracle identity materializes current and candle entries", () => {
  for (const [graphicType, subtype, , builderKey, cutCode] of actions) {
    const pending = workflow.classify(raw(graphicType, subtype, 7, false), options(`entry-${cutCode}`));
    const normalized = workflow.normalizeResponse(
      response("load-one", [pending]),
      "load-one",
      [pending]);
    assert.ok(normalized);
    const entry = workflow.materialize(pending, normalized.rows[0]);
    assert.ok(entry);
    assert.equal(workflow.isMaterializedEntry(entry), true);
    assert.equal(entry.builderKey, builderKey);
    assert.equal(entry.code, cutCode);
    assert.equal(entry.enabled, false);
    assert.equal(entry.symbol, "NAS@NVDA");
    assert.equal(entry.nationCode, "US");
    assert.equal(entry.fdtc, "1");
    assert.equal(entry.operator.source, workflow.source);
    assert.equal(entry.selection.groupCode, "FOREIGN_STOCK");
    assert.equal(entry.selection.dataCode, "NAS@NVDA|US|1");
    assert.equal(entry.selection.graphicType, builderKey === "s8010" ? "MA5" : "1열판기본");
  }
});

test("a correlated Hangul-letter provider symbol remains a bound closed identity", () => {
  const pending = workflow.classify(
    raw("캔들그래프", "5일", 8),
    options("hangul-symbol"));
  const normalized = workflow.normalizeResponse(response(
    "hangul-symbol-load",
    [pending],
    [outcome(pending, { symbol: "NAS@해외 종목1" })]),
  "hangul-symbol-load",
  [pending]);
  assert.ok(normalized);
  const entry = workflow.materialize(pending, normalized.rows[0]);
  assert.ok(entry);
  assert.equal(entry.selection.dataCode, "NAS@해외 종목1|US|1");
  assert.ok(workflow.restorePlaylistEntry(entry));
});

test("safe supplementary input-name and letter symbols match the native scalar contract", () => {
  const pending = workflow.classify(
    { ...raw("1열판기본", "", 10), subject: "회사🚀" },
    options("supplementary-symbol"));
  const normalized = workflow.normalizeResponse(response(
    "supplementary-symbol-load",
    [pending],
    [outcome(pending, { inputName: "회사🚀", symbol: "NAS@𐐀1" })]),
  "supplementary-symbol-load",
  [pending]);

  assert.ok(normalized);
  const entry = workflow.materialize(pending, normalized.rows[0]);
  assert.ok(entry);
  assert.equal(entry.selection.dataCode, "NAS@𐐀1|US|1");
  assert.ok(workflow.restorePlaylistEntry(entry));
});

test("batch count, order, request, action and identity correlation are strict", () => {
  const first = workflow.classify(raw("캔들그래프", "5일", 4), options("first"));
  const secondRaw = { ...raw("1열판기본", "", 9), subject: "TSMC" };
  const second = workflow.classify(secondRaw, options("second"));
  const rows = [outcome(first), outcome(second, { inputName: "TSMC", symbol: "TPE@2330", nationCode: "TW" })];
  const exact = response("load-batch", [first, second], rows);
  assert.ok(workflow.normalizeResponse(exact, "load-batch", [first, second]));
  assert.equal(workflow.normalizeResponse(exact, "stale", [first, second]), null);
  assert.equal(workflow.normalizeResponse({ ...exact, totalRowCount: 1 }, "load-batch", [first, second]), null);
  assert.equal(workflow.normalizeResponse({ ...exact, rows: [...rows].reverse() }, "load-batch", [first, second]), null);
  assert.equal(workflow.normalizeResponse({ ...exact, extra: true }, "load-batch", [first, second]), null);

  for (const [key, value] of [
    ["actionId", "stock-current"],
    ["inputName", "TSMC"],
    ["nationCode", "KR"],
    ["foreignDomesticCode", "0"],
    ["builderKey", "s5001"],
    ["cutCode", "5001"],
    ["valueType", "VOLUME"],
    ["periodDays", 20]
  ]) {
    const tampered = [{ ...rows[0], [key]: value }, rows[1]];
    assert.equal(workflow.normalizeResponse({ ...exact, rows: tampered }, "load-batch", [first, second]), null, key);
  }
});

test("terminal per-row failures, forged capability objects and retryable errors fail closed", () => {
  const pending = workflow.classify(raw("캔들그래프", "120일"), options("failed"));
  for (const failure of [
    "INVALID_ROW", "IDENTITY_NOT_FOUND", "IDENTITY_AMBIGUOUS",
    "DATABASE_DATA_INVALID", "RESTORE_FAILED"
  ]) {
    const normalized = workflow.normalizeResponse(response("failed-load", [pending], [{
      itemIndex: pending.itemIndex,
      status: "failed",
      failure
    }]), "failed-load", [pending]);
    assert.ok(normalized);
    assert.equal(workflow.materialize(pending, normalized.rows[0]), null);
  }
  const normalized = workflow.normalizeResponse(response("resolved-load", [pending]), "resolved-load", [pending]);
  assert.equal(workflow.materialize({ ...pending }, normalized.rows[0]), null);
  assert.equal(workflow.materialize(pending, { ...normalized.rows[0] }), null);

  const error = {
    requestId: "error-load",
    code: "PLAYOUT_PRIORITY",
    message: "송출 우선 처리로 취소했습니다.",
    retryable: false
  };
  assert.deepEqual(workflow.normalizeError(error, "error-load"), error);
  assert.equal(workflow.normalizeError({ ...error, retryable: true }, "error-load"), null);
  assert.equal(workflow.normalizeError(error, "stale"), null);
});

test("verified identity, moving averages and original seven-field signature round-trip together", () => {
  const storedRaw = raw("캔들그래프", "240일", 3, false);
  const pending = workflow.classify(storedRaw, options("round-trip"));
  const normalized = workflow.normalizeResponse(response("round-trip-load", [pending]), "round-trip-load", [pending]);
  const entry = workflow.materialize(pending, normalized.rows[0]);
  const restored = workflow.restorePlaylistEntry(entry);
  assert.ok(restored);
  assert.deepEqual(restored.operator.movingAverages, { ma5: true, ma20: false });
  assert.deepEqual(workflow.legacySelectionForEntry(restored), {
    groupCode: "해외종목",
    subject: "NVIDIA",
    graphicType: "캔들그래프",
    subtype: "240일",
    dataCode: ""
  });
  assert.equal(workflow.matchesRaw(restored, storedRaw), true);
  assert.equal(workflow.matchesRaw(restored, { ...storedRaw, subtype: "120일" }), false);

  for (const tampered of [
    { ...entry, code: "8040" },
    { ...entry, selection: { ...entry.selection, dataCode: "TPE@2330|TW|1" } },
    { ...entry, operator: { ...entry.operator, identity: { ...entry.operator.identity, symbol: "TPE@2330" } } },
    { ...entry, operator: { ...entry.operator, rawSelection: { ...entry.operator.rawSelection, subtype: "120일" } } },
    { ...entry, operator: { ...entry.operator, movingAverages: { ma5: false, ma20: false } } },
    { ...entry, extra: true }
  ]) {
    assert.equal(workflow.restorePlaylistEntry(tampered), null);
  }
});

test("enabled replacement preserves foreign-stock identity proof without mutating the frozen entry", () => {
  const storedRaw = raw("1열판기본", "", 2, true);
  const pending = workflow.classify(storedRaw, options("foreign-toggle"));
  const normalized = workflow.normalizeResponse(response("foreign-toggle-load", [pending]),
    "foreign-toggle-load", [pending]);
  const entry = workflow.materialize(pending, normalized.rows[0]);
  const replacement = workflow.withEnabled(entry, false);

  assert.notEqual(replacement, entry);
  assert.equal(Object.isFrozen(replacement), true);
  assert.equal(entry.enabled, true);
  assert.equal(replacement.enabled, false);
  assert.equal(workflow.isMaterializedEntry(replacement), true);
  assert.equal(workflow.matchesRaw(replacement, { ...storedRaw, enabled: false }), true);
  assert.deepEqual(workflow.legacySelectionForEntry(replacement),
    workflow.legacySelectionForEntry(entry));
  assert.equal(workflow.withEnabled({ ...entry }, false), null);
});
