const test = require("node:test");
const assert = require("node:assert/strict");

const workflow = require("../../Web/named-manual-restore-workflow.js");
const manualLists = require("../../Web/manual-lists-workflow.js");
const manualFinancial = require("../../Web/manual-financial-workflow.js");
const namedPlaylists = require("../../Web/named-playlist-workflow.js");

function raw(overrides = {}) {
  return {
    itemIndex: 0,
    enabled: true,
    groupCode: "INDIVIDUAL",
    subject: "INDEX",
    graphicType: "MANUAL_NET_SELL",
    subtype: "MANUAL_NET_SELL",
    dataCode: "",
    pageText: "1/1",
    ...overrides
  };
}

const options = { id: "db-invalid-00000001-0", fadeDuration: 6 };
const rows = Array.from({ length: 5 }, (_, index) => ({
  leftName: `L${index}`, leftAmount: `${index}`,
  rightName: `R${index}`, rightAmount: `${index + 10}`
}));

test("s5025 raw restoration accepts only canonical or exact legacy audience signatures", () => {
  const canonical = workflow.classify(raw(), options);
  assert.equal(canonical.kind, "net-sell");
  assert.equal(canonical.audience, "INDIVIDUAL");

  const legacy = workflow.classify(raw({
    groupCode: "외국인 순매도 상위(수동)",
    subject: "지수",
    graphicType: "순매도 상위",
    subtype: "순매도 상위"
  }), options);
  assert.equal(legacy.audience, "FOREIGN");
  assert.equal(legacy.historical, true);
  assert.equal(workflow.classify(raw({
    groupCode: "외국인 순매도 상위",
    subject: "지수",
    graphicType: "순매도 상위",
    subtype: "순매도 상위"
  }), options), null);
  assert.equal(workflow.classify(raw({ dataCode: "path.dat" }), options), null);
  assert.equal(workflow.classify(raw({ pageText: "1/2" }), options), null);
});

test("s5025 becomes playable only after one correlated trusted five-row read", () => {
  const pending = workflow.classify(raw({ enabled: false }), options);
  const request = workflow.createManualListReadRequest("named-net-read", pending);
  const entry = workflow.materializeManualList(pending, {
    requestId: "named-net-read",
    audience: "INDIVIDUAL",
    rows
  }, request);
  assert.ok(entry);
  assert.equal(entry.enabled, false);
  assert.equal(entry.builderKey, "s5025");
  assert.equal(manualLists.isPlaylistEntryPlayable(entry), true);
  assert.equal(manualLists.restorePlaylistEntry(entry).operator.audience, "INDIVIDUAL");
  assert.equal(workflow.materializeManualList(pending, {
    requestId: "stale-read",
    audience: "INDIVIDUAL",
    rows
  }, request), null);
});

test("versioned and historical VI raw rows require the current exact native snapshot", () => {
  const version = "a".repeat(64);
  const current = workflow.classify(raw({
    groupCode: "PAGED_VI",
    subject: manualLists.viSnapshotSubject,
    graphicType: "INPUT_ORDER",
    subtype: "CURRENT",
    dataCode: version
  }), options);
  const request = workflow.createManualListReadRequest("named-vi-read", current);
  const payload = {
    requestId: "named-vi-read",
    itemCount: 2,
    pageCount: 1,
    items: [{ code: "005930", name: "삼성전자" }, { code: "000660", name: "SK하이닉스" }],
    version
  };
  assert.equal(workflow.materializeManualList(current, payload, request).builderKey, "s5074");
  assert.equal(workflow.materializeManualList(current, { ...payload, version: "b".repeat(64) }, request), null);
  assert.equal(workflow.materializeManualList(
    workflow.classify(raw({
      groupCode: "PAGED_VI",
      subject: manualLists.viSnapshotSubject,
      graphicType: "INPUT_ORDER",
      subtype: "CURRENT",
      dataCode: version,
      pageText: "1/2"
    }), options),
    payload,
    request), null);

  const historical = workflow.classify(raw({
    groupCode: "",
    subject: "삼성전자,SK하이닉스",
    graphicType: "5단 표그래프",
    subtype: "VI 발동(수동)",
    dataCode: ""
  }), options);
  const historicalRequest = workflow.createManualListReadRequest("named-vi-history", historical);
  assert.ok(workflow.materializeManualList(historical, {
    ...payload,
    requestId: "named-vi-history"
  }, historicalRequest));
  assert.equal(workflow.materializeManualList(historical, {
    ...payload,
    requestId: "named-vi-history",
    items: [...payload.items].reverse()
  }, historicalRequest), null);
  const duplicate = workflow.classify(raw({
    groupCode: "",
    subject: "삼성전자,삼성전자",
    graphicType: "5단 표그래프",
    subtype: "VI 발동(수동)"
  }), options);
  const duplicateRequest = workflow.createManualListReadRequest("named-vi-duplicate", duplicate);
  assert.ok(workflow.materializeManualList(duplicate, {
    ...payload,
    requestId: "named-vi-duplicate",
    items: [payload.items[0], payload.items[0]]
  }, duplicateRequest));
  assert.equal(workflow.materializeManualList(duplicate, {
    ...payload,
    requestId: "named-vi-duplicate"
  }, duplicateRequest), null);
  assert.equal(workflow.classify(raw({
    groupCode: "",
    subject: "삼성전자,SK하이닉스",
    graphicType: "5단 표그래프",
    subtype: "VI 발동(수동)",
    pageText: "1/2"
  }), options), null);
  assert.equal(workflow.classify(raw({
    groupCode: "",
    subject: "삼성전자, SK하이닉스",
    graphicType: "5단 표그래프",
    subtype: "VI 발동(수동)"
  }), options), null);
});

test("GraphE raw rows yield only a fresh-load descriptor with exact screen, market and name", () => {
  for (const profile of manualFinancial.sourceContract.screens) {
    const pending = workflow.classify(raw({
      groupCode: "코스피",
      subject: "삼성전자",
      graphicType: profile.label,
      subtype: "",
      dataCode: ""
    }), options);
    assert.equal(pending.kind, "financial");
    assert.equal(pending.screen, profile.screen);
    assert.equal(pending.builderKey, profile.builderKey);
    assert.equal(pending.market, "kospi");
    assert.deepEqual(pending.identity, { screen: profile.screen, stockName: "삼성전자" });
  }
  const profile = manualFinancial.sourceContract.screens[0];
  assert.equal(workflow.classify(raw({
    groupCode: "KOSPI",
    subject: "삼성전자",
    graphicType: profile.label,
    subtype: "",
    dataCode: ""
  }), options), null);
  assert.equal(workflow.classify(raw({
    groupCode: "코스피",
    subject: "삼성전자",
    graphicType: profile.graphicType,
    subtype: "",
    dataCode: ""
  }), options), null);
  assert.equal(workflow.classify(raw({
    groupCode: "UNKNOWN",
    subject: "삼성전자",
    graphicType: profile.graphicType,
    subtype: "",
    dataCode: ""
  }), options), null);
  assert.equal(workflow.classify(raw({
    groupCode: "KOSPI",
    subject: "삼성전자",
    graphicType: profile.graphicType,
    subtype: "",
    dataCode: "005930"
  }), options), null);
  assert.equal(workflow.classify(raw({
    groupCode: "KOSPI",
    subject: "삼성전자",
    graphicType: profile.graphicType,
    subtype: "",
    dataCode: "",
    pageText: "1/2"
  }), options), null);
});

function createFreshGraphEntry() {
  const screen = "sales";
  const record = {
    kind: screen,
    stockName: "삼성전자",
    quarters: ["1Q", "2Q", "3Q", "4Q", "5Q", "6Q"]
      .map((quarter, index) => ({ quarter, value: index + 1 }))
  };
  const rowVersion = "A".repeat(64);
  const loadRequest = manualFinancial.createLoadRequest("named-save-load", screen, "삼성전자");
  const load = manualFinancial.normalizeLoadResponse({
    requestId: "named-save-load",
    screen,
    retrievedAt: "2026-07-12T01:00:00+09:00",
    snapshot: { stockName: "삼성전자", rowVersion, record }
  }, loadRequest);
  const stockResponse = manualFinancial.normalizeStockSearchResponse({
    requestId: "named-save-stock",
    query: "삼성전자",
    retrievedAt: "2026-07-12T01:00:01+09:00",
    totalRowCount: 1,
    truncated: false,
    results: [{ market: "kospi", source: "oracle", name: "삼성전자", code: "005930" }]
  });
  const stock = manualFinancial.verifyStockForRecord(record, stockResponse, "kospi", "005930");
  const selectionRequest = manualFinancial.createSelectionRequest("named-save-selection", load.snapshot, stock);
  const selection = manualFinancial.normalizeSelectionResponse({
    requestId: "named-save-selection",
    screen,
    stockName: "삼성전자",
    rowVersion,
    builderKey: "s5080",
    code: "5080",
    aliases: ["5080"],
    selection: {
      groupCode: "KOSPI",
      subject: "삼성전자",
      graphicType: "SALES",
      subtype: "",
      dataCode: ""
    },
    currentPage: 1,
    totalPages: 1
  }, selectionRequest);
  return manualFinancial.createPlaylistEntry(load.snapshot, stock, selection, {
    id: "named-save-graph",
    fadeDuration: 6
  });
}

test("named save accepts only live fresh-read manual identities", () => {
  const netRead = manualLists.normalizeNetSellDataResponse({
    requestId: "named-save-net-read",
    audience: "FOREIGN",
    rows
  }, { requestId: "named-save-net-read", audience: "FOREIGN" });
  const net = manualLists.createNetSellPlaylistEntry("FOREIGN", {
    id: "named-save-net",
    fadeDuration: 6
  }, netRead);
  assert.equal(workflow.isTrustedEntryForSave(net, 0), true);
  assert.equal(workflow.isTrustedEntryForSave(JSON.parse(JSON.stringify(net)), 0), false);

  const version = "b".repeat(64);
  const viPayload = {
    requestId: "named-save-vi-read",
    itemCount: 2,
    pageCount: 1,
    items: [{ code: "P005930", name: "삼성전자" }, { code: "P000660", name: "SK하이닉스" }],
    version
  };
  const viRead = manualLists.normalizeViDataResponse(viPayload, viPayload.requestId);
  const vi = manualLists.createViPlaylistEntry(viRead.items, viRead.version, {
    id: "named-save-vi",
    fadeDuration: 6
  }, viRead);
  assert.equal(workflow.isTrustedEntryForSave(vi, 1), true);
  vi.pageCount = 2;
  assert.equal(workflow.isTrustedEntryForSave(vi, 1), false);

  const graph = createFreshGraphEntry();
  assert.equal(workflow.isTrustedEntryForSave(graph, 2), true);
  assert.equal(workflow.isTrustedEntryForSave(JSON.parse(JSON.stringify(graph)), 2), false);

  vi.pageCount = 1;
  const replace = namedPlaylists.createReplaceRequest(
    "named-save-replace",
    "12345678",
    [net, vi, graph],
    {
      isTrustedEntry: workflow.isTrustedEntryForSave,
      pageTextForEntry: entry => entry.operator?.kind === "vi" ? `1/${entry.pageCount}` : "1/1",
      fieldsForEntry: workflow.legacyFieldsForEntry
    });
  assert.deepEqual(replace.items.map(item => [
    item.groupCode,
    item.subject,
    item.graphicType,
    item.subtype,
    item.pageText,
    item.dataCode
  ]), [
    ["외국인 순매도 상위(수동)", "지수", "순매도 상위", "순매도 상위", "1/1", ""],
    ["", "삼성전자,SK하이닉스", "5단 표그래프", "VI 발동(수동)", "1/1", ""],
    ["코스피", "삼성전자", "매출액", "", "1/1", ""]
  ]);
});

test("stored local FSell and VI entries require the same fresh-read materialization", () => {
  const net = manualLists.createNetSellPlaylistEntry("INSTITUTION", {
    id: "stored-local-net",
    fadeDuration: 6,
    enabled: false
  });
  const pendingNet = workflow.createStoredManualListPending(
    JSON.parse(JSON.stringify(net)),
    0);
  assert.equal(workflow.isStoredManualListPending(pendingNet), true);
  const netRequest = workflow.createManualListReadRequest("stored-net-read", pendingNet);
  const restoredNet = workflow.materializeManualList(pendingNet, {
    requestId: "stored-net-read",
    audience: "INSTITUTION",
    rows
  }, netRequest);
  assert.ok(restoredNet);
  assert.equal(restoredNet.enabled, false);
  assert.equal(manualLists.isPlaylistEntryPlayable(restoredNet), true);

  const version = "c".repeat(64);
  const items = [{ code: "P005930", name: "삼성전자" }];
  const vi = manualLists.createViPlaylistEntry(items, version, {
    id: "stored-local-vi",
    fadeDuration: 6
  });
  const pendingVi = workflow.createStoredManualListPending(
    JSON.parse(JSON.stringify(vi)),
    1);
  const viRequest = workflow.createManualListReadRequest("stored-vi-read", pendingVi);
  const viPayload = {
    requestId: "stored-vi-read",
    itemCount: 1,
    pageCount: 1,
    items,
    version
  };
  assert.equal(workflow.materializeManualList(pendingVi, viPayload, viRequest).builderKey, "s5074");
  assert.equal(workflow.materializeManualList(
    pendingVi,
    { ...viPayload, version: "d".repeat(64) },
    viRequest), null);
  assert.equal(workflow.createStoredManualListPending({
    ...JSON.parse(JSON.stringify(vi)),
    selection: { ...vi.selection, dataCode: "tampered" }
  }, 1), null);
});
