// ST-BME: regression tests — reroll should reuse persisted recall record
//
// Covers:
//   1. ensurePersistedRecallRecordForGeneration re-writes when existing record
//      has same injectionText/nodeIds but empty recallInput
//   2. resolveReusablePersistedRecallRecord (inside runRecallController) reuses
//      a persisted record when recallInput matches the user floor text
//   3. End-to-end: regenerate does NOT call retrieve when a valid persisted
//      record exists

import assert from "node:assert/strict";
import {
  buildPersistedRecallRecord,
  readPersistedRecallFromUserMessage,
  writePersistedRecallToUserMessage,
  BME_RECALL_EXTRA_KEY,
} from "../retrieval/recall-persistence.js";
import { runRecallController } from "../retrieval/recall-controller.js";
import { createGenerationRecallHarness } from "./helpers/generation-recall-harness.mjs";
import {
  normalizeRecallInputText,
  createRecallRunResult,
  createRecallInputRecord,
  isFreshRecallInputRecord,
} from "../ui/ui-status.js";
import { defaultSettings } from "../runtime/settings-defaults.js";

// ═══════════════════════════════════════════════════════════════
// 1. ensurePersistedRecallRecordForGeneration: empty recallInput override
// ═══════════════════════════════════════════════════════════════

const harness = await createGenerationRecallHarness({ realApplyFinal: true });

// Prime settings
Object.assign(harness.settings, {
  ...defaultSettings,
  enabled: true,
  recallEnabled: true,
});

harness.chat = [
  { is_user: true, mes: "楼层里的稳定用户输入" },
  { is_user: false, mes: "好的。", is_system: false },
];

const persistedWriteResult = harness.result.persistRecallInjectionRecord({
  recallInput: {
    userMessage: "发送前捕获的权威输入",
    source: "send-intent",
    hookName: "GENERATION_AFTER_COMMANDS",
    targetUserMessageIndex: 0,
    authoritativeInputUsed: true,
    boundUserFloorText: "楼层里的稳定用户输入",
  },
  result: {
    selectedNodeIds: ["node-write-1"],
  },
  injectionText: "注入:楼层里的稳定用户输入",
  tokenEstimate: 8,
});
assert.ok(persistedWriteResult?.record, "persistRecallInjectionRecord should write a record");
assert.equal(
  persistedWriteResult.record.authoritativeInputUsed,
  true,
  "initial persisted record should keep authoritativeInputUsed",
);
assert.equal(
  persistedWriteResult.record.boundUserFloorText,
  "楼层里的稳定用户输入",
  "initial persisted record should keep boundUserFloorText",
);

console.log("  ✓ persistRecallInjectionRecord stores authoritative input metadata");

// Set up chat: user + assistant
harness.chat = [
  { is_user: true, mes: "去摩耶山看夜景" },
  { is_user: false, mes: "好的，我们出发吧。", is_system: false },
];

// Pre-write a persisted record with EMPTY recallInput (simulates old bug)
const emptyRecallInputRecord = buildPersistedRecallRecord({
  injectionText: "注入:去摩耶山看夜景",
  selectedNodeIds: ["node-test-1"],
  recallInput: "",
  recallSource: "chat-tail-user",
  hookName: "GENERATION_AFTER_COMMANDS",
  tokenEstimate: 5,
  manuallyEdited: false,
});
writePersistedRecallToUserMessage(harness.chat, 0, emptyRecallInputRecord);

// Verify the record is written with empty recallInput
const beforeRecord = readPersistedRecallFromUserMessage(harness.chat, 0);
assert.ok(beforeRecord, "persisted record should exist before ensure");
assert.equal(beforeRecord.recallInput, "", "recallInput should be empty before fix");
assert.equal(
  beforeRecord.injectionText,
  "注入:去摩耶山看夜景",
  "injectionText should match",
);

// Build a mock recall result with the same injectionText
const mockRecallResult = {
  status: "completed",
  didRecall: true,
  ok: true,
  injectionText: "注入:去摩耶山看夜景",
  selectedNodeIds: ["node-test-1"],
  source: "chat-last-user",
  sourceLabel: "历史最后用户楼层",
  hookName: "GENERATION_AFTER_COMMANDS",
  authoritativeInputUsed: false,
  boundUserFloorText: "去摩耶山看夜景",
};

// Build frozen recall options with overrideUserMessage
const frozenRecallOptions = {
  generationType: "regenerate",
  targetUserMessageIndex: 0,
  overrideUserMessage: "去摩耶山看夜景",
  overrideSource: "chat-last-user",
  overrideSourceLabel: "历史最后用户楼层",
  lockedSource: "chat-last-user",
  lockedSourceLabel: "历史最后用户楼层",
  authoritativeInputUsed: false,
  boundUserFloorText: "去摩耶山看夜景",
};

// Call ensurePersistedRecallRecordForGeneration
const ensureResult = harness.result.ensurePersistedRecallRecordForGeneration({
  generationType: "regenerate",
  recallResult: mockRecallResult,
  transaction: { frozenRecallOptions },
  recallOptions: frozenRecallOptions,
  hookName: "GENERATION_AFTER_COMMANDS",
});

// After fix: the record should be overwritten because existing recallInput is empty
const afterRecord = readPersistedRecallFromUserMessage(harness.chat, 0);
assert.ok(afterRecord, "persisted record should still exist after ensure");
assert.equal(
  afterRecord.recallInput,
  "去摩耶山看夜景",
  "recallInput should now be populated after ensure overwrites empty-recallInput record",
);
assert.equal(
  afterRecord.boundUserFloorText,
  "去摩耶山看夜景",
  "boundUserFloorText should be populated",
);

console.log("  ✓ ensurePersistedRecallRecordForGeneration overwrites record with empty recallInput");

harness.chat = [
  { is_user: true, mes: "稳定楼层文本" },
  { is_user: false, mes: "好的。", is_system: false },
];
const staleMetadataRecord = buildPersistedRecallRecord({
  injectionText: "注入:稳定楼层文本",
  selectedNodeIds: ["node-stale-meta"],
  recallInput: "发送前捕获文本",
  recallSource: "send-intent",
  hookName: "GENERATION_AFTER_COMMANDS",
  tokenEstimate: 4,
  manuallyEdited: false,
});
writePersistedRecallToUserMessage(harness.chat, 0, staleMetadataRecord);
const staleMetadataEnsureResult = harness.result.ensurePersistedRecallRecordForGeneration({
  generationType: "regenerate",
  recallResult: {
    status: "completed",
    didRecall: true,
    ok: true,
    injectionText: "注入:稳定楼层文本",
    selectedNodeIds: ["node-stale-meta"],
    recallInput: "发送前捕获文本",
    source: "send-intent",
    hookName: "GENERATION_AFTER_COMMANDS",
    authoritativeInputUsed: false,
    boundUserFloorText: "稳定楼层文本",
  },
  transaction: {
    frozenRecallOptions: {
      generationType: "regenerate",
      targetUserMessageIndex: 0,
      overrideUserMessage: "稳定楼层文本",
      overrideSource: "chat-last-user",
      authoritativeInputUsed: false,
      boundUserFloorText: "稳定楼层文本",
    },
  },
  recallOptions: {
    generationType: "regenerate",
    targetUserMessageIndex: 0,
    overrideUserMessage: "稳定楼层文本",
    authoritativeInputUsed: false,
    boundUserFloorText: "稳定楼层文本",
  },
  hookName: "GENERATION_AFTER_COMMANDS",
});
assert.equal(
  staleMetadataEnsureResult.persisted,
  true,
  "ensure should rewrite records whose metadata is stale even when text/nodeIds match",
);
const repairedMetadataRecord = readPersistedRecallFromUserMessage(harness.chat, 0);
assert.equal(
  repairedMetadataRecord.boundUserFloorText,
  "稳定楼层文本",
  "ensure should repair missing boundUserFloorText",
);

console.log("  ✓ ensurePersistedRecallRecordForGeneration repairs stale metadata");

// ═══════════════════════════════════════════════════════════════
// 2. ensurePersistedRecallRecordForGeneration: populated recallInput skip
// ═══════════════════════════════════════════════════════════════

harness.chat = [
  { is_user: true, mes: "去摩耶山看夜景" },
  { is_user: false, mes: "好的，我们出发吧。", is_system: false },
];
writePersistedRecallToUserMessage(harness.chat, 0, afterRecord);

// Now the record has proper recallInput — calling ensure again should skip
const ensureResult2 = harness.result.ensurePersistedRecallRecordForGeneration({
  generationType: "regenerate",
  recallResult: mockRecallResult,
  transaction: { frozenRecallOptions },
  recallOptions: frozenRecallOptions,
  hookName: "GENERATION_AFTER_COMMANDS",
});
assert.equal(
  ensureResult2.reason,
  "already-up-to-date",
  "should skip when recallInput is already populated",
);

console.log("  ✓ ensurePersistedRecallRecordForGeneration skips when recallInput is populated");

harness.chat = [
  { is_user: true, mes: "继续写摩耶山夜景" },
  { is_user: false, mes: "前一次回复。", is_system: false },
];
harness.result.cleanupGenerationRecallTransactions(Date.now() + 60000);
const afterCommandsRecallOptions = harness.result.buildGenerationAfterCommandsRecallInput(
  "regenerate",
  {},
  harness.chat,
);
const afterCommandsContext = harness.result.createGenerationRecallContext({
  hookName: "GENERATION_AFTER_COMMANDS",
  generationType: "regenerate",
  recallOptions: afterCommandsRecallOptions,
});
assert.ok(
  afterCommandsContext.transaction,
  "after-commands should create a history transaction",
);
harness.result.markGenerationRecallTransactionHookState(
  afterCommandsContext.transaction,
  "GENERATION_AFTER_COMMANDS",
  "completed",
);
const beforeCombineNormalFallback = harness.result.buildNormalGenerationRecallInput(
  harness.chat,
);
const beforeCombineContext = harness.result.createGenerationRecallContext({
  hookName: "GENERATE_BEFORE_COMBINE_PROMPTS",
  generationType: "normal",
  recallOptions: beforeCombineNormalFallback,
});
assert.equal(
  beforeCombineContext.transaction,
  afterCommandsContext.transaction,
  "before-combine should reuse the existing history transaction despite normal fallback input",
);
assert.equal(
  beforeCombineContext.generationType,
  "regenerate",
  "before-combine should keep the transaction's history generation type",
);
assert.equal(
  beforeCombineContext.shouldRun,
  false,
  "before-combine should not run recall again after after-commands completed",
);

console.log("  ✓ before-combine reuses existing history transaction");

// ═══════════════════════════════════════════════════════════════
// 3. runRecallController: regenerate reuses persisted record
// ═══════════════════════════════════════════════════════════════

// Set up a fresh chat with a properly persisted recall record
const rerollChat = [
  { is_user: true, mes: "明日去摩耶山看夜景" },
  { is_user: false, mes: "好的，明天约好了。", is_system: false },
];

const validRecord = buildPersistedRecallRecord({
  injectionText: "注入:明日去摩耶山看夜景",
  selectedNodeIds: ["node-a"],
  recallInput: "发送意图中的扩展文本，不等于当前用户楼层",
  recallSource: "send-intent",
  hookName: "GENERATION_AFTER_COMMANDS",
  tokenEstimate: 5,
  manuallyEdited: false,
  boundUserFloorText: "明日去摩耶山看夜景",
});
writePersistedRecallToUserMessage(rerollChat, 0, validRecord);

let retrieveCalled = false;
let rerollEnsureVectorReadyCalled = false;
let rerollRecoverHistoryCalled = false;
const rerollStatusLabels = [];
const rerollRuntime = {
  getIsRecalling: () => false,
  getCurrentGraph: () => ({ nodes: [], edges: [] }),
  getSettings: () => ({
    ...defaultSettings,
    enabled: true,
    recallEnabled: true,
    recallLlmContextMessages: 5,
  }),
  isGraphReadableForRecall: () => true,
  isGraphMetadataWriteAllowed: () => true,
  recoverHistoryIfNeeded: async () => {
    rerollRecoverHistoryCalled = true;
    return true;
  },
  getContext: () => ({ chat: rerollChat, chatId: "chat-reroll" }),
  nextRecallRunSequence: () => 1,
  beginStageAbortController: () => ({ signal: { aborted: false } }),
  finishStageAbortController: () => {},
  setIsRecalling: () => {},
  setActiveRecallPromise: () => {},
  getActiveRecallPromise: () => null,
  setLastRecallStatus: (label) => {
    rerollStatusLabels.push(String(label || ""));
  },
  clampInt: (v, f, mn, mx) => {
    const n = Number(v);
    if (!Number.isFinite(n)) return f;
    return Math.min(mx, Math.max(mn, Math.trunc(n)));
  },
  normalizeRecallInputText,
  createRecallInputRecord,
  createRecallRunResult,
  isFreshRecallInputRecord,
  getLatestUserChatMessage: (chat = []) =>
    [...chat].reverse().find((m) => m?.is_user) || null,
  getLastNonSystemChatMessage: (chat = []) =>
    [...chat].reverse().find((m) => !m?.is_system) || null,
  getRecallUserMessageSourceLabel: (s) => s,
  buildRecallRecentMessages: () => [],
  readPersistedRecallFromUserMessage,
  bumpPersistedRecallGenerationCount: (chat, idx) => {
    // no-op in test; just return the record
    return readPersistedRecallFromUserMessage(chat, idx);
  },
  triggerChatMetadataSave: () => {},
  schedulePersistedRecallMessageUiRefresh: () => {},
  refreshPanelLiveState: () => {},
  ensureVectorReadyIfNeeded: async () => {
    rerollEnsureVectorReadyCalled = true;
  },
  resolveRecallInput: (chat, limit, override) => {
    // Simulate resolveRecallInputController override path
    const overrideText = normalizeRecallInputText(
      override?.overrideUserMessage || override?.userMessage || "",
    );
    return {
      userMessage: overrideText,
      generationType: String(override?.generationType || "normal"),
      targetUserMessageIndex: Number.isFinite(override?.targetUserMessageIndex)
        ? override.targetUserMessageIndex
        : null,
      source: override?.overrideSource || "chat-last-user",
      sourceLabel: override?.overrideSourceLabel || "历史最后用户楼层",
      reason: "override-bound",
      authoritativeInputUsed: Boolean(override?.authoritativeInputUsed),
      boundUserFloorText: normalizeRecallInputText(
        override?.boundUserFloorText || "",
      ),
      recentMessages: [],
      hookName: override?.hookName || "",
      deliveryMode: "immediate",
    };
  },
  applyRecallInjection: (_settings, _input, _recent, result) => ({
    injectionText: result?.injectionText || "",
    applied: true,
    source: "persisted-reuse",
    mode: "module-injection",
  }),
  retrieve: async () => {
    retrieveCalled = true;
    return {
      injectionText: "should-not-appear",
      selectedNodeIds: ["node-b"],
    };
  },
  buildRecallRetrieveOptions: () => ({}),
  getEmbeddingConfig: () => ({}),
  getSchema: () => ({}),
  console,
  isAbortError: () => false,
  toastr: { error: () => {} },
  getRecallHookLabel: () => "",
  setPendingRecallSendIntent: () => {},
};

// Simulate regenerate: override with the user floor text and generationType regenerate
const rerollResult = await runRecallController(rerollRuntime, {
  overrideUserMessage: "明日去摩耶山看夜景",
  generationType: "regenerate",
  targetUserMessageIndex: 0,
  overrideSource: "chat-last-user",
  overrideSourceLabel: "历史最后用户楼层",
  hookName: "GENERATION_AFTER_COMMANDS",
  deliveryMode: "immediate",
});

assert.equal(rerollResult.status, "completed", "reroll should complete");
assert.equal(
  rerollResult.reason,
  "persisted-user-floor-reused",
  "reroll should reuse persisted record, not run fresh recall",
);
assert.equal(
  retrieveCalled,
  false,
  "retrieve() should NOT be called when persisted record is reused",
);
assert.equal(
  rerollEnsureVectorReadyCalled,
  false,
  "persisted reroll reuse should not even prepare vectors before reusing the user-floor record",
);
assert.equal(
  rerollRecoverHistoryCalled,
  false,
  "persisted reroll reuse should not trigger pre-recall history recovery",
);
assert.equal(
  rerollStatusLabels.includes("召回中"),
  false,
  "persisted reroll reuse should not enter visible fresh recall state",
);
assert.equal(
  rerollResult.injectionText,
  "注入:明日去摩耶山看夜景",
  "injection text should come from persisted record",
);

console.log("  ✓ runRecallController reuses persisted record on regenerate");

const normalTypedReuseChat = [
  { is_user: true, mes: "重 Roll 但宿主仍标 normal" },
  { is_user: false, mes: "上一条回复。", is_system: false },
];
const normalTypedReuseRecord = buildPersistedRecallRecord({
  injectionText: "注入:重 Roll 但宿主仍标 normal",
  selectedNodeIds: ["node-normal-type"],
  recallInput: "旧捕获文本",
  recallSource: "chat-last-user",
  hookName: "GENERATION_AFTER_COMMANDS",
  tokenEstimate: 5,
  manuallyEdited: false,
  boundUserFloorText: "重 Roll 但宿主仍标 normal",
});
writePersistedRecallToUserMessage(normalTypedReuseChat, 0, normalTypedReuseRecord);

let normalTypedRetrieveCalled = false;
const normalTypedReuseRuntime = {
  ...rerollRuntime,
  getContext: () => ({ chat: normalTypedReuseChat, chatId: "chat-normal-typed-reroll" }),
  retrieve: async () => {
    normalTypedRetrieveCalled = true;
    return {
      injectionText: "不应出现的新召回",
      selectedNodeIds: ["node-new"],
    };
  },
};

const normalTypedReuseResult = await runRecallController(normalTypedReuseRuntime, {
  overrideUserMessage: "重 Roll 但宿主仍标 normal",
  generationType: "normal",
  targetUserMessageIndex: 0,
  overrideSource: "chat-last-user",
  hookName: "GENERATION_AFTER_COMMANDS",
  deliveryMode: "immediate",
});

assert.equal(
  normalTypedReuseResult.reason,
  "persisted-user-floor-reused",
  "normal-typed reroll should still reuse target user-floor recall",
);
assert.equal(
  normalTypedRetrieveCalled,
  false,
  "normal-typed reroll should not call retrieve when target user floor has recall",
);

console.log("  ✓ runRecallController reuses persisted record when host reports reroll as normal");

const rerollInputHarness = await createGenerationRecallHarness({ realApplyFinal: true });
Object.assign(rerollInputHarness.settings, {
  ...defaultSettings,
  enabled: true,
  recallEnabled: true,
  recallUseAuthoritativeGenerationInput: true,
});
rerollInputHarness.chat = [
  { is_user: true, mes: "重 roll replacement 应复用这一楼" },
  { is_user: false, mes: "旧 assistant 回复", is_system: false },
];
writePersistedRecallToUserMessage(
  rerollInputHarness.chat,
  0,
  buildPersistedRecallRecord({
    injectionText: "注入:重 roll replacement 应复用这一楼",
    selectedNodeIds: ["node-reroll-input"],
    recallInput: "重 roll replacement 应复用这一楼",
    recallSource: "chat-last-user",
    hookName: "GENERATION_AFTER_COMMANDS",
    tokenEstimate: 6,
    manuallyEdited: false,
    boundUserFloorText: "重 roll replacement 应复用这一楼",
  }),
);

const preparedRerollReuse = rerollInputHarness.result.prepareRerollRecallReuse({
  fromFloor: 1,
});
assert.ok(preparedRerollReuse, "assistant-only reroll should prepare recall reuse marker");

rerollInputHarness.result.recordRecallSendIntent(
  "错误的主动输入不应覆盖 reroll 用户楼",
  "send-intent",
);
rerollInputHarness.result.freezeHostGenerationInputSnapshot(
  "错误的宿主快照不应覆盖 reroll 用户楼",
  "host-generation-lifecycle",
);

const rerollReplacementInput = rerollInputHarness.result.buildNormalGenerationRecallInput(
  rerollInputHarness.chat,
  {
    frozenInputSnapshot: rerollInputHarness.result.getPendingHostGenerationInputSnapshot(),
  },
);
assert.equal(
  rerollReplacementInput.overrideUserMessage,
  "重 roll replacement 应复用这一楼",
  "reroll replacement should ignore stale live input sources and bind to stable user floor",
);
assert.equal(rerollReplacementInput.overrideSource, "chat-last-user");
assert.equal(rerollReplacementInput.overrideReason, "reroll-user-floor-reuse");
assert.equal(
  rerollInputHarness.result.getPendingRerollRecallReuse(),
  null,
  "reroll reuse marker should be one-shot after binding recall input",
);

console.log("  ✓ reroll replacement normal input is forced to stable user-floor recall source");

const legacyUnboundReuseChat = [
  { is_user: true, mes: "旧记录没有绑定楼层" },
  { is_user: false, mes: "上一条回复。", is_system: false },
];
const legacyUnboundRecord = buildPersistedRecallRecord({
  injectionText: "注入:旧记录没有绑定楼层",
  selectedNodeIds: ["node-legacy-unbound"],
  recallInput: "历史旧输入",
  recallSource: "chat-last-user",
  hookName: "GENERATION_AFTER_COMMANDS",
  tokenEstimate: 5,
  manuallyEdited: false,
});
writePersistedRecallToUserMessage(legacyUnboundReuseChat, 0, legacyUnboundRecord);

let legacyUnboundRetrieveCalled = false;
const legacyUnboundRuntime = {
  ...rerollRuntime,
  getContext: () => ({ chat: legacyUnboundReuseChat, chatId: "chat-legacy-unbound" }),
  retrieve: async () => {
    legacyUnboundRetrieveCalled = true;
    return {
      injectionText: "不应出现的旧记录新召回",
      selectedNodeIds: ["node-new"],
    };
  },
};

const legacyUnboundResult = await runRecallController(legacyUnboundRuntime, {
  overrideUserMessage: "旧记录没有绑定楼层",
  generationType: "normal",
  targetUserMessageIndex: 0,
  overrideSource: "chat-last-user",
  hookName: "GENERATION_AFTER_COMMANDS",
  deliveryMode: "immediate",
});

assert.equal(
  legacyUnboundResult.reason,
  "persisted-user-floor-reused",
  "legacy unbound user-floor recall should be reused for normal-typed history generation",
);
assert.equal(
  legacyUnboundRetrieveCalled,
  false,
  "legacy unbound user-floor recall should not call retrieve",
);

console.log("  ✓ runRecallController reuses legacy unbound user-floor recall");

const activeInputUnboundChat = [
  { is_user: true, mes: "主动新输入不应复用旧召回" },
  { is_user: false, mes: "上一条回复。", is_system: false },
];
const activeInputUnboundRecord = buildPersistedRecallRecord({
  injectionText: "旧注入:主动新输入不应复用旧召回",
  selectedNodeIds: ["node-active-old"],
  recallInput: "旧输入",
  recallSource: "send-intent",
  hookName: "GENERATION_AFTER_COMMANDS",
  tokenEstimate: 5,
  manuallyEdited: false,
});
writePersistedRecallToUserMessage(activeInputUnboundChat, 0, activeInputUnboundRecord);

let activeInputRetrieveCalled = false;
const activeInputRuntime = {
  ...rerollRuntime,
  getContext: () => ({ chat: activeInputUnboundChat, chatId: "chat-active-input" }),
  retrieve: async () => {
    activeInputRetrieveCalled = true;
    return {
      injectionText: "新召回:主动新输入不应复用旧召回",
      selectedNodeIds: ["node-active-new"],
    };
  },
};

const activeInputResult = await runRecallController(activeInputRuntime, {
  overrideUserMessage: "主动新输入不应复用旧召回",
  generationType: "normal",
  targetUserMessageIndex: 0,
  overrideSource: "send-intent",
  hookName: "GENERATION_AFTER_COMMANDS",
  deliveryMode: "immediate",
});

assert.equal(
  activeInputRetrieveCalled,
  true,
  "active send-intent input should not reuse an unbound stale record",
);
assert.equal(
  activeInputResult.injectionText,
  "新召回:主动新输入不应复用旧召回",
  "active send-intent input should use the fresh recall result",
);

console.log("  ✓ runRecallController does not reuse unbound record for active input");

rerollRecoverHistoryCalled = false;
const activeInputBoundChat = [
  { is_user: true, mes: "主动新输入绑定记录也不应复用" },
  { is_user: false, mes: "上一条回复。", is_system: false },
];
const activeInputBoundRecord = buildPersistedRecallRecord({
  injectionText: "旧注入:主动新输入绑定记录也不应复用",
  selectedNodeIds: ["node-active-bound-old"],
  recallInput: "主动新输入绑定记录也不应复用",
  recallSource: "chat-last-user",
  hookName: "GENERATION_AFTER_COMMANDS",
  tokenEstimate: 5,
  manuallyEdited: false,
  boundUserFloorText: "主动新输入绑定记录也不应复用",
});
writePersistedRecallToUserMessage(activeInputBoundChat, 0, activeInputBoundRecord);

let activeInputBoundRetrieveCalled = false;
const activeInputBoundRuntime = {
  ...rerollRuntime,
  getContext: () => ({ chat: activeInputBoundChat, chatId: "chat-active-input-bound" }),
  retrieve: async () => {
    activeInputBoundRetrieveCalled = true;
    return {
      injectionText: "新召回:主动新输入绑定记录也不应复用",
      selectedNodeIds: ["node-active-bound-new"],
    };
  },
};

const activeInputBoundResult = await runRecallController(activeInputBoundRuntime, {
  overrideUserMessage: "主动新输入绑定记录也不应复用",
  generationType: "normal",
  targetUserMessageIndex: 0,
  overrideSource: "send-intent",
  hookName: "GENERATION_AFTER_COMMANDS",
  deliveryMode: "immediate",
});

assert.equal(
  activeInputBoundRetrieveCalled,
  true,
  "active send-intent input should not reuse even a bound target user-floor record",
);
assert.equal(
  rerollRecoverHistoryCalled,
  true,
  "active send-intent input should still run normal pre-recall history recovery before fresh recall",
);
assert.equal(
  activeInputBoundResult.injectionText,
  "新召回:主动新输入绑定记录也不应复用",
  "active send-intent input should force fresh recall for bound records",
);

console.log("  ✓ runRecallController does not reuse bound record for active input");

const mismatchedBoundChat = [
  { is_user: true, mes: "已经编辑过的新楼层" },
  { is_user: false, mes: "上一条回复。", is_system: false },
];
const mismatchedBoundRecord = buildPersistedRecallRecord({
  injectionText: "旧注入:已经编辑过的新楼层",
  selectedNodeIds: ["node-mismatch-old"],
  recallInput: "已经编辑过的新楼层",
  recallSource: "chat-last-user",
  hookName: "GENERATION_AFTER_COMMANDS",
  tokenEstimate: 5,
  manuallyEdited: false,
  boundUserFloorText: "编辑前的旧楼层",
});
writePersistedRecallToUserMessage(mismatchedBoundChat, 0, mismatchedBoundRecord);

let mismatchedBoundRetrieveCalled = false;
const mismatchedBoundRuntime = {
  ...rerollRuntime,
  getContext: () => ({ chat: mismatchedBoundChat, chatId: "chat-bound-mismatch" }),
  retrieve: async () => {
    mismatchedBoundRetrieveCalled = true;
    return {
      injectionText: "新召回:已经编辑过的新楼层",
      selectedNodeIds: ["node-mismatch-new"],
    };
  },
};

const mismatchedBoundResult = await runRecallController(mismatchedBoundRuntime, {
  overrideUserMessage: "已经编辑过的新楼层",
  generationType: "normal",
  targetUserMessageIndex: 0,
  overrideSource: "chat-last-user",
  hookName: "GENERATION_AFTER_COMMANDS",
  deliveryMode: "immediate",
});

assert.equal(
  mismatchedBoundRetrieveCalled,
  true,
  "bound user-floor mismatch should force a fresh recall",
);
assert.equal(
  mismatchedBoundResult.injectionText,
  "新召回:已经编辑过的新楼层",
  "bound user-floor mismatch should not reuse stale persisted recall",
);

console.log("  ✓ runRecallController does not reuse record when bound user floor mismatches");

// ═══════════════════════════════════════════════════════════════
// 4. runRecallController: regenerate with empty recallInput reuses user-floor record
// ═══════════════════════════════════════════════════════════════

const noReuseChat = [
  { is_user: true, mes: "去看星星" },
  { is_user: false, mes: "好的。", is_system: false },
];
const emptyInputRecord = buildPersistedRecallRecord({
  injectionText: "注入:去看星星",
  selectedNodeIds: ["node-c"],
  recallInput: "",
  recallSource: "chat-tail-user",
  hookName: "GENERATION_AFTER_COMMANDS",
  tokenEstimate: 3,
  manuallyEdited: false,
});
writePersistedRecallToUserMessage(noReuseChat, 0, emptyInputRecord);

let noReuseRetrieveCalled = false;
const noReuseRuntime = {
  ...rerollRuntime,
  getContext: () => ({ chat: noReuseChat, chatId: "chat-no-reuse" }),
  readPersistedRecallFromUserMessage,
  retrieve: async () => {
    noReuseRetrieveCalled = true;
    return {
      injectionText: "新召回结果",
      selectedNodeIds: ["node-d"],
    };
  },
  resolveRecallInput: (chat, limit, override) => ({
    userMessage: normalizeRecallInputText(
      override?.overrideUserMessage || "",
    ),
    generationType: String(override?.generationType || "normal"),
    targetUserMessageIndex: Number.isFinite(override?.targetUserMessageIndex)
      ? override.targetUserMessageIndex
      : null,
    source: override?.overrideSource || "chat-last-user",
    sourceLabel: override?.overrideSourceLabel || "",
    reason: "override-bound",
    authoritativeInputUsed: false,
    boundUserFloorText: "",
    recentMessages: [],
    hookName: override?.hookName || "",
    deliveryMode: "immediate",
  }),
};

const noReuseResult = await runRecallController(noReuseRuntime, {
  overrideUserMessage: "去看星星",
  generationType: "regenerate",
  targetUserMessageIndex: 0,
  overrideSource: "chat-last-user",
  hookName: "GENERATION_AFTER_COMMANDS",
  deliveryMode: "immediate",
});

assert.equal(noReuseResult.status, "completed", "no-reuse should complete");
assert.equal(
  noReuseRetrieveCalled,
  false,
  "retrieve() should NOT be called when target user floor has a persisted recall record",
);
assert.equal(
  noReuseResult.reason,
  "persisted-user-floor-reused",
  "empty recallInput legacy records should still be reused from the user floor",
);

console.log("  ✓ runRecallController reuses user-floor record with empty recallInput");

// ═══════════════════════════════════════════════════════════════
// 5. runRecallController: normal generation below an assistant reuses user-floor record
// ═══════════════════════════════════════════════════════════════

const assistantTailChat = [
  { is_user: true, mes: "今晚去海边看烟花" },
  { is_user: false, mes: "好，我会准备好相机。", is_system: false },
];
const assistantTailRecord = buildPersistedRecallRecord({
  injectionText: "注入:今晚去海边看烟花",
  selectedNodeIds: ["node-fireworks"],
  recallInput: "今晚去海边看烟花",
  recallSource: "chat-latest-user",
  hookName: "GENERATION_AFTER_COMMANDS",
  tokenEstimate: 4,
  manuallyEdited: false,
  boundUserFloorText: "今晚去海边看烟花",
});
writePersistedRecallToUserMessage(assistantTailChat, 0, assistantTailRecord);

let assistantTailRetrieveCalled = false;
const assistantTailRuntime = {
  ...rerollRuntime,
  getContext: () => ({ chat: assistantTailChat, chatId: "chat-assistant-tail" }),
  readPersistedRecallFromUserMessage,
  retrieve: async () => {
    assistantTailRetrieveCalled = true;
    return {
      injectionText: "fresh recall should not run",
      selectedNodeIds: ["node-fresh"],
    };
  },
  resolveRecallInput: (chat, limit, override) => ({
    userMessage: normalizeRecallInputText(
      override?.overrideUserMessage || override?.userMessage || "今晚去海边看烟花",
    ),
    generationType: String(override?.generationType || "normal"),
    targetUserMessageIndex: Number.isFinite(override?.targetUserMessageIndex)
      ? override.targetUserMessageIndex
      : null,
    source: override?.overrideSource || "chat-latest-user",
    sourceLabel: override?.overrideSourceLabel || "最近用户消息",
    reason: "assistant-tail-normal-generation",
    authoritativeInputUsed: Boolean(override?.authoritativeInputUsed),
    boundUserFloorText: normalizeRecallInputText(
      override?.boundUserFloorText || "今晚去海边看烟花",
    ),
    recentMessages: [],
    hookName: override?.hookName || "",
    deliveryMode: "immediate",
  }),
};

const assistantTailResult = await runRecallController(assistantTailRuntime, {
  overrideUserMessage: "今晚去海边看烟花",
  generationType: "normal",
  overrideSource: "chat-latest-user",
  hookName: "GENERATION_AFTER_COMMANDS",
  deliveryMode: "immediate",
});

assert.equal(assistantTailResult.status, "completed");
assert.equal(
  assistantTailRetrieveCalled,
  false,
  "normal generation below an assistant should find and reuse the matching user-floor persisted recall",
);
assert.equal(assistantTailResult.reason, "persisted-user-floor-reused");

console.log("  ✓ runRecallController reuses user-floor record below assistant tail");
console.log("recall-reroll-reuse tests passed");
