"use strict";

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

const kospi = Object.freeze({
  market: "kospi",
  stockCode: "005930",
  displayName: "삼성전자"
});
const kosdaq = Object.freeze({
  market: "kosdaq",
  stockCode: "A123456",
  displayName: "코스닥정지종목"
});

function clone(value) {
  return JSON.parse(JSON.stringify(value));
}

function validResponse(overrides = {}) {
  return {
    requestId: "halt-request-1",
    query: "",
    retrievedAt: "2026-07-11T12:34:56+09:00",
    totalRowCount: 2,
    truncated: false,
    results: [
      { market: "kospi", stockCode: "000001", displayName: "가나다" },
      { market: "kosdaq", stockCode: "000002", displayName: "라마바" }
    ],
    ...overrides
  };
}

test("workflow pins the UC7 bridge and two closed scene actions", () => {
  assert.deepEqual(workflow.bridgeContract, {
    requestType: "search-trading-halts",
    resultType: "trading-halt-results",
    errorType: "trading-halt-error",
    defaultMaximumResults: 100,
    maximumResults: 500,
    maximumQueryLength: 64
  });
  assert.deepEqual(workflow.sourceContract, {
    originalControl: "Control/UC7.cs",
    currentCutCode: "5001",
    candleCutCode: "8051",
    candlePeriodDays: 120,
    currentResolverGroups: { kospi: "KOSPI", kosdaq: "KOSDAQ" },
    candleResolverGroups: { kospi: "KOSPI_STOCK", kosdaq: "KOSDAQ_STOCK" }
  });
  assert.deepEqual(workflow.actions.map(value => [
    value.id, value.builderKey, value.code, value.kind
  ]), [
    ["current", "s5001", "5001", "current"],
    ["candle-120d", "s8010", "8051", "candle"]
  ]);
});

test("search requests match the exact native bridge payload and normalize the query", () => {
  assert.deepEqual(
    workflow.createTradingHaltSearchRequest("request-1", "  삼성  "),
    { requestId: "request-1", query: "삼성" });
  assert.deepEqual(
    workflow.createTradingHaltSearchRequest("request-2", "", 200),
    { requestId: "request-2", query: "", maximumResults: 200 });

  assert.throws(() => workflow.createTradingHaltSearchRequest("bad id", ""), /safe/i);
  assert.throws(() => workflow.createTradingHaltSearchRequest("request", "bad\nquery"), /safe/i);
  assert.throws(() => workflow.createTradingHaltSearchRequest("request", "x".repeat(65)), /64/);
  assert.throws(() => workflow.createTradingHaltSearchRequest("request", "", 0), /1 through 500/);
  assert.throws(() => workflow.createTradingHaltSearchRequest("request", "", 501), /1 through 500/);
  assert.throws(() => workflow.createTradingHaltSearchRequest("request", "", "100"), /1 through 500/);
});

test("native rows normalize only exact typed market, stock-code and display-name values", () => {
  assert.deepEqual(workflow.normalizeTradingHalt(kospi), kospi);
  assert.deepEqual(workflow.normalizeTradingHalt(kosdaq), kosdaq);
  assert.equal(workflow.normalizeTradingHalt({ ...kospi, market: "KOSPI" }), null);
  assert.equal(workflow.normalizeTradingHalt({ ...kospi, market: "nxt-kospi" }), null);
  assert.equal(workflow.normalizeTradingHalt({ ...kospi, stockCode: "../005930" }), null);
  assert.equal(workflow.normalizeTradingHalt({ ...kospi, stockCode: " 005930" }), null);
  assert.equal(workflow.normalizeTradingHalt({ ...kospi, displayName: " 삼성전자" }), null);
  assert.equal(workflow.normalizeTradingHalt({ ...kospi, displayName: "삼성\n전자" }), null);
  assert.equal(workflow.normalizeTradingHalt({ ...kospi, displayName: "삼성\u200B전자" }), null);
  assert.equal(workflow.normalizeTradingHalt({ ...kospi, code: "005930" }), null);
  assert.equal(workflow.normalizeTradingHalt({ market: "kospi", code: "005930", name: "삼성전자" }), null);
});

test("search responses are request-bound, deterministically ordered and deeply normalized", () => {
  const request = workflow.createTradingHaltSearchRequest("halt-request-1", "", 2);
  const response = workflow.normalizeTradingHaltSearchResponse(validResponse(), request);
  assert.ok(response);
  assert.deepEqual(response, validResponse());
  assert.equal(Object.isFrozen(response), true);
  assert.equal(Object.isFrozen(response.results), true);
  assert.equal(Object.isFrozen(response.results[0]), true);

  assert.equal(workflow.normalizeTradingHaltSearchResponse(
    validResponse({ requestId: "other-request" }), request), null);
  assert.equal(workflow.normalizeTradingHaltSearchResponse(
    validResponse({ query: "삼성" }), request), null);
  assert.equal(workflow.normalizeTradingHaltSearchResponse(
    validResponse({ totalRowCount: 1 }), request), null);
  assert.equal(workflow.normalizeTradingHaltSearchResponse(
    validResponse({ retrievedAt: "not-a-date" }), request), null);
  assert.equal(workflow.normalizeTradingHaltSearchResponse(
    { ...validResponse(), unexpected: true }, request), null);
});

test("search responses reject duplicates, ambiguous names, invalid rows and reordered markets", () => {
  const duplicateIdentity = validResponse({
    results: [
      { market: "kospi", stockCode: "000001", displayName: "가나다" },
      { market: "kospi", stockCode: "000001", displayName: "라마바" }
    ]
  });
  assert.equal(workflow.normalizeTradingHaltSearchResponse(duplicateIdentity), null);

  const ambiguousName = validResponse({
    results: [
      { market: "kospi", stockCode: "000001", displayName: "가나다" },
      { market: "kospi", stockCode: "000002", displayName: "가나다" }
    ]
  });
  assert.equal(workflow.normalizeTradingHaltSearchResponse(ambiguousName), null);

  const crossMarketSameName = validResponse({
    results: [
      { market: "kospi", stockCode: "000001", displayName: "가나다" },
      { market: "kosdaq", stockCode: "000001", displayName: "가나다" }
    ]
  });
  assert.ok(workflow.normalizeTradingHaltSearchResponse(crossMarketSameName));

  const reordered = validResponse({ results: [...validResponse().results].reverse() });
  assert.equal(workflow.normalizeTradingHaltSearchResponse(reordered), null);
  assert.equal(workflow.normalizeTradingHaltSearchResponse(validResponse({
    results: [validResponse().results[0], { ...validResponse().results[1], market: "nxt-kosdaq" }]
  })), null);
});

test("UC7 name-sort toggles descending first and ascending second", () => {
  assert.equal(workflow.nextTradingHaltNameSort(null), "descending");
  assert.equal(workflow.nextTradingHaltNameSort("descending"), "ascending");
  assert.equal(workflow.nextTradingHaltNameSort("ascending"), "descending");
});

test("display-only name sort handles mixed markets and deterministic duplicate-name ties", () => {
  const rows = [
    { market: "kospi", stockCode: "000002", displayName: "동명" },
    { market: "kosdaq", stockCode: "000003", displayName: "동명" },
    { market: "kospi", stockCode: "000004", displayName: "마지막" },
    { market: "kosdaq", stockCode: "000001", displayName: "동명" },
    { market: "kospi", stockCode: "000005", displayName: "가나다" }
  ];
  const original = [...rows];

  assert.deepEqual(
    workflow.sortTradingHaltsForDisplay(rows, "ascending").map(value => `${value.displayName}|${value.market}|${value.stockCode}`),
    [
      "가나다|kospi|000005",
      "동명|kosdaq|000001",
      "동명|kosdaq|000003",
      "동명|kospi|000002",
      "마지막|kospi|000004"
    ]);
  assert.deepEqual(
    workflow.sortTradingHaltsForDisplay(rows, "descending").map(value => `${value.displayName}|${value.market}|${value.stockCode}`),
    [
      "마지막|kospi|000004",
      "동명|kosdaq|000001",
      "동명|kosdaq|000003",
      "동명|kospi|000002",
      "가나다|kospi|000005"
    ]);
  const selectedIdentity = workflow.tradingHaltIdentity(rows[0]);
  const selectedAfterSort = workflow.sortTradingHaltsForDisplay(rows, "ascending")
    .find(value => workflow.tradingHaltIdentity(value) === selectedIdentity);
  assert.equal(selectedAfterSort, rows[0]);
  const serverOrder = workflow.sortTradingHaltsForDisplay(rows, null);
  assert.deepEqual(rows, original);
  assert.deepEqual(serverOrder, rows);
  assert.notEqual(serverOrder, rows);
  assert.equal(serverOrder[0], rows[0]);
  assert.throws(() => workflow.sortTradingHaltsForDisplay(rows, "server"), /ascending/i);
});

test("trading-halt selection identity is market plus stock code, independent of display name", () => {
  assert.equal(
    workflow.tradingHaltIdentity({ market: "kospi", stockCode: "005930", displayName: "삼성전자" }),
    "kospi\u0000005930");
  assert.equal(
    workflow.tradingHaltIdentity({ market: "kospi", stockCode: "005930", displayName: "표시명 변경" }),
    "kospi\u0000005930");
  assert.notEqual(
    workflow.tradingHaltIdentity({ market: "kosdaq", stockCode: "005930", displayName: "삼성전자" }),
    "kospi\u0000005930");
  assert.equal(workflow.tradingHaltIdentity(null), "");
});

test("5001 current selections bind the exact halted-stock Core contract", () => {
  for (const [stock, groupCode] of [[kospi, "KOSPI"], [kosdaq, "KOSDAQ"]]) {
    const mapping = workflow.buildTradingHaltSelection("current", stock, {
      ma5: true,
      ma20: true
    });
    assert.equal(mapping.builderKey, "s5001");
    assert.equal(mapping.code, "5001");
    assert.deepEqual(mapping.aliases, ["5001"]);
    assert.deepEqual(mapping.selection, {
      groupCode,
      subject: stock.displayName,
      graphicType: "1열판기본",
      subtype: "HALTED",
      dataCode: stock.stockCode
    });
    assert.deepEqual(mapping.movingAverages, { ma5: false, ma20: false });
  }
});

test("8051 selections expose only the four closed moving-average flag combinations", () => {
  const cases = [
    [false, false, ""],
    [true, false, "MA5"],
    [false, true, "MA20"],
    [true, true, "MA5,MA20"]
  ];
  for (const [ma5, ma20, graphicType] of cases) {
    const mapping = workflow.buildTradingHaltSelection("candle-120d", kosdaq, { ma5, ma20 });
    assert.equal(mapping.builderKey, "s8010");
    assert.equal(mapping.code, "8051");
    assert.deepEqual(mapping.aliases, ["8051"]);
    assert.deepEqual(mapping.selection, {
      groupCode: "KOSDAQ_STOCK",
      subject: "코스닥정지종목",
      graphicType,
      subtype: "TRADING_HALT",
      dataCode: "A123456"
    });
  }
  assert.throws(() => workflow.buildTradingHaltSelection("candle-120d", kospi, {
    ma5: "true", ma20: false
  }), /boolean/i);
});

test("playlist entries preserve the typed identity and reject unsafe options", () => {
  const entry = workflow.createTradingHaltPlaylistEntry(kospi, "candle-120d", {
    id: "halt-candle",
    fadeDuration: 8,
    ma5: true,
    ma20: false
  });
  assert.equal(entry.builderKey, "s8010");
  assert.equal(entry.code, "8051");
  assert.equal(entry.category, "chart");
  assert.equal(entry.market, "kospi");
  assert.equal(entry.stockCode, "005930");
  assert.equal(entry.displayName, "삼성전자");
  assert.equal(entry.selection.graphicType, "MA5");
  assert.equal(entry.operator.source, "legacy-trading-halt-workflow");
  assert.equal(entry.operator.schemaVersion, 1);
  assert.deepEqual(entry.operator.tradingHalt, kospi);

  assert.throws(() => workflow.createTradingHaltPlaylistEntry(kospi, "unknown", {
    id: "unknown"
  }), /unknown/i);
  assert.throws(() => workflow.createTradingHaltPlaylistEntry({}, "current", {
    id: "invalid"
  }), /valid/i);
  assert.throws(() => workflow.createTradingHaltPlaylistEntry(kospi, "current", {
    id: "bad id"
  }), /safe/i);
  assert.throws(() => workflow.createTradingHaltPlaylistEntry(kospi, "current", {
    id: "fade", fadeDuration: 61
  }), /0 through 60/i);
  assert.throws(() => workflow.createTradingHaltPlaylistEntry(kospi, "current", {
    id: "flags", ma20: 1
  }), /boolean/i);
});

test("trusted restore reconstructs entries and rejects mapping, identity and flag tampering", () => {
  const original = workflow.createTradingHaltPlaylistEntry(kosdaq, "candle-120d", {
    id: "stored-halt",
    fadeDuration: 9,
    ma5: true,
    ma20: true
  });
  original.enabled = false;
  const restored = workflow.restoreTradingHaltPlaylistEntry(clone(original));
  assert.ok(restored);
  assert.equal(restored.enabled, false);
  assert.equal(restored.selection.graphicType, "MA5,MA20");

  const tampers = [
    value => { value.builderKey = "s5001"; },
    value => { value.code = "5001"; },
    value => { value.selection.groupCode = "KOSPI_STOCK"; },
    value => { value.selection.subject = "다른종목"; },
    value => { value.selection.dataCode = "000001"; },
    value => { value.selection.graphicType = "MA20,MA5"; },
    value => { value.selection.subtype = "PRICE"; },
    value => { value.operator.actionId = "current"; },
    value => { value.operator.schemaVersion = 2; },
    value => { value.operator.resolverGroup = "KOSPI_STOCK"; },
    value => { value.operator.movingAverages.ma5 = false; },
    value => { value.operator.tradingHalt.stockCode = "000001"; },
    value => { value.enabled = "false"; }
  ];
  for (const tamper of tampers) {
    const candidate = clone(original);
    tamper(candidate);
    assert.equal(workflow.restoreTradingHaltPlaylistEntry(candidate), null);
  }
  assert.equal(workflow.restoreTradingHaltPlaylistEntry(null), null);
});

test("native error payloads are exact and bound to the active request", () => {
  const request = workflow.createTradingHaltSearchRequest("halt-request-1", "삼성", 100);
  const error = {
    requestId: "halt-request-1",
    query: "삼성",
    message: "거래정지 종목을 검색하지 못했습니다."
  };
  assert.deepEqual(workflow.normalizeTradingHaltError(error, request), error);
  assert.equal(workflow.normalizeTradingHaltError({ ...error, requestId: "old" }, request), null);
  assert.equal(workflow.normalizeTradingHaltError({ ...error, message: "bad\nmessage" }, request), null);
  assert.equal(workflow.normalizeTradingHaltError({ ...error, detail: "extra" }, request), null);
});
