"use strict";

const test = require("node:test");
const assert = require("node:assert/strict");
const legacyRows = require("../../Web/legacy-named-row-workflow.js");
const namedPlaylists = require("../../Web/named-playlist-workflow.js");
const stocks = require("../../Web/operator-workflow.js");
const fixed = require("../../Web/legacy-fixed-workflow.js");
const themes = require("../../Web/theme-workflow.js");
const experts = require("../../Web/expert-workflow.js");
const comparisons = require("../../Web/comparison-workflow.js");
const industries = require("../../Web/industry-workflow.js");

function rawRow(overrides = {}) {
  return {
    itemIndex: 0,
    enabled: true,
    groupCode: "코스피",
    subject: "삼성전자",
    graphicType: "1열판기본",
    subtype: "현재가",
    pageText: "1/1",
    dataCode: "005930",
    ...overrides
  };
}

function legacySelection(value) {
  const { groupCode, subject, graphicType, subtype, dataCode } = value;
  return { groupCode, subject, graphicType, subtype, dataCode };
}

test("the original MainForm Korean stock row maps to exactly one canonical stock cut", () => {
  const entry = stocks.createStockPlaylistEntry({
    market: "kospi",
    stockName: "삼성전자",
    displayName: "삼성전자",
    stockCode: "005930",
    isNxt: false
  }, "basic-current", { id: "stock-1", fadeDuration: 6 });
  const raw = rawRow();

  assert.equal(legacyRows.matchesCanonicalEntry(entry, raw), true);
  assert.deepEqual(legacyRows.legacySelectionForEntry(entry), legacySelection(raw));
  assert.equal(legacyRows.selectUniqueCanonicalEntry([entry], raw), entry);
  for (const [field, value] of [
    ["groupCode", "코스닥"],
    ["subject", "삼성전자우"],
    ["graphicType", "1열판상세"],
    ["subtype", "예상체결가"],
    ["dataCode", "000660"]
  ]) {
    assert.equal(legacyRows.matchesCanonicalEntry(entry, { ...raw, [field]: value }), false, field);
  }
});

test("all 322 closed fixed actions reproduce their original UC1 Korean row signature", () => {
  const available = fixed.actions.filter(action => action.available);
  assert.equal(available.length, 322);
  const signatures = new Set();

  for (const action of available) {
    const signature = legacyRows.fixedLegacySignature(action);
    assert.ok(signature, action.id);
    assert.equal(legacyRows.matchesFixedActionRaw(signature, action), true, action.id);
    const entry = fixed.createFixedPlaylistEntry(action.id, {
      id: `entry-${action.id}`,
      fadeDuration: 6,
      now: new Date(2026, 6, 12, 9, 0, 0)
    });
    assert.equal(legacyRows.matchesCanonicalEntry(entry, signature), true, action.id);
    assert.deepEqual(legacyRows.legacySelectionForEntry(entry), signature, action.id);
    signatures.add(JSON.stringify(signature));
  }

  assert.equal(signatures.size, 322, "legacy fixed rows must remain unambiguous");
  const dow = fixed.actions.find(action => action.id === "fixed-001");
  assert.deepEqual(legacyRows.fixedLegacySignature(dow), {
    groupCode: "해외지수",
    subject: "다우",
    graphicType: "1열판기본",
    subtype: "",
    dataCode: ""
  });
  assert.equal(legacyRows.matchesFixedActionRaw({
    ...legacyRows.fixedLegacySignature(dow), subtype: "현재가"
  }, dow), false);
});

test("the original UC4 theme rows map only through the closed action and sort grammar", () => {
  const raw = rawRow({
    groupCode: "테마",
    subject: "반도체",
    graphicType: "5단 표그래프",
    subtype: "테마-현재가(상승률순)",
    pageText: "1/4",
    dataCode: "0123"
  });
  const descriptor = legacyRows.themeLegacyDescriptor(raw);
  assert.deepEqual(descriptor, {
    actionId: "five-row-current",
    sort: "GAIN_DESC",
    theme: {
      market: "krx",
      themeCode: "0123",
      title: "반도체",
      session: null,
      itemCount: 1
    }
  });
  const entry = themes.createThemePlaylistEntry(
    descriptor.actionId,
    descriptor.theme,
    descriptor.sort,
    { id: "theme-1", fadeDuration: 6 });
  assert.equal(legacyRows.matchesCanonicalEntry(entry, raw), true);
  assert.deepEqual(legacyRows.legacySelectionForEntry(entry), legacySelection(raw));
  const originalFallback = { ...raw, subtype: "테마-현재가" };
  const fallbackDescriptor = legacyRows.themeLegacyDescriptor(originalFallback);
  assert.equal(fallbackDescriptor.sort, "GAIN_ASC");
  const fallbackEntry = themes.createThemePlaylistEntry(
    fallbackDescriptor.actionId,
    fallbackDescriptor.theme,
    fallbackDescriptor.sort,
    { id: "theme-fallback", fadeDuration: 6 });
  assert.equal(legacyRows.matchesCanonicalEntry(fallbackEntry, originalFallback), true);
  assert.equal(
    legacyRows.legacySelectionForEntry(fallbackEntry).subtype,
    "테마-현재가(하락률순)",
    "new saves make the original implicit fallback explicit");
  assert.equal(legacyRows.themeLegacyDescriptor({
    ...raw, subtype: "테마-현재가(상승률)"
  }), null);
  const nxtRaw = {
    ...raw,
    groupCode: "테마_NXT",
    subject: "반도체(NXT)",
    subtype: "테마-현재가(입력순)"
  };
  const morning = legacyRows.themeLegacyDescriptor(nxtRaw, {
    now: new Date(2026, 6, 12, 12, 59, 59)
  });
  const afternoon = legacyRows.themeLegacyDescriptor(nxtRaw, {
    now: new Date(2026, 6, 12, 13, 0, 0)
  });
  assert.equal(morning.theme.session, "PRE_MARKET");
  assert.equal(afternoon.theme.session, "AFTER_MARKET");
  const nxtEntry = themes.createThemePlaylistEntry(
    morning.actionId,
    morning.theme,
    morning.sort,
    { id: "theme-nxt-legacy", fadeDuration: 6 });
  assert.equal(legacyRows.matchesCanonicalEntry(nxtEntry, nxtRaw, {
    now: new Date(2026, 6, 12, 12, 30, 0)
  }), true);
  assert.equal(legacyRows.matchesCanonicalEntry(nxtEntry, nxtRaw, {
    now: new Date(2026, 6, 12, 13, 0, 0)
  }), false);
  assert.equal(legacyRows.legacySelectionForEntry(nxtEntry), null,
    "new named saves remain blocked because the old row does not persist a session");
  assert.equal(legacyRows.matchesCanonicalEntry(entry, { ...raw, dataCode: "87654321" }), false);
});

test("the original UC6 expert row maps to the canonical paged expert entry", () => {
  const raw = rawRow({
    groupCode: "전문가 추천",
    subject: "김전문",
    graphicType: "5단 표그래프",
    subtype: "현재가",
    pageText: "1/2",
    dataCode: "0123"
  });
  const descriptor = legacyRows.expertLegacyDescriptor(raw);
  assert.deepEqual(descriptor, {
    expertCode: "0123",
    expertName: "김전문",
    source: "oracle",
    itemCount: 1,
    previewTruncated: false
  });
  const entry = experts.createExpertPlaylistEntry(descriptor, {
    id: "expert-1",
    fadeDuration: 6
  });
  assert.equal(legacyRows.matchesCanonicalEntry(entry, raw), true);
  assert.deepEqual(legacyRows.legacySelectionForEntry(entry), legacySelection(raw));
  assert.equal(legacyRows.expertLegacyDescriptor({ ...raw, dataCode: "123" }), null);
  assert.equal(legacyRows.matchesCanonicalEntry(entry, { ...raw, subject: "다른전문가" }), false);
});

test("comparison rows project only identities that the original seven fields preserve", () => {
  const fixedEntry = comparisons.createComparisonPlaylistEntry("two-column-current", [
    comparisons.getMarketTarget("Kospi"),
    comparisons.getMarketTarget("Kosdaq")
  ], { id: "comparison-fixed", fadeDuration: 6 });
  const fixedRaw = rawRow({
    groupCode: "지수",
    subject: "코스피 지수,코스닥 지수",
    graphicType: "2열판",
    subtype: "",
    dataCode: ""
  });
  assert.deepEqual(legacyRows.comparisonLegacySignature(fixedEntry.operator), legacySelection(fixedRaw));
  assert.equal(legacyRows.matchesCanonicalEntry(fixedEntry, fixedRaw), true);
  assert.deepEqual(legacyRows.legacySelectionForEntry(fixedEntry), legacySelection(fixedRaw));
  const exchangeOnly = comparisons.createComparisonPlaylistEntry("two-column-current", [
    comparisons.getMarketTarget("WonDollar"),
    comparisons.getMarketTarget("WonYen")
  ], { id: "comparison-exchange-lossy", fadeDuration: 6 });
  assert.equal(legacyRows.legacySelectionForEntry(exchangeOnly), null);

  const pair = [
    { kind: "krx-stock", market: "kospi", stockName: "삼성전자", stockCode: "005930" },
    { kind: "krx-stock", market: "kosdaq", stockName: "에이비엘바이오", stockCode: "298380" }
  ];
  const candle = comparisons.createComparisonPlaylistEntry(
    "comparison-candle", pair, { id: "comparison-candle", fadeDuration: 6 });
  const candleRaw = rawRow({
    groupCode: "코스피,코스닥",
    subject: "삼성전자,에이비엘바이오",
    graphicType: "종목별 비교분석_캔들 그래프",
    subtype: "",
    dataCode: ""
  });
  assert.equal(legacyRows.matchesCanonicalEntry(candle, candleRaw), true);
  assert.deepEqual(legacyRows.legacySelectionForEntry(candle), legacySelection(candleRaw));

  const oneMonth = comparisons.createComparisonPlaylistEntry(
    "return-1m", pair, { id: "comparison-return", fadeDuration: 6 });
  assert.deepEqual(legacyRows.legacySelectionForEntry(oneMonth), {
    groupCode: "코스피,코스닥",
    subject: "삼성전자,에이비엘바이오",
    graphicType: "종목별 수익률 비교",
    subtype: "1개월",
    dataCode: ""
  });

  const lossyStockPlate = comparisons.createComparisonPlaylistEntry(
    "two-column-current", pair, { id: "comparison-lossy", fadeDuration: 6 });
  assert.equal(legacyRows.legacySelectionForEntry(lossyStockPlate), null);
  assert.equal(legacyRows.matchesCanonicalEntry(lossyStockPlate, {
    ...fixedRaw,
    groupCode: "종목",
    subject: "삼성전자,에이비엘바이오"
  }), false);
});

test("identity-free industry market rows map only to the three closed fixed actions", () => {
  const now = new Date(2026, 6, 12, 9, 0, 0);
  const cases = [
    ["kospi", "five-row", "업종_코스피", "코스피", "5단 표그래프"],
    ["kospi", "square", "업종_코스피", "코스피", "네모그래프"],
    ["kospi", "sector", "업종_코스피", "코스피", "섹터지수"],
    ["kosdaq", "five-row", "업종_코스닥", "코스닥", "5단 표그래프"],
    ["kosdaq", "square", "업종_코스닥", "코스닥", "네모그래프"],
    ["kosdaq", "sector", "업종_코스닥", "코스닥", "섹터지수"]
  ];
  for (let index = 0; index < cases.length; index += 1) {
    const [market, cutId, groupCode, subject, graphicType] = cases[index];
    const raw = rawRow({ groupCode, subject, graphicType, subtype: "", dataCode: "" });
    const descriptor = legacyRows.industryFixedDescriptor(raw);
    assert.deepEqual(descriptor, { market, cutId });
    const entry = industries.createIndustryPlaylistEntry(market, cutId, {
      id: `industry-fixed-${index}`,
      fadeDuration: 6,
      now
    });
    assert.deepEqual(legacyRows.industryFixedLegacySignature(entry.operator), legacySelection(raw));
    assert.equal(legacyRows.matchesCanonicalEntry(entry, raw), true);
    assert.deepEqual(legacyRows.legacySelectionForEntry(entry), legacySelection(raw));
    if (cutId === "five-row") {
      assert.equal(entry.selection.dataCode, "2026-07-12");
      assert.equal(entry.builderKey, "s5074");
    }
  }

  const selected = { market: "kospi", name: "전기전자", code: "013" };
  const individual = industries.createIndustryPlaylistEntry("kospi", "one-column", {
    id: "industry-identity-required",
    selected
  });
  assert.equal(legacyRows.industryFixedLegacySignature(individual.operator), null);
  assert.equal(legacyRows.industryFixedDescriptor({
    ...rawRow(), groupCode: "업종_코스피", subject: "전기전자",
    graphicType: "1열판", subtype: "", dataCode: ""
  }), null);
});

test("named restoration accepts a closed legacy mapping but rejects it by default or after tamper", () => {
  const raw = rawRow();
  const load = namedPlaylists.normalizeLoadResponse({
    requestId: "load-legacy-1",
    definition: { programCode: "00000001", title: "Legacy" },
    retrievedAt: "2026-07-12T09:00:00+09:00",
    totalRowCount: 1,
    canMutate: true,
    writeQuarantined: false,
    rows: [raw]
  });
  const entry = stocks.createStockPlaylistEntry({
    market: "kospi",
    stockName: "삼성전자",
    displayName: "삼성전자",
    stockCode: "005930",
    isNxt: false
  }, "basic-current", { id: "stock-restore-1", fadeDuration: 6 });

  assert.equal(namedPlaylists.restoreRawRows(load, () => entry).rows[0].trusted, false);
  assert.equal(namedPlaylists.restoreRawRows(
    load, () => entry, legacyRows.matchesCanonicalEntry).rows[0].trusted, true);

  const tamperedLoad = namedPlaylists.normalizeLoadResponse({
    ...{
      requestId: "load-legacy-2",
      definition: { programCode: "00000001", title: "Legacy" },
      retrievedAt: "2026-07-12T09:00:00+09:00",
      totalRowCount: 1,
      canMutate: true,
      writeQuarantined: false
    },
    rows: [{ ...raw, dataCode: "000660" }]
  });
  assert.equal(namedPlaylists.restoreRawRows(
    tamperedLoad, () => entry, legacyRows.matchesCanonicalEntry).rows[0].trusted, false);
  assert.throws(() => namedPlaylists.restoreRawRows(load, () => entry, true), /resolver/i);
});

test("canonical rows that identify multiple fixed actions remain blocked as ambiguous", () => {
  const first = fixed.createFixedPlaylistEntry("fixed-136", {
    id: "candle-1",
    fadeDuration: 6,
    now: new Date(2026, 6, 12, 9, 0, 0)
  });
  const second = fixed.createFixedPlaylistEntry("fixed-137", {
    id: "candle-2",
    fadeDuration: 6,
    now: new Date(2026, 6, 12, 9, 0, 0)
  });
  assert.deepEqual(first.selection, second.selection);
  const raw = legacySelection(first.selection);
  assert.equal(legacyRows.matchesCanonicalEntry(first, raw), true);
  assert.equal(legacyRows.matchesCanonicalEntry(second, raw), true);
  assert.equal(legacyRows.selectUniqueCanonicalEntry([first, second], raw), null);
});

test("save projection is closed and refuses unknown or lossy NXT theme entries", () => {
  assert.equal(legacyRows.legacySelectionForEntry({
    selection: {
      groupCode: "FREE", subject: "text", graphicType: "type", subtype: "sub", dataCode: "code"
    },
    operator: { source: "unknown" }
  }), null);

  const fixedEntry = fixed.createFixedPlaylistEntry("fixed-001", {
    id: "tampered-fixed-save",
    fadeDuration: 6,
    now: new Date(2026, 6, 12, 9, 0, 0)
  });
  assert.equal(legacyRows.legacySelectionForEntry({
    ...fixedEntry,
    operator: { ...fixedEntry.operator, actionId: "fixed-002" }
  }), null);

  const nxt = themes.createThemePlaylistEntry("five-row-current", {
    market: "nxt",
    themeCode: "0123",
    title: "NXT 테마",
    session: "PRE_MARKET",
    itemCount: 5
  }, "INPUT_ORDER", { id: "nxt-theme-save", fadeDuration: 6 });
  assert.equal(legacyRows.legacySelectionForEntry(nxt), null);
  assert.equal(legacyRows.legacySelectionForEntry({
    ...nxt,
    selection: { ...nxt.selection, subject: "bad^value" }
  }), null);
});

test("named replace serializes stock, fixed, theme and expert as original Korean rows", () => {
  const entries = [
    stocks.createStockPlaylistEntry({
      market: "kospi",
      stockName: "삼성전자",
      displayName: "삼성전자",
      stockCode: "005930",
      isNxt: false
    }, "basic-current", { id: "save-stock", fadeDuration: 6 }),
    fixed.createFixedPlaylistEntry("fixed-001", {
      id: "save-fixed",
      fadeDuration: 6,
      now: new Date(2026, 6, 12, 9, 0, 0)
    }),
    themes.createThemePlaylistEntry("five-row-current", {
      market: "krx",
      themeCode: "0123",
      title: "반도체",
      session: null,
      itemCount: 5
    }, "GAIN_DESC", { id: "save-theme", fadeDuration: 6 }),
    experts.createExpertPlaylistEntry({
      expertCode: "0123",
      expertName: "김전문",
      source: "oracle",
      itemCount: 5,
      previewTruncated: false
    }, { id: "save-expert", fadeDuration: 6 })
  ];
  const request = namedPlaylists.createReplaceRequest(
    "replace-legacy-projection",
    "00000001",
    entries,
    {
      isTrustedEntry: entry => legacyRows.legacySelectionForEntry(entry) !== null,
      fieldsForEntry(entry, _index, pageText) {
        const selection = legacyRows.legacySelectionForEntry(entry);
        return [
          entry.enabled ? "1" : "0",
          selection.groupCode,
          selection.subject,
          selection.graphicType,
          selection.subtype,
          pageText,
          selection.dataCode
        ];
      }
    });

  assert.deepEqual(request.items, [
    {
      itemIndex: 0, enabled: true, groupCode: "코스피", subject: "삼성전자",
      graphicType: "1열판기본", subtype: "현재가", pageText: "1/1", dataCode: "005930"
    },
    {
      itemIndex: 1, enabled: true, groupCode: "해외지수", subject: "다우",
      graphicType: "1열판기본", subtype: "", pageText: "1/1", dataCode: ""
    },
    {
      itemIndex: 2, enabled: true, groupCode: "테마", subject: "반도체",
      graphicType: "5단 표그래프", subtype: "테마-현재가(상승률순)",
      pageText: "1/1", dataCode: "0123"
    },
    {
      itemIndex: 3, enabled: true, groupCode: "전문가 추천", subject: "김전문",
      graphicType: "5단 표그래프", subtype: "현재가", pageText: "1/1", dataCode: "0123"
    }
  ]);
});
