"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/operator-catalog-workflow.js");

const repositoryRoot = path.resolve(__dirname, "..", "..");
const nativeBridge = fs.readFileSync(
  path.join(repositoryRoot, "MainWindow.OperatorCatalogs.cs"),
  "utf8");
const playoutBridge = fs.readFileSync(
  path.join(repositoryRoot, "MainWindow.Playout.cs"),
  "utf8");
const operatorGate = fs.readFileSync(
  path.join(repositoryRoot, "src", "MBN_STOCK_WEBVIEW.Core", "Playout", "OperatorMutationGate.cs"),
  "utf8");
const stockMasterValidation = fs.readFileSync(
  path.join(repositoryRoot, "src", "MBN_STOCK_WEBVIEW.Core", "Data",
    "StockMasterIdentityValidation.cs"),
  "utf8");

const krxSelection = Object.freeze({
  market: "krx",
  session: "notApplicable",
  themeCode: "00000001",
  themeTitle: "AI 반도체",
  source: "oracle"
});

const expertSelection = Object.freeze({
  expertCode: "0001",
  expertName: "홍길동",
  source: "oracle"
});

function themePreview(overrides = {}) {
  return {
    requestId: "theme-preview-1",
    selection: {
      market: "krx",
      session: "notApplicable",
      themeCode: "00000001",
      title: "AI 반도체"
    },
    retrievedAt: "2026-07-11T23:00:00+09:00",
    totalRowCount: 2,
    truncated: false,
    items: [
      { inputIndex: 0, itemCode: "P005930", itemName: "삼성전자" },
      { inputIndex: 3, itemCode: "D035720", itemName: "카카오" }
    ],
    ...overrides
  };
}

function expertPreview(overrides = {}) {
  return {
    requestId: "expert-preview-1",
    selection: { expertCode: "0001", expertName: "홍길동" },
    retrievedAt: "2026-07-11T23:00:00+09:00",
    totalRowCount: 2,
    truncated: false,
    items: [
      { playIndex: 0, stockCode: "005930", stockName: "삼성전자", buyAmount: 70000 },
      { playIndex: 4, stockCode: "035720", stockName: "카카오", buyAmount: 45000 }
    ],
    ...overrides
  };
}

test("bridge contract pins every independent request and event name", () => {
  assert.deepEqual(workflow.bridgeContract.requests, {
    themeList: "request-theme-catalog-list",
    expertList: "request-expert-catalog-list",
    schemaPreflight: "request-operator-catalog-schema",
    themeNextCode: "request-theme-catalog-next-code",
    expertNextCode: "request-expert-catalog-next-code",
    themeCreate: "create-theme-catalog",
    themeReplace: "replace-theme-catalog",
    themeDelete: "delete-theme-catalog",
    expertCreate: "create-expert-catalog",
    expertReplace: "replace-expert-catalog",
    expertDelete: "delete-expert-catalog"
  });
  assert.deepEqual(workflow.bridgeContract.events, {
    themeList: "theme-catalog-list-results",
    expertList: "expert-catalog-list-results",
    schemaPreflight: "operator-catalog-schema-results",
    themeNextCode: "theme-catalog-next-code-results",
    expertNextCode: "expert-catalog-next-code-results",
    mutation: "operator-catalog-mutation-results",
    error: "operator-catalog-error"
  });
  assert.deepEqual(workflow.stockMasterContract, {
    request: "search-stocks",
    result: "stock-search-results",
    error: "stock-search-error"
  });
  assert.deepEqual(workflow.limits, {
    maximumThemeItems: 240,
    maximumRecommendations: 100,
    maximumResults: 500,
    maximumQueryLength: 64
  });
});

test("stock master search accepts only complete unique typed identities", () => {
  const request = workflow.createStockMasterSearchRequest("stock-master-1", "  삼성  ", 25);
  assert.deepEqual(request, {
    requestId: "stock-master-1",
    query: "삼성",
    maximumResults: 25
  });
  const base = {
    requestId: request.requestId,
    query: request.query,
    retrievedAt: "2026-07-12T10:00:00+09:00",
    totalRowCount: 1,
    truncated: false,
    results: [{
      market: "kospi", source: "oracle", name: "삼성전자", code: "005930"
    }]
  };
  assert.ok(workflow.normalizeStockMasterSearchResult(base, request));
  assert.equal(workflow.normalizeStockMasterSearchResult({ ...base, truncated: true }, request), null);
  assert.equal(workflow.normalizeStockMasterSearchResult({
    ...base,
    results: [{ ...base.results[0], extra: true }]
  }, request), null);
  assert.equal(workflow.normalizeStockMasterSearchResult({
    ...base,
    totalRowCount: 2,
    results: [
      base.results[0],
      { market: "kosdaq", source: "oracle", name: "삼성전자", code: "123456" }
    ]
  }, request), null);
  assert.equal(workflow.normalizeStockMasterSearchResult({
    ...base,
    requestId: "stale"
  }, request), null);
});

test("native partial pins the same contract and fail-closed write gates without a retry loop", () => {
  for (const requestType of Object.values(workflow.bridgeContract.requests)) {
    assert.match(nativeBridge, new RegExp(`"${requestType}"`));
  }
  for (const eventType of Object.values(workflow.bridgeContract.events)) {
    assert.match(nativeBridge, new RegExp(`"${eventType}"`));
  }
  assert.match(nativeBridge, /_operatorCatalogWriteGate\.WaitAsync\(0\)/);
  assert.match(nativeBridge, /_playoutCommandGate\.WaitAsync\(0, requestToken\)/);
  assert.match(nativeBridge, /_databaseActivityGate\.WaitAsync\(0, requestToken\)/);
  assert.match(nativeBridge, /CanStartOperatorCatalogWrite\(\)/);
  assert.match(nativeBridge,
    /CanStartOperatorMutation\(IsOperatorCatalogWriteQuarantined\(\)\)/);
  assert.match(playoutBridge, /private bool CanStartOperatorMutation\(/);
  assert.match(playoutBridge, /new OperatorMutationGateSnapshot\(/);
  assert.match(operatorGate, /!snapshot\.IsPlayCompletionPending/);
  assert.match(operatorGate, /snapshot\.PreparedSceneName/);
  assert.match(operatorGate, /snapshot\.OnAirSceneName/);
  assert.match(operatorGate, /snapshot\.PreparedCutCode/);
  assert.match(operatorGate, /snapshot\.OnAirCutCode/);
  assert.match(nativeBridge, /LatchOperatorCatalogWriteQuarantine/);
  assert.match(nativeBridge, /OUTCOME_UNKNOWN/);
  assert.match(nativeBridge, /OperatorCatalogMutationExecutor/);
  assert.match(nativeBridge, /LegacyStockMasterIdentityValidationService/);
  assert.match(nativeBridge,
    /await validationBody\(requestToken\)[\s\S]*mutationDispatched = true;/s);
  assert.match(stockMasterValidation, /STOCK_MASTER_VERIFY_KOSPI/);
  assert.match(stockMasterValidation, /STOCK_MASTER_VERIFY_KOSDAQ/);
  assert.match(stockMasterValidation, /STOCK_MASTER_VERIFY_NXT_KOSPI/);
  assert.match(stockMasterValidation, /STOCK_MASTER_VERIFY_NXT_KOSDAQ/);
  assert.doesNotMatch(nativeBridge, /MaximumRetryCount|Task\.Delay|while\s*\(/);
});

test("list, schema and next-code requests are exact, normalized and bounded", () => {
  assert.deepEqual(workflow.createThemeCatalogListRequest(
    "theme-list-1", "  AI  ", "preMarket", 25), {
    requestId: "theme-list-1",
    query: "AI",
    nxtSession: "preMarket",
    maximumResults: 25
  });
  assert.deepEqual(workflow.createExpertCatalogListRequest("expert-list-1", "  홍  "), {
    requestId: "expert-list-1",
    query: "홍"
  });
  assert.deepEqual(workflow.createSchemaPreflightRequest("schema-1"), {
    requestId: "schema-1"
  });
  assert.deepEqual(workflow.createThemeNextCodeRequest("next-1", "nxt"), {
    requestId: "next-1",
    market: "nxt"
  });
  assert.deepEqual(workflow.createExpertNextCodeRequest("next-2"), {
    requestId: "next-2"
  });
  assert.throws(() => workflow.createThemeCatalogListRequest("bad id", "", "preMarket"), /request id/);
  assert.throws(() => workflow.createThemeCatalogListRequest("safe", "", "none"), /session/);
  assert.throws(() => workflow.createExpertCatalogListRequest("safe", "bad\u0000query"), /query/);
  assert.throws(() => workflow.createExpertCatalogListRequest("safe", "", 501), /limited/);
});

test("theme selection plus complete preview becomes an editable explicit identity and reindexed draft", () => {
  const edit = workflow.createThemeEditDraft(krxSelection, themePreview());
  assert.deepEqual(edit, {
    expectedIdentity: {
      market: "krx",
      session: "notApplicable",
      themeCode: "00000001",
      themeTitle: "AI 반도체"
    },
    draft: {
      market: "krx",
      themeCode: "00000001",
      themeTitle: "AI 반도체",
      items: [
        { inputIndex: 0, itemCode: "P005930", itemName: "삼성전자" },
        { inputIndex: 1, itemCode: "D035720", itemName: "카카오" }
      ]
    }
  });
  edit.draft.themeTitle = "AI·로봇";
  edit.draft.items.reverse();
  edit.draft.items.forEach((item, index) => { item.inputIndex = index; });
  const request = workflow.createThemeReplaceRequest("theme-replace-1", edit);
  assert.deepEqual(request, {
    requestId: "theme-replace-1",
    expectedIdentity: {
      market: "krx",
      session: "notApplicable",
      themeCode: "00000001",
      themeTitle: "AI 반도체"
    },
    newTitle: "AI·로봇",
    items: [
      { inputIndex: 0, itemCode: "D035720", itemName: "카카오" },
      { inputIndex: 1, itemCode: "P005930", itemName: "삼성전자" }
    ]
  });
});

test("theme edit conversion rejects truncated, stale, duplicate and market-mismatched previews", () => {
  assert.throws(() => workflow.createThemeEditDraft(
    krxSelection,
    themePreview({ truncated: true })), /complete/);
  assert.throws(() => workflow.createThemeEditDraft(
    krxSelection,
    themePreview({ selection: { ...themePreview().selection, themeCode: "00000002" } })), /correlated/);
  assert.throws(() => workflow.createThemeEditDraft(
    krxSelection,
    themePreview({
      items: [
        { inputIndex: 0, itemCode: "P005930", itemName: "삼성전자" },
        { inputIndex: 1, itemCode: "D035720", itemName: "삼성전자" }
      ]
    })), /complete/);
  assert.equal(workflow.normalizeThemeDraft({
    market: "nxt",
    themeCode: "00000001",
    themeTitle: "로봇(NXT)",
    items: []
  }), null);
  assert.equal(workflow.normalizeThemeDraft({
    market: "krx",
    themeCode: "00000001",
    themeTitle: "AI",
    items: [{ inputIndex: 0, itemCode: "X005930", itemName: "삼성전자" }]
  }), null);
});

test("new theme drafts require a valid title before create and delete keeps full identity", () => {
  const draft = workflow.createBlankThemeDraft("nxt", "00000003");
  assert.deepEqual(draft, {
    market: "nxt",
    themeCode: "00000003",
    themeTitle: "",
    items: []
  });
  assert.throws(() => workflow.createThemeCreateRequest("create-1", draft), /complete/);
  draft.themeTitle = "로봇";
  draft.items.push({ inputIndex: 0, itemCode: "X005930", itemName: "삼성전자" });
  assert.deepEqual(workflow.createThemeCreateRequest("create-1", draft), {
    requestId: "create-1",
    draft
  });
  assert.deepEqual(workflow.createThemeDeleteRequest(
    "delete-1",
    { market: "nxt", session: "afterMarket", themeCode: "00000003", themeTitle: "로봇" }), {
    requestId: "delete-1",
    identity: {
      market: "nxt",
      session: "afterMarket",
      themeCode: "00000003",
      themeTitle: "로봇"
    }
  });
});

test("existing theme edits preserve legacy three through eight digit codes", () => {
  const edit = workflow.createThemeEditDraft(
    { ...krxSelection, themeCode: "007" },
    {
      ...themePreview(),
      selection: { ...themePreview().selection, themeCode: "007" }
    });
  edit.draft.themeTitle = "수정 테마";
  assert.equal(
    workflow.createThemeReplaceRequest("legacy-theme-replace", edit)
      .expectedIdentity.themeCode,
    "007");
  assert.throws(() => workflow.createThemeCreateRequest(
    "legacy-theme-create",
    edit.draft), /eight-digit/i);
});

test("expert selection plus preview becomes editable and preserves buy amounts while reindexing", () => {
  const edit = workflow.createExpertEditDraft(expertSelection, expertPreview());
  assert.deepEqual(edit, {
    expectedIdentity: { expertCode: "0001", expertName: "홍길동" },
    draft: {
      expertCode: "0001",
      expertName: "홍길동",
      recommendations: [
        { playIndex: 0, stockCode: "005930", stockName: "삼성전자", buyAmount: 70000 },
        { playIndex: 1, stockCode: "035720", stockName: "카카오", buyAmount: 45000 }
      ]
    }
  });
  edit.draft.expertName = "홍길동 위원";
  assert.deepEqual(workflow.createExpertReplaceRequest("expert-replace-1", edit), {
    requestId: "expert-replace-1",
    expectedIdentity: { expertCode: "0001", expertName: "홍길동" },
    newName: "홍길동 위원",
    recommendations: edit.draft.recommendations
  });
  assert.throws(() => workflow.createExpertEditDraft(
    expertSelection,
    expertPreview({ truncated: true })), /complete/);
  assert.equal(workflow.normalizeExpertDraft({
    expertCode: "0001",
    expertName: "홍길동",
    recommendations: [
      { playIndex: 0, stockCode: "005930", stockName: "삼성전자", buyAmount: 70000 },
      { playIndex: 1, stockCode: "005930", stockName: "다른이름", buyAmount: 1 }
    ]
  }), null);
});

test("new expert create and delete requests carry exact draft and identity only", () => {
  const draft = workflow.createBlankExpertDraft("0002");
  assert.throws(() => workflow.createExpertCreateRequest("expert-create-1", draft), /complete/);
  draft.expertName = "김전문";
  draft.recommendations.push({
    playIndex: 0,
    stockCode: "005930",
    stockName: "삼성전자",
    buyAmount: 71000
  });
  assert.deepEqual(workflow.createExpertCreateRequest("expert-create-1", draft), {
    requestId: "expert-create-1",
    draft
  });
  assert.deepEqual(workflow.createExpertDeleteRequest(
    "expert-delete-1",
    { expertCode: "0002", expertName: "김전문" }), {
    requestId: "expert-delete-1",
    identity: { expertCode: "0002", expertName: "김전문" }
  });
  assert.throws(() => workflow.createExpertCreateRequest("safe", {
    ...draft,
    recommendations: [{ ...draft.recommendations[0], buyAmount: 1.5 }]
  }), /complete/);
});

test("read responses reject stale correlation, ambiguous identities and quarantined mutation flags", () => {
  const themeRequest = workflow.createThemeCatalogListRequest("theme-list-1", "", "preMarket", 10);
  const themeResult = {
    requestId: "theme-list-1",
    query: "",
    nxtSession: "preMarket",
    retrievedAt: "2026-07-11T23:00:00+09:00",
    totalRowCount: 1,
    truncated: false,
    canMutate: true,
    writeQuarantined: false,
    results: [krxSelection]
  };
  assert.ok(workflow.normalizeThemeListResult(themeResult, themeRequest));
  assert.equal(workflow.normalizeThemeListResult({ ...themeResult, requestId: "stale" }, themeRequest), null);
  assert.equal(workflow.normalizeThemeListResult({
    ...themeResult,
    canMutate: true,
    writeQuarantined: true
  }, themeRequest), null);

  const expertRequest = workflow.createExpertCatalogListRequest("expert-list-1", "", 10);
  const expertResult = {
    requestId: "expert-list-1",
    query: "",
    retrievedAt: "2026-07-11T23:00:00+09:00",
    totalRowCount: 2,
    truncated: false,
    canMutate: true,
    writeQuarantined: false,
    results: [
      expertSelection,
      { expertCode: "0002", expertName: "김전문", source: "oracle" }
    ]
  };
  assert.ok(workflow.normalizeExpertListResult(expertResult, expertRequest));
  assert.equal(workflow.normalizeExpertListResult({
    ...expertResult,
    results: [expertSelection, { expertCode: "0002", expertName: "홍길동", source: "oracle" }]
  }, expertRequest), null);
});

test("schema and next-code results are exact and correlated", () => {
  const schemaRequest = workflow.createSchemaPreflightRequest("schema-1");
  assert.deepEqual(workflow.normalizeSchemaResult({
    requestId: "schema-1",
    validatedAt: "2026-07-11T23:00:00+09:00",
    validatedSources: ["oracle", "mariaDb"],
    themeCanMutate: true,
    expertCanMutate: true,
    writeQuarantined: false
  }, schemaRequest).validatedSources, ["oracle", "mariaDb"]);
  const themeNextRequest = workflow.createThemeNextCodeRequest("next-1", "krx");
  assert.ok(workflow.normalizeThemeNextCodeResult({
    requestId: "next-1",
    market: "krx",
    themeCode: "00000003",
    canMutate: true,
    writeQuarantined: false
  }, themeNextRequest));
  assert.equal(workflow.normalizeThemeNextCodeResult({
    requestId: "next-1",
    market: "nxt",
    themeCode: "00000003",
    canMutate: true,
    writeQuarantined: false
  }, themeNextRequest), null);
  assert.ok(workflow.normalizeExpertNextCodeResult({
    requestId: "next-2",
    expertCode: "0007",
    canMutate: false,
    writeQuarantined: true
  }, workflow.createExpertNextCodeRequest("next-2")));
});

test("mutation coordinator permits one correlated write and latches process quarantine on unknown outcome", () => {
  const request = workflow.createThemeCreateRequest("create-1", {
    market: "krx",
    themeCode: "00000003",
    themeTitle: "로봇",
    items: []
  });
  const coordinator = workflow.createMutationCoordinator();
  assert.deepEqual(coordinator.begin(workflow.bridgeContract.requests.themeCreate, request), {
    requestId: "create-1",
    entity: "theme",
    operation: "create",
    identityCode: "00000003"
  });
  assert.throws(() => coordinator.begin(workflow.bridgeContract.requests.themeCreate, request), /already active/);
  assert.equal(coordinator.acceptResult({
    requestId: "stale",
    entity: "theme",
    operation: "create",
    identityCode: "00000003",
    operationId: "123e4567-e89b-12d3-a456-426614174000",
    committedAt: "2026-07-11T23:00:00+09:00",
    writeQuarantined: false
  }), null);
  assert.ok(coordinator.getState().active);
  const error = coordinator.acceptError({
    requestId: "create-1",
    entity: "theme",
    operation: "create",
    identityCode: "00000003",
    code: "OUTCOME_UNKNOWN",
    message: "Restart and reconcile before another write.",
    retryable: false,
    outcomeUnknown: true,
    writeQuarantined: true
  });
  assert.ok(error);
  assert.deepEqual(coordinator.getState(), { active: null, quarantined: true });
  assert.throws(() => coordinator.begin(workflow.bridgeContract.requests.themeCreate, request), /quarantined/);
});

test("known rollback clears the active coordinator without inventing an automatic retry", () => {
  const request = workflow.createExpertDeleteRequest(
    "delete-1",
    { expertCode: "0001", expertName: "홍길동" });
  const coordinator = workflow.createMutationCoordinator();
  coordinator.begin(workflow.bridgeContract.requests.expertDelete, request);
  assert.ok(coordinator.acceptError({
    requestId: "delete-1",
    entity: "expert",
    operation: "delete",
    identityCode: "0001",
    code: "CONFLICT",
    message: "Refresh before a new edit.",
    retryable: false,
    outcomeUnknown: false,
    writeQuarantined: false
  }));
  assert.deepEqual(coordinator.getState(), { active: null, quarantined: false });
  coordinator.begin(workflow.bridgeContract.requests.expertDelete, request);
  coordinator.loseCorrelation();
  assert.deepEqual(coordinator.getState(), { active: null, quarantined: true });
});
