"use strict";

const test = require("node:test");
const assert = require("node:assert/strict");
const workflow = require("../../Web/operator-workflow.js");

const legacyLabels = [
  "1열판기본_예상체결가",
  "1열판기본_현재가",
  "1열판기본_시간외단일가",
  "1열판상세_시가",
  "1열판상세_액면가",
  "1열판상세_PBR",
  "1열판상세_거래량",
  "수익률그래프_5일",
  "수익률그래프_20일",
  "수익률그래프_60일",
  "수익률그래프_120일",
  "수익률그래프_240일",
  "캔들그래프_일봉",
  "캔들그래프_5일",
  "캔들그래프_20일",
  "캔들그래프_60일",
  "캔들그래프_120일",
  "캔들그래프_240일",
  "캔들그래프(거래량)_일봉",
  "캔들그래프(거래량)_5일",
  "캔들그래프(거래량)_20일",
  "캔들그래프(거래량)_60일",
  "캔들그래프(거래량)_120일",
  "캔들그래프(거래량)_240일",
  "캔들그래프(예상체결가)_5일",
  "캔들그래프(예상체결가)_20일",
  "캔들그래프(예상체결가)_60일",
  "캔들그래프(예상체결가)_120일",
  "캔들그래프(예상체결가)_240일",
  "호가창_표그래프",
  "거래원_표그래프"
];

const kospiStock = Object.freeze({
  market: "kospi",
  stockName: "테스트전자",
  displayName: "테스트전자",
  stockCode: "A123456",
  isNxt: false
});

test("stock catalog is pinned to the 31-row runtime 종목.ini workflow", () => {
  assert.deepEqual(workflow.runtimeStockAsset, {
    relativePath: "bin/Debug/Res/종목.ini",
    sha256: "45DFB1804828F0E4A45F73D681AF0709CE5662872FAA49CFC9F7A0B940409E20"
  });
  assert.equal(workflow.stockCuts.length, 31);
  assert.deepEqual(workflow.stockCuts.map(cut => cut.label), legacyLabels);
  assert.equal(new Set(workflow.stockCuts.map(cut => cut.id)).size, 31);
  assert.equal(workflow.stockCuts.some(cut => cut.label === "-"), false);

  for (const cut of workflow.stockCuts) {
    assert.equal(typeof cut.id, "string");
    assert.equal(typeof cut.section, "string");
    assert.equal(typeof cut.group, "string");
    assert.equal(typeof cut.detail, "string");
    assert.match(cut.builderKey, /^s\d+$/);
    assert.match(cut.alias, /^[A-Z]?\d+$/);
    assert.equal(cut.cutCode, cut.alias);
    assert.equal(cut.aliases.includes(cut.alias), true);
  }
});

test("native stock search rows normalize to a typed stock without sample fallbacks", () => {
  assert.deepEqual(
    workflow.normalizeStockResult({
      market: "kospi",
      source: "oracle",
      name: "실제조회종목",
      code: "005930"
    }),
    {
      market: "kospi",
      stockName: "실제조회종목",
      displayName: "실제조회종목",
      stockCode: "005930",
      isNxt: false
    });

  assert.deepEqual(
    workflow.normalizeStockResult({
      market: "nxt-kosdaq",
      source: "mariaDb",
      name: "NXT조회종목",
      code: "123456"
    }),
    {
      market: "kosdaq",
      stockName: "NXT조회종목",
      displayName: "NXT조회종목(NXT)",
      stockCode: "123456",
      isNxt: true
    });

  assert.deepEqual(
    workflow.normalizeStockResult({
      market: "코스피_NXT",
      stockName: "표시종목(NXT)",
      displayName: "표시종목(NXT)",
      stockCode: "A000001",
      isNxt: true
    }),
    {
      market: "kospi",
      stockName: "표시종목",
      displayName: "표시종목(NXT)",
      stockCode: "A000001",
      isNxt: true
    });

  assert.equal(workflow.normalizeStockResult(null), null);
  assert.equal(workflow.normalizeStockResult({}), null);
  assert.equal(workflow.normalizeStockResult({ market: "kospi", name: "", code: "005930" }), null);
  assert.equal(workflow.normalizeStockResult({ market: "overseas", name: "종목", code: "005930" }), null);
  assert.equal(workflow.normalizeStockResult({ market: "kospi", name: "종목", code: "../cut" }), null);
  assert.equal(workflow.normalizeStockResult({ market: "nxt-kospi", name: "종목", code: "005930", isNxt: false }), null);
});

test("NXT selection permits only 1열판기본_현재가", () => {
  const nxt = {
    market: "nxt-kospi",
    stockName: "NXT테스트",
    displayName: "NXT테스트(NXT)",
    stockCode: "123456",
    isNxt: true
  };
  for (const cut of workflow.stockCuts) {
    assert.equal(
      workflow.isStockCutAllowed(nxt, cut.id),
      cut.id === "basic-current",
      cut.label);
  }
  assert.equal(workflow.isStockCutAllowed(kospiStock, "not-a-cut"), false);
});

test("every regular stock cut maps to its closed builder, alias and LegacySceneSelection", () => {
  const expected = [
    ["basic-expected", "s5001", "5001", "KOSPI", "1열판기본", "EXPECTED_OPENING"],
    ["basic-current", "s5001", "5001", "KOSPI", "1열판기본", "CURRENT"],
    ["basic-after-hours", "s5001", "5001", "KOSPI", "1열판기본", "AFTER_HOURS_SINGLE_PRICE"],
    ["detail-open", "s5006", "5006", "KOSPI_STOCK", "1열판상세", "시가"],
    ["detail-face-value", "s5011", "5011", "KOSPI_STOCK", "1열판상세", "FaceValue"],
    ["detail-pbr", "s5011", "5011", "KOSPI_STOCK", "1열판상세", "Valuation"],
    ["detail-volume", "s5011", "5011", "KOSPI_STOCK", "1열판상세", "Volume"],
    ["yield-5d", "s5086", "5086", "KOSPI", "수익률그래프", "5일"],
    ["yield-20d", "s5086", "5086", "KOSPI", "수익률그래프", "20일"],
    ["yield-60d", "s5086", "5086", "KOSPI", "수익률그래프", "60일"],
    ["yield-120d", "s5086", "5086", "KOSPI", "수익률그래프", "120일"],
    ["yield-240d", "s5086", "5086", "KOSPI", "수익률그래프", "240일"],
    ["candle-daily", "s8010", "8035", "KOSPI_STOCK", "MA5,MA20", "PRICE"],
    ["candle-5d", "s8010", "8061", "KOSPI_STOCK", "MA5,MA20", "PRICE"],
    ["candle-20d", "s8010", "8040", "KOSPI_STOCK", "MA5,MA20", "PRICE"],
    ["candle-60d", "s8010", "8046", "KOSPI_STOCK", "MA5,MA20", "PRICE"],
    ["candle-120d", "s8010", "8051", "KOSPI_STOCK", "MA5,MA20", "PRICE"],
    ["candle-240d", "s8010", "8056", "KOSPI_STOCK", "MA5,MA20", "PRICE"],
    ["candle-volume-daily", "s8010", "8035", "KOSPI_STOCK", "MA5,MA20", "VOLUME"],
    ["candle-volume-5d", "s8010", "8061", "KOSPI_STOCK", "MA5,MA20", "VOLUME"],
    ["candle-volume-20d", "s8010", "8040", "KOSPI_STOCK", "MA5,MA20", "VOLUME"],
    ["candle-volume-60d", "s8010", "8046", "KOSPI_STOCK", "MA5,MA20", "VOLUME"],
    ["candle-volume-120d", "s8010", "8051", "KOSPI_STOCK", "MA5,MA20", "VOLUME"],
    ["candle-volume-240d", "s8010", "8056", "KOSPI_STOCK", "MA5,MA20", "VOLUME"],
    ["candle-expected-5d", "s8010", "8061", "KOSPI_STOCK", "MA5,MA20", "EXPECTED"],
    ["candle-expected-20d", "s8010", "8040", "KOSPI_STOCK", "MA5,MA20", "EXPECTED"],
    ["candle-expected-60d", "s8010", "8046", "KOSPI_STOCK", "MA5,MA20", "EXPECTED"],
    ["candle-expected-120d", "s8010", "8051", "KOSPI_STOCK", "MA5,MA20", "EXPECTED"],
    ["candle-expected-240d", "s8010", "8056", "KOSPI_STOCK", "MA5,MA20", "EXPECTED"],
    ["order-book", "s8003", "8003", "KOSPI", "ORDER_BOOK", "TABLE_GRAPH"],
    ["traders", "s5037", "5037", "KOSPI", "TRADER", "TABLE_GRAPH"]
  ];

  assert.equal(expected.length, 31);
  for (const [id, builderKey, code, groupCode, graphicType, subtype] of expected) {
    const entry = workflow.createStockPlaylistEntry(kospiStock, id, {
      id: `entry-${id}`,
      ma5: true,
      ma20: true,
      fadeDuration: 8
    });
    assert.equal(entry.builderKey, builderKey, id);
    assert.equal(entry.code, code, id);
    assert.equal(entry.selection.groupCode, groupCode, id);
    assert.equal(entry.selection.subject, "테스트전자", id);
    assert.equal(entry.selection.graphicType, graphicType, id);
    assert.equal(entry.selection.subtype, subtype, id);
    assert.equal(entry.selection.dataCode, "A123456", id);
    assert.equal(entry.category, "stock", id);
    assert.equal(entry.market, "kospi", id);
    assert.equal(entry.enabled, true, id);
    assert.equal(entry.fadeDuration, 8, id);
    assert.equal(entry.operator.cutId, id, id);
    assert.equal(entry.operator.source, "legacy-stock-workflow", id);
  }
});

test("moving-average flags are ordered and apply only to candle cuts", () => {
  const ma20 = workflow.createStockPlaylistEntry(kospiStock, "candle-20d", {
    id: "ma20",
    ma5: false,
    ma20: true
  });
  assert.equal(ma20.selection.graphicType, "MA20");
  assert.deepEqual(ma20.operator.movingAverages, { ma5: false, ma20: true });

  const none = workflow.createStockPlaylistEntry(kospiStock, "candle-5d", { id: "none" });
  assert.equal(none.selection.graphicType, "");

  const nonCandle = workflow.createStockPlaylistEntry(kospiStock, "basic-current", {
    id: "quote",
    ma5: true,
    ma20: true
  });
  assert.equal(nonCandle.selection.graphicType, "1열판기본");
  assert.deepEqual(nonCandle.operator.movingAverages, { ma5: false, ma20: false });
});

test("NXT current quote uses N5001 and the native NXT market selection", () => {
  const entry = workflow.createStockPlaylistEntry({
    market: "nxt-kosdaq",
    stockName: "NXT테스트",
    displayName: "NXT테스트",
    stockCode: "654321",
    isNxt: true
  }, "basic-current", { id: "nxt-current" });

  assert.equal(entry.builderKey, "s5001");
  assert.equal(entry.code, "N5001");
  assert.equal(entry.market, "kosdaq");
  assert.equal(entry.title, "NXT테스트(NXT)");
  assert.deepEqual(entry.selection, {
    groupCode: "NXT_KOSDAQ",
    subject: "NXT테스트(NXT)",
    dataCode: "654321",
    graphicType: "1열판기본",
    subtype: "CURRENT"
  });
  assert.throws(
    () => workflow.createStockPlaylistEntry(entry.operator.stock, "order-book", { id: "bad" }),
    /NXT stocks support only/);
});

test("entry construction fails closed and never supplies a sample stock", () => {
  assert.throws(
    () => workflow.createStockPlaylistEntry({}, "basic-current", { id: "missing-stock" }),
    /valid selected stock/);
  assert.throws(
    () => workflow.createStockPlaylistEntry(kospiStock, "missing-cut", { id: "missing-cut" }),
    /unknown/);
  assert.throws(
    () => workflow.createStockPlaylistEntry(kospiStock, "basic-current", {}),
    /entry id/);
  assert.throws(
    () => workflow.createStockPlaylistEntry(kospiStock, "basic-current", { id: "unsafe id" }),
    /entry id/);
  assert.throws(
    () => workflow.createStockPlaylistEntry(kospiStock, "basic-current", { id: "fade", fadeDuration: 61 }),
    /Fade duration/);
  assert.throws(
    () => workflow.createStockPlaylistEntry(kospiStock, "basic-current", { id: "flags", ma5: "yes" }),
    /Moving-average flags/);

  const entry = workflow.createStockPlaylistEntry(kospiStock, "basic-current", { id: "default-fade" });
  assert.equal(entry.fadeDuration, 6);
  assert.equal(entry.stockName, "테스트전자");
});

test("stored operator stock entries restore only when every trusted mapping still matches", () => {
  const stored = workflow.createStockPlaylistEntry(kospiStock, "candle-volume-60d", {
    id: "stored-candle",
    ma5: true,
    ma20: false,
    fadeDuration: 9
  });
  stored.enabled = false;

  const restored = workflow.restoreStockPlaylistEntry(JSON.parse(JSON.stringify(stored)));
  assert.ok(restored);
  assert.equal(restored.enabled, false);
  assert.equal(restored.code, "8046");
  assert.equal(restored.detail, "캔들그래프(거래량)_60일");
  assert.equal(restored.selection.graphicType, "MA5");

  for (const tamper of [
    value => { value.builderKey = "s5001"; },
    value => { value.code = "5001"; },
    value => { value.selection.subject = "다른종목"; },
    value => { value.operator.cutId = "basic-current"; },
    value => { value.operator.source = "unknown"; },
    value => { value.operator.legacyLabel = "변조"; },
    value => { value.enabled = "false"; }
  ]) {
    const candidate = JSON.parse(JSON.stringify(stored));
    tamper(candidate);
    assert.equal(workflow.restoreStockPlaylistEntry(candidate), null);
  }
  assert.equal(workflow.restoreStockPlaylistEntry(null), null);
});

test("four legacy manual-data shortcuts are explicit unavailable prerequisites", () => {
  assert.deepEqual(
    workflow.manualStockActions.map(action => [
      action.label,
      action.builderKey,
      action.alias,
      action.prerequisiteTable
    ]),
    [
      ["주요매출 구성", "s5076", "5076", "INPUT_PIE"],
      ["성장성 지표", "s5079", "5079", "INPUT_GROW"],
      ["매출액", "s5080", "5080", "INPUT_SELL"],
      ["영업이익", "s5081", "5081", "INPUT_PROFIT"]
    ]);
  for (const action of workflow.manualStockActions) {
    assert.equal(action.available, false);
    assert.equal(action.autoAdd, false);
    assert.equal(action.status, "unavailable");
    assert.equal(action.prerequisites.length, 3);
    assert.match(action.prerequisites[0], new RegExp(action.prerequisiteTable));
  }
});
