"use strict";

const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const workflow = require("../../Web/named-krx-theme-restore-workflow.js");

function raw(overrides = {}) {
  return {
    itemIndex: 4,
    enabled: true,
    groupCode: "테마",
    subject: "반도체",
    graphicType: "5단 표그래프",
    subtype: "테마-현재가(입력순)",
    pageText: "1/9",
    dataCode: "0123",
    ...overrides
  };
}

function resolved(overrides = {}) {
  return {
    itemIndex: 4,
    status: "resolved",
    actionId: "five-row-current",
    builderKey: "s5074",
    cutCode: "5074",
    pageSize: 5,
    sort: "INPUT_ORDER",
    themeTitle: "반도체",
    storedThemeCode: "0123",
    currentThemeCode: "87654321",
    previewItemCount: 7,
    previewIsTruncated: false,
    codeWasRemapped: true,
    ...overrides
  };
}

function response(row) {
  return {
    requestId: "load-1",
    retrievedAt: "2026-07-12T12:59:00+09:00",
    totalRowCount: 1,
    rows: [row]
  };
}

function normalize(sourceRaw = raw(), outcome = resolved()) {
  const pending = workflow.classify(sourceRaw, { id: "db-4", fadeDuration: 6 });
  const normalized = workflow.normalizeResponse(response(outcome), "load-1", [pending]);
  return { pending, normalized, outcome: normalized?.rows[0] ?? null };
}

test("classify routes every exact KRX theme group row for native fail-closed verification", () => {
  const pending = workflow.classify(raw({
    subject: "",
    graphicType: "unsupported",
    subtype: "",
    pageText: "",
    dataCode: ""
  }), { id: "db-4", fadeDuration: 6 });
  assert.equal(workflow.isPending(pending), true);
  assert.equal(pending.rawSelection.groupCode, "테마");
  assert.equal(workflow.classify(raw({ groupCode: "테마_NXT" }), {
    id: "db-4",
    fadeDuration: 6
  }), null);
});

test("normalizeResponse accepts one exact correlated current identity", () => {
  const { normalized } = normalize();
  assert.equal(normalized.rows[0].currentThemeCode, "87654321");
  assert.equal(normalized.rows[0].previewItemCount, 7);
  assert.deepEqual(workflow.bridgeContract, {
    resultType: "named-krx-theme-restore-results",
    errorType: "named-krx-theme-restore-error"
  });
});

test("normalizeResponse enforces exact wire schema, raw action correlation and preview bounds", () => {
  const pending = workflow.classify(raw(), { id: "db-4", fadeDuration: 6 });
  for (const invalid of [
    { ...resolved(), extra: true },
    resolved({ storedThemeCode: "999" }),
    resolved({ themeTitle: "다른제목" }),
    resolved({ actionId: "six-row-current", builderKey: "s5077", cutCode: "5077", pageSize: 6 }),
    resolved({ sort: "GAIN_ASC" }),
    resolved({ previewItemCount: 0 }),
    resolved({ previewItemCount: 239, previewIsTruncated: true }),
    resolved({ previewItemCount: 241, previewIsTruncated: true }),
    resolved({ codeWasRemapped: false })
  ]) {
    assert.equal(workflow.normalizeResponse(response(invalid), "load-1", [pending]), null);
  }

  assert.equal(workflow.normalizeResponse({
    requestId: "load-1",
    retrievedAt: "2026-07-12T12:59:00+09:00",
    totalRowCount: 2,
    rows: [resolved({ itemIndex: 5 }), resolved({ itemIndex: 4 })]
  }, "load-1", [
    workflow.classify(raw({ itemIndex: 4 }), { id: "db-4", fadeDuration: 6 }),
    workflow.classify(raw({ itemIndex: 5 }), { id: "db-5", fadeDuration: 6 })
  ]), null);
});

test("failed rows preserve a closed no-retry reason", () => {
  const pending = workflow.classify(raw(), { id: "db-4", fadeDuration: 6 });
  for (const failure of [
    "INVALID_ROW", "UNSUPPORTED_ACTION", "IDENTITY_NOT_FOUND", "IDENTITY_AMBIGUOUS",
    "PREVIEW_EMPTY", "DATABASE_DATA_INVALID", "RESTORE_FAILED"
  ]) {
    const result = workflow.normalizeResponse(response({
      itemIndex: 4,
      status: "failed",
      failure
    }), "load-1", [pending]);
    assert.equal(result.rows[0].failure, failure);
  }
  assert.equal(workflow.normalizeError({
    requestId: "load-1",
    code: "DATABASE_UNAVAILABLE",
    message: "조회 실패",
    retryable: false
  }, "load-1").retryable, false);
  assert.equal(workflow.normalizeError({
    requestId: "load-1",
    code: "DATABASE_UNAVAILABLE",
    message: "조회 실패",
    retryable: true
  }, "load-1"), null);
});

test("materialize uses only current code and retains the old raw identity for lossless save", () => {
  const sourceRaw = raw({ enabled: false });
  const { pending, outcome } = normalize(sourceRaw);
  const entry = workflow.materialize(pending, outcome);

  assert.equal(workflow.isMaterializedEntry(entry), true);
  assert.equal(entry.enabled, false);
  assert.equal(entry.selection.groupCode, "PAGED_THEME");
  assert.equal(entry.selection.dataCode, "87654321");
  assert.equal(entry.operator.theme.themeCode, "87654321");
  assert.equal(entry.pagePreview.itemCount, 7);
  assert.deepEqual(workflow.legacySelectionForEntry(entry), {
    groupCode: "테마",
    subject: "반도체",
    graphicType: "5단 표그래프",
    subtype: "테마-현재가(입력순)",
    dataCode: "0123"
  });
  assert.equal(workflow.matchesRaw(entry, sourceRaw), true);
  assert.equal(workflow.matchesRaw(entry, { ...sourceRaw, dataCode: "87654321" }), false);
});

test("expected price and the original no-token loss-rate branch remain closed", () => {
  const sourceRaw = raw({
    subtype: "테마-예상체결가",
    pageText: "1/2"
  });
  const native = resolved({
    actionId: "five-row-expected",
    sort: "GAIN_ASC"
  });
  const { pending, outcome } = normalize(sourceRaw, native);
  const entry = workflow.materialize(pending, outcome);

  assert.equal(entry.operator.actionId, "five-row-expected");
  assert.equal(entry.operator.sort, "GAIN_ASC");
  assert.equal(entry.selection.subtype, "EXPECTED");
  assert.equal(entry.selection.graphicType, "GAIN_ASC");
  assert.equal(workflow.matchesRaw(entry, sourceRaw), true);
});

test("5-current, 5-expected, 6 and 12 row wire profiles retain the final sort fallback", () => {
  const cases = [
    ["5단 표그래프", "테마-현재가", "five-row-current", "s5074", "5074", 5],
    ["5단 표그래프", "테마-예상체결가", "five-row-expected", "s5074", "5074", 5],
    ["6종목 현재가", "테마-현재가", "six-row-current", "s5077", "5077", 6],
    ["12종목 현재가", "테마-현재가", "twelve-row-current", "s5088", "5088", 12]
  ];
  for (const [graphicType, subtype, actionId, builderKey, cutCode, pageSize] of cases) {
    const sourceRaw = raw({ graphicType, subtype });
    const native = resolved({ actionId, builderKey, cutCode, pageSize, sort: "GAIN_ASC" });
    const { pending, outcome } = normalize(sourceRaw, native);
    const entry = workflow.materialize(pending, outcome);
    assert.equal(entry.builderKey, builderKey);
    assert.equal(entry.pageSize, pageSize);
    assert.equal(entry.operator.sort, "GAIN_ASC");
  }
});

test("240 visible rows with truncation is the only accepted truncated preview boundary", () => {
  const { pending, outcome } = normalize(raw(), resolved({
    previewItemCount: 240,
    previewIsTruncated: true
  }));
  const entry = workflow.materialize(pending, outcome);
  assert.equal(entry.operator.namedKrxRestore.previewItemCount, 240);
  assert.equal(entry.operator.namedKrxRestore.previewIsTruncated, true);
  assert.equal(entry.pagePreview.pageCount, 20);
  assert.equal(entry.pagePreview.isTruncated, true);
});

test("materialization accepts only branded normalized responses", () => {
  const pending = workflow.classify(raw(), { id: "db-4", fadeDuration: 6 });
  assert.equal(workflow.materialize(pending, resolved()), null);
  const failed = workflow.normalizeResponse(response({
    itemIndex: 4,
    status: "failed",
    failure: "PREVIEW_EMPTY"
  }), "load-1", [pending]).rows[0];
  assert.equal(workflow.materialize(pending, failed), null);
  assert.equal(workflow.materialize({}, resolved()), null);
});

test("enabled replacement is immutable and preserves only the trusted KRX brand", () => {
  const sourceRaw = raw({ enabled: true });
  const { pending, outcome } = normalize(sourceRaw);
  const entry = workflow.materialize(pending, outcome);
  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.deepEqual(workflow.legacySelectionForEntry(replacement),
    workflow.legacySelectionForEntry(entry));
  assert.equal(workflow.matchesRaw(replacement, { ...sourceRaw, enabled: false }), true);
  assert.equal(workflow.withEnabled({ ...entry }, false), null);
  assert.equal(workflow.withEnabled(entry, "false"), null);
});

test("native partial emits the exact result and non-retry error contract", () => {
  const root = path.resolve(__dirname, "..", "..");
  const native = fs.readFileSync(path.join(root, "MainWindow.NamedKrxThemeRestore.cs"), "utf8");
  assert.match(native, /PostMessage\("named-krx-theme-restore-results"/u);
  assert.match(native, /PostMessage\("named-krx-theme-restore-error"/u);
  assert.match(native, /identity\.StoredThemeCode/u);
  assert.match(native, /identity\.CurrentThemeCode/u);
  assert.match(native, /identity\.PreviewIsTruncated/u);
  assert.match(native, /retryable = false/u);
  assert.doesNotMatch(native, /retryable = true/u);
  assert.doesNotMatch(native, /IPlayoutEngine|PrepareAsync|TakeInAsync|NextAsync|TakeOutAsync/u);
});
