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

const expert = Object.freeze({
  expertCode: "0001",
  expertName: "Analyst A",
  source: "oracle"
});

function previewRequest(id = "preview-1", maximumItems) {
  return workflow.createExpertPreviewRequest(id, expert, maximumItems);
}

function previewPayload(items, overrides = {}) {
  return {
    requestId: "preview-1",
    selection: { expertCode: "0001", expertName: "Analyst A" },
    retrievedAt: "2026-07-11T23:10:00+09:00",
    totalRowCount: items.length,
    truncated: false,
    items,
    ...overrides
  };
}

const recommendations = Object.freeze([
  Object.freeze({ playIndex: 0, stockCode: "005930", stockName: "Samsung", buyAmount: 70000 }),
  Object.freeze({ playIndex: 2, stockCode: "035720", stockName: "Kakao", buyAmount: 42000 })
]);

test("workflow pins the Oracle UC6 reader and one exact 5074 action", () => {
  assert.deepEqual(workflow.bridgeContract, {
    searchRequestType: "search-experts",
    searchResultType: "expert-search-results",
    searchErrorType: "expert-search-error",
    previewRequestType: "request-expert-preview",
    previewResultType: "expert-preview-results",
    previewErrorType: "expert-preview-error",
    defaultMaximumResults: 100,
    maximumResults: 500,
    maximumQueryLength: 64,
    defaultMaximumPreviewItems: 100,
    maximumPreviewItems: 100
  });
  assert.equal(workflow.sourceContract.provider, "oracle");
  assert.equal(workflow.sourceContract.expertTable, "EXPERT_LIST");
  assert.equal(workflow.sourceContract.recommendationTable, "RECOMMEND_LIST");
  assert.equal(workflow.sourceContract.pageSize, 5);
  assert.equal(workflow.sourceContract.maximumPageCount, 20);
  assert.deepEqual(workflow.actions, [{
    id: "five-row-current",
    label: "전문가 추천 · 5단 표그래프 · 현재가",
    builderKey: "s5074",
    code: "5074",
    aliases: ["5074"],
    pageSize: 5
  }]);
});

test("expert search requests are exact, bounded and normalize the original blank load", () => {
  assert.deepEqual(workflow.createExpertSearchRequest("search-1", "   "), {
    requestId: "search-1",
    query: ""
  });
  assert.deepEqual(workflow.createExpertSearchRequest("search-2", "  Kim  ", 25), {
    requestId: "search-2",
    query: "Kim",
    maximumResults: 25
  });
  assert.throws(() => workflow.createExpertSearchRequest("bad id", ""), /request id/);
  assert.throws(() => workflow.createExpertSearchRequest("safe", "bad\u0000query"), /query/);
  assert.throws(() => workflow.createExpertSearchRequest("safe", "", 0), /limited/);
  assert.throws(() => workflow.createExpertSearchRequest("safe", "", 501), /limited/);
});

test("search responses keep typed code and name and reject ambiguity or reordering", () => {
  const request = workflow.createExpertSearchRequest("search-1", "", 5);
  const payload = {
    requestId: "search-1",
    query: "",
    retrievedAt: "2026-07-11T23:00:00+09:00",
    totalRowCount: 2,
    truncated: false,
    results: [
      { expertCode: "0001", expertName: "Alpha", source: "oracle" },
      { expertCode: "0002", expertName: "Beta", source: "oracle" }
    ]
  };
  const normalized = workflow.normalizeExpertSearchResponse(payload, request);
  assert.ok(normalized);
  assert.deepEqual(normalized.results[0], {
    expertCode: "0001",
    expertName: "Alpha",
    source: "oracle"
  });
  assert.ok(Object.isFrozen(normalized.results));
  assert.equal(workflow.normalizeExpertSearchResponse({
    ...payload,
    results: [...payload.results].reverse()
  }, request), null);
  assert.equal(workflow.normalizeExpertSearchResponse({
    ...payload,
    results: [payload.results[0], { ...payload.results[1], expertName: "Alpha" }]
  }, request), null);
  assert.equal(workflow.normalizeExpertSearchResponse({
    ...payload,
    results: [payload.results[0], { ...payload.results[1], expertCode: "0001" }]
  }, request), null);
  assert.equal(workflow.normalizeExpertSearchResponse({
    ...payload,
    results: [{ ...payload.results[0], source: "mariaDb" }, payload.results[1]]
  }, request), null);
  assert.equal(workflow.normalizeExpertSearchResponse({ ...payload, requestId: "stale" }, request), null);
});

test("search errors are exact and correlated to the active request", () => {
  const request = workflow.createExpertSearchRequest("search-1", "Kim");
  assert.deepEqual(workflow.normalizeExpertSearchError({
    requestId: "search-1",
    query: "Kim",
    message: "Database unavailable"
  }, request), {
    requestId: "search-1",
    query: "Kim",
    message: "Database unavailable"
  });
  assert.equal(workflow.normalizeExpertSearchError({
    requestId: "other",
    query: "Kim",
    message: "Database unavailable"
  }, request), null);
  assert.equal(workflow.normalizeExpertSearchError({
    requestId: "search-1",
    query: "Kim",
    message: "bad\nmessage"
  }, request), null);
});

test("preview requests bind the exact expert code and name", () => {
  assert.deepEqual(previewRequest("preview-1", 25), {
    requestId: "preview-1",
    expertCode: "0001",
    expertName: "Analyst A",
    maximumItems: 25
  });
  assert.throws(() => workflow.createExpertPreviewRequest("preview", {
    expertCode: "0001",
    expertName: "Analyst A",
    source: "mariaDb"
  }), /Oracle expert/);
  assert.throws(() => workflow.createExpertPreviewRequest("preview", {
    expertCode: "001",
    expertName: "Analyst A",
    source: "oracle"
  }), /Oracle expert/);
  assert.throws(() => previewRequest("preview", 101), /limited/);
});

test("preview responses preserve PLAYINDEX and reject duplicate or unsafe rows", () => {
  const request = previewRequest();
  const normalized = workflow.normalizeExpertPreviewResponse(
    previewPayload(recommendations),
    request);
  assert.ok(normalized);
  assert.deepEqual(normalized.items, recommendations);
  assert.ok(Object.isFrozen(normalized.items));

  for (const items of [
    [recommendations[0], { ...recommendations[1], playIndex: 0 }],
    [recommendations[0], { ...recommendations[1], stockCode: "005930" }],
    [recommendations[0], { ...recommendations[1], stockName: "Samsung" }],
    [recommendations[1], recommendations[0]],
    [{ ...recommendations[0], buyAmount: 0 }],
    [{ ...recommendations[0], buyAmount: 100.5 }],
    [{ ...recommendations[0], stockCode: "../005930" }]
  ]) {
    assert.equal(workflow.normalizeExpertPreviewResponse(previewPayload(items), request), null);
  }
  assert.equal(workflow.normalizeExpertPreviewResponse(
    previewPayload(recommendations, {
      selection: { expertCode: "0002", expertName: "Analyst A" }
    }), request), null);
});

test("only a native-normalized preview can create the closed PAGED_EXPERT selection", () => {
  const raw = previewPayload(recommendations);
  assert.equal(workflow.createSelectableExpert(expert, raw), null);
  const preview = workflow.normalizeExpertPreviewResponse(raw, previewRequest());
  const selected = workflow.createSelectableExpert(expert, preview);
  assert.deepEqual(selected, {
    expertCode: "0001",
    expertName: "Analyst A",
    source: "oracle",
    itemCount: 2,
    previewTruncated: false
  });
  assert.deepEqual(workflow.buildExpertSelection(selected), {
    builderKey: "s5074",
    code: "5074",
    aliases: ["5074"],
    selection: {
      groupCode: "PAGED_EXPERT",
      subject: "Analyst A",
      graphicType: "INPUT_ORDER",
      subtype: "CURRENT",
      dataCode: "0001"
    }
  });
  assert.deepEqual(workflow.calculatePagePreview(selected), {
    itemCount: 2,
    accessibleItemCount: 2,
    pageSize: 5,
    pageCount: 1,
    maximumPageCount: 20,
    isTruncated: false
  });
});

test("the 5 by 20 boundary is explicit and empty experts cannot create a cut", () => {
  const hundred = Array.from({ length: 100 }, (_, index) => ({
    playIndex: index,
    stockCode: String(index + 1).padStart(6, "0"),
    stockName: `Stock ${String(index + 1).padStart(3, "0")}`,
    buyAmount: 1000 + index
  }));
  const request = previewRequest("preview-1", 100);
  const preview = workflow.normalizeExpertPreviewResponse(
    previewPayload(hundred, { truncated: true }),
    request);
  const selected = workflow.createSelectableExpert(expert, preview);
  assert.deepEqual(workflow.calculatePagePreview(selected), {
    itemCount: 100,
    accessibleItemCount: 100,
    pageSize: 5,
    pageCount: 20,
    maximumPageCount: 20,
    isTruncated: true
  });

  const emptyPreview = workflow.normalizeExpertPreviewResponse(
    previewPayload([]),
    previewRequest());
  const empty = workflow.createSelectableExpert(expert, emptyPreview);
  assert.throws(() => workflow.createExpertPlaylistEntry(empty, { id: "empty" }), /no recommendation/);
});

test("playlist creation and trusted restore bind every identity and page field", () => {
  const preview = workflow.normalizeExpertPreviewResponse(
    previewPayload(recommendations),
    previewRequest());
  const selected = workflow.createSelectableExpert(expert, preview);
  const entry = workflow.createExpertPlaylistEntry(selected, {
    id: "expert-1",
    fadeDuration: 8
  });
  assert.equal(entry.builderKey, "s5074");
  assert.equal(entry.code, "5074");
  assert.equal(entry.pageSize, 5);
  assert.equal(entry.pageCount, 1);
  assert.equal(entry.fadeDuration, 8);
  assert.equal(entry.operator.schemaVersion, 1);

  const stored = JSON.parse(JSON.stringify(entry));
  stored.enabled = false;
  const restored = workflow.restoreExpertPlaylistEntry(stored);
  assert.ok(restored);
  assert.equal(restored.enabled, false);
  assert.equal(restored.selection.dataCode, "0001");

  for (const tamper of [
    value => { value.builderKey = "s5077"; },
    value => { value.code = "5077"; },
    value => { value.selection.subject = "Other"; },
    value => { value.selection.dataCode = "0002"; },
    value => { value.operator.expert.expertName = "Other"; },
    value => { value.operator.pagePreview.pageCount = 2; },
    value => { value.operator.actionId = "unknown"; },
    value => { value.enabled = "false"; }
  ]) {
    const candidate = JSON.parse(JSON.stringify(entry));
    tamper(candidate);
    assert.equal(workflow.restoreExpertPlaylistEntry(candidate), null);
  }
  assert.throws(() => workflow.createExpertPlaylistEntry(selected, { id: "bad id" }), /options/);
  assert.throws(() => workflow.createExpertPlaylistEntry(selected, { id: "safe", fadeDuration: 61 }), /Fade/);
});

test("preview errors are exact and bound to expert identity", () => {
  const request = previewRequest();
  assert.deepEqual(workflow.normalizeExpertPreviewError({
    requestId: "preview-1",
    selection: { expertCode: "0001", expertName: "Analyst A" },
    message: "Preview unavailable"
  }, request), {
    requestId: "preview-1",
    selection: { expertCode: "0001", expertName: "Analyst A" },
    message: "Preview unavailable"
  });
  assert.equal(workflow.normalizeExpertPreviewError({
    requestId: "preview-1",
    selection: { expertCode: "0002", expertName: "Analyst A" },
    message: "Preview unavailable"
  }, request), null);
  assert.equal(workflow.normalizeExpertPreviewError({
    requestId: "preview-1",
    selection: { expertCode: "0001", expertName: "Analyst A" },
    message: "bad\u200Bmessage"
  }, request), null);
});
