import assert from "node:assert/strict";
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import vm from "node:vm";
import { onManualExtractController } from "../maintenance/extraction-controller.js";
import { onRebuildController } from "../ui/ui-actions-controller.js";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const indexPath = path.resolve(__dirname, "../index.js");
const indexSource = await fs.readFile(indexPath, "utf8");

function extractSnippet(startMarker, endMarker) {
  const start = indexSource.indexOf(startMarker);
  const end = indexSource.indexOf(endMarker);
  if (start < 0 || end < 0 || end <= start) {
    throw new Error(`无法提取 index.js 片段: ${startMarker} -> ${endMarker}`);
  }
  return indexSource.slice(start, end).replace(/^export\s+/gm, "");
}

const statusSnippet = extractSnippet(
  "function setRuntimeStatus(",
  "function notifyExtractionIssue(",
);
const vectorSnippet = extractSnippet(
  "async function syncVectorState({",
  "async function ensureVectorReadyIfNeeded(",
);
const manualExtractSnippet = extractSnippet(
  "async function onManualExtract(options = {}) {",
  "async function onReroll(",
);
const rebuildSnippet = extractSnippet(
  "async function onRebuild() {",
  "async function onManualCompress() {",
);

function createBaseStatusContext() {
  return {
    console,
    Date,
    createUiStatus(text = "待命", meta = "", level = "idle") {
      return {
        text: String(text || "待命"),
        meta: String(meta || ""),
        level,
        updatedAt: Date.now(),
      };
    },
    runtimeStatus: { text: "待命", meta: "", level: "idle" },
    lastExtractionStatus: { text: "待命", meta: "", level: "idle" },
    lastVectorStatus: { text: "待命", meta: "", level: "idle" },
    lastRecallStatus: { text: "待命", meta: "", level: "idle" },
    lastStatusToastAt: {},
    STATUS_TOAST_THROTTLE_MS: 1500,
    getContext() {
      return {};
    },
    resolveOperationalChatId(context, graph, explicitChatId = "") {
      // VM snippet calls this as a free function (no `this`); derive only from
      // arguments so it never depends on per-test getCurrentChatId closures.
      return (
        String(explicitChatId || "").trim() ||
        String(graph?.historyState?.chatId || "").trim() ||
        "chat-mobile"
      );
    },
    _panelModule: {
      updateFloatingBallStatus() {},
    },
    refreshPanelLiveState() {},
    updateStageNotice() {},
    notifyStatusToast() {},
    toastr: {
      info() {},
      success() {},
      warning() {},
      error() {},
    },
  };
}

function testIndexDefinesLastProcessedAssistantFloorHelper() {
  assert.match(
    indexSource,
    /function\s+getLastProcessedAssistantFloor\s*\(/,
  );
}

async function testVectorSyncTerminalStateUpdatesRuntime() {
  const context = {
    ...createBaseStatusContext(),
    currentGraph: {
      vectorIndexState: {
        dirty: true,
        lastWarning: "",
      },
    },
    ensureCurrentGraphRuntimeState() {
      return context.currentGraph;
    },
    getEmbeddingConfig() {
      return { mode: "direct" };
    },
    validateVectorConfig() {
      return { valid: true };
    },
    async syncGraphVectorIndex() {
      return {
        insertedHashes: [],
        stats: {
          indexed: 12,
          pending: 0,
        },
      };
    },
    getCurrentChatId() {
      return "chat-mobile";
    },
    getVectorIndexStats() {
      return { indexed: 12, pending: 0 };
    },
    isAbortError() {
      return false;
    },
    markVectorStateDirty() {},
    result: null,
  };
  vm.createContext(context);
  vm.runInContext(
    `${statusSnippet}\n${vectorSnippet}\nresult = { syncVectorState };`,
    context,
    { filename: indexPath },
  );

  const result = await context.result.syncVectorState({ force: true });
  assert.equal(result.stats.indexed, 12);
  assert.equal(context.lastVectorStatus.text, "向量完成");
  assert.equal(context.runtimeStatus.text, "向量完成");
  assert.equal(context.runtimeStatus.level, "success");
}

async function testManualExtractNoBatchesDoesNotStayRunning() {
  let assistantTurnCallCount = 0;
  const chat = [{ is_user: true, mes: "u" }, { is_user: false, mes: "a" }];
  const context = {
    ...createBaseStatusContext(),
    isExtracting: false,
    currentGraph: {},
    graphPersistenceState: {
      pendingPersist: false,
    },
    getCurrentChatId() {
      return "chat-mobile";
    },
    getGraphPersistenceState() {
      return { pendingPersist: false };
    },
    ensureGraphMutationReady() {
      return true;
    },
    async recoverHistoryIfNeeded() {
      return true;
    },
    normalizeGraphRuntimeState(graph) {
      return graph;
    },
    createEmptyGraph() {
      return {};
    },
    getContext() {
      return { chat };
    },
    getAssistantTurns() {
      assistantTurnCallCount += 1;
      return assistantTurnCallCount === 1 ? [1] : [];
    },
    getLastProcessedAssistantFloor() {
      return 0;
    },
    clampInt(value, fallback) {
      return Number.isFinite(Number(value)) ? Number(value) : fallback;
    },
    getSettings() {
      return { extractEvery: 1 };
    },
    beginStageAbortController() {
      return { signal: {} };
    },
    async executeExtractionBatch() {
      throw new Error("不应进入批次执行");
    },
    async retryPendingGraphPersist() {
      return {
        accepted: false,
        reason: "no-pending-persist",
      };
    },
    isAbortError() {
      return false;
    },
    onManualExtractController,
    finishStageAbortController() {},
    result: null,
  };
  vm.createContext(context);
  vm.runInContext(
    `${statusSnippet}\n${manualExtractSnippet}\nresult = { onManualExtract };`,
    context,
    { filename: indexPath },
  );

  await context.result.onManualExtract();
  assert.equal(context.isExtracting, false);
  assert.equal(context.lastExtractionStatus.text, "无待提取内容");
  assert.equal(context.runtimeStatus.text, "无待提取内容");
  assert.notEqual(context.runtimeStatus.level, "running");
}

async function testManualExtractIgnoresSupersededPendingPersistence() {
  let executeExtractionBatchCalls = 0;
  let assistantTurnCallCount = 0;
  const chat = [{ is_user: true, mes: "u" }, { is_user: false, mes: "a" }];
  const context = {
    ...createBaseStatusContext(),
    isExtracting: false,
    graphPersistenceState: {
      pendingPersist: false,
      lastAcceptedRevision: 7,
    },
    currentGraph: {
      historyState: {
        lastBatchStatus: {
          processedRange: [1, 1],
          persistence: {
            outcome: "queued",
            accepted: false,
            revision: 7,
            reason: "extraction-batch-complete:pending",
            storageTier: "none",
          },
        },
      },
    },
    getCurrentChatId() {
      return "chat-mobile";
    },
    getCurrentGraph() {
      return context.currentGraph;
    },
    getIsExtracting() {
      return context.isExtracting;
    },
    getGraphPersistenceState() {
      return {
        pendingPersist: false,
        lastAcceptedRevision: 7,
      };
    },
    ensureGraphMutationReady() {
      return true;
    },
    async recoverHistoryIfNeeded() {
      return true;
    },
    normalizeGraphRuntimeState(graph) {
      return graph;
    },
    setCurrentGraph(graph) {
      context.currentGraph = graph;
    },
    createEmptyGraph() {
      return {};
    },
    getContext() {
      return { chat };
    },
    getAssistantTurns() {
      assistantTurnCallCount += 1;
      return assistantTurnCallCount <= 2 ? [1] : [];
    },
    getLastProcessedAssistantFloor() {
      return 0;
    },
    clampInt(value, fallback) {
      return Number.isFinite(Number(value)) ? Number(value) : fallback;
    },
    getSettings() {
      return { extractEvery: 1 };
    },
    beginStageAbortController() {
      return { signal: {} };
    },
    async executeExtractionBatch() {
      executeExtractionBatchCalls += 1;
      return {
        success: true,
        result: {
          newNodes: 0,
          updatedNodes: 0,
          newEdges: 0,
        },
        effects: {},
        batchStatus: {
          persistence: {
            accepted: true,
          },
        },
        historyAdvanceAllowed: true,
      };
    },
    async retryPendingGraphPersist() {
      return {
        accepted: false,
        reason: "no-pending-persist",
      };
    },
    isAbortError() {
      return false;
    },
    onManualExtractController,
    finishStageAbortController() {},
    setIsExtracting(value) {
      context.isExtracting = value;
    },
    setLastExtractionStatus(text, meta, level) {
      context.lastExtractionStatus = { text, meta, level };
      context.runtimeStatus = { text, meta, level };
    },
    toastr: {
      info() {},
      success() {},
      warning() {},
      error() {},
    },
    result: null,
  };
  await onManualExtractController(context, { drainAll: false });
  assert.equal(executeExtractionBatchCalls, 1);
  assert.notEqual(context.lastExtractionStatus.text, "等待持久化确认");
}

async function testManualExtractContinuesWithRecoverablePendingPersistence() {
  let executeExtractionBatchCalls = 0;
  let assistantTurnCallCount = 0;
  const chat = [{ is_user: true, mes: "u" }, { is_user: false, mes: "a" }];
  const context = {
    ...createBaseStatusContext(),
    isExtracting: false,
    graphPersistenceState: {
      pendingPersist: true,
      lastAcceptedRevision: 0,
      queuedPersistRevision: 7,
      shadowSnapshotRevision: 7,
      lastRecoverableStorageTier: "shadow",
    },
    currentGraph: {
      historyState: {
        lastBatchStatus: {
          processedRange: [1, 1],
          persistence: {
            outcome: "queued",
            accepted: false,
            revision: 7,
            reason: "extraction-batch-complete:pending",
            storageTier: "shadow",
          },
        },
      },
    },
    getCurrentChatId() {
      return "chat-mobile";
    },
    getCurrentGraph() {
      return context.currentGraph;
    },
    getIsExtracting() {
      return context.isExtracting;
    },
    getGraphPersistenceState() {
      return {
        pendingPersist: true,
        lastAcceptedRevision: 0,
        queuedPersistRevision: 7,
        shadowSnapshotRevision: 7,
        lastRecoverableStorageTier: "shadow",
      };
    },
    ensureGraphMutationReady() {
      return true;
    },
    async recoverHistoryIfNeeded() {
      return true;
    },
    normalizeGraphRuntimeState(graph) {
      return graph;
    },
    setCurrentGraph(graph) {
      context.currentGraph = graph;
    },
    createEmptyGraph() {
      return {};
    },
    getContext() {
      return { chat };
    },
    getAssistantTurns() {
      assistantTurnCallCount += 1;
      return assistantTurnCallCount <= 2 ? [1] : [];
    },
    getLastProcessedAssistantFloor() {
      return 0;
    },
    clampInt(value, fallback) {
      return Number.isFinite(Number(value)) ? Number(value) : fallback;
    },
    getSettings() {
      return { extractEvery: 1 };
    },
    beginStageAbortController() {
      return { signal: {} };
    },
    async executeExtractionBatch() {
      executeExtractionBatchCalls += 1;
      return {
        success: true,
        result: {
          newNodes: 0,
          updatedNodes: 0,
          newEdges: 0,
        },
        effects: {},
        batchStatus: {
          persistence: {
            accepted: true,
          },
        },
        historyAdvanceAllowed: true,
      };
    },
    async retryPendingGraphPersist() {
      return {
        accepted: false,
        reason: "shadow-still-pending",
      };
    },
    isAbortError() {
      return false;
    },
    onManualExtractController,
    finishStageAbortController() {},
    setIsExtracting(value) {
      context.isExtracting = value;
    },
    setLastExtractionStatus(text, meta, level) {
      context.lastExtractionStatus = { text, meta, level };
      context.runtimeStatus = { text, meta, level };
    },
    toastr: {
      info() {},
      success() {},
      warning() {},
      error() {},
    },
    result: null,
  };
  await onManualExtractController(context, { drainAll: false });
  assert.equal(executeExtractionBatchCalls, 1);
  assert.notEqual(context.lastExtractionStatus.text, "等待持久化确认");
}

async function testManualExtractIgnoresFailedBatchWithoutPersistenceAttempt() {
  let executeExtractionBatchCalls = 0;
  const chat = [{ is_user: true, mes: "u" }, { is_user: false, mes: "a" }];
  const context = {
    ...createBaseStatusContext(),
    isExtracting: false,
    graphPersistenceState: {
      pendingPersist: false,
      lastAcceptedRevision: 0,
    },
    currentGraph: {
      historyState: {
        lastBatchStatus: {
          outcome: "failed",
          processedRange: [1, 1],
          persistence: {
            outcome: "queued",
            accepted: false,
            revision: 0,
            reason: "",
            storageTier: "none",
          },
        },
      },
    },
    getCurrentChatId() {
      return "chat-mobile";
    },
    getCurrentGraph() {
      return context.currentGraph;
    },
    getIsExtracting() {
      return context.isExtracting;
    },
    getGraphPersistenceState() {
      return {
        pendingPersist: false,
        lastAcceptedRevision: 0,
      };
    },
    ensureGraphMutationReady() {
      return true;
    },
    async recoverHistoryIfNeeded() {
      return true;
    },
    normalizeGraphRuntimeState(graph) {
      return graph;
    },
    setCurrentGraph(graph) {
      context.currentGraph = graph;
    },
    createEmptyGraph() {
      return {};
    },
    getContext() {
      return { chat };
    },
    getAssistantTurns() {
      return [1];
    },
    getLastProcessedAssistantFloor() {
      return 0;
    },
    clampInt(value, fallback) {
      return Number.isFinite(Number(value)) ? Number(value) : fallback;
    },
    getSettings() {
      return { extractEvery: 1 };
    },
    beginStageAbortController() {
      return { signal: {} };
    },
    async executeExtractionBatch() {
      executeExtractionBatchCalls += 1;
      return {
        success: true,
        result: {
          newNodes: 0,
          updatedNodes: 0,
          newEdges: 0,
        },
        effects: {},
        batchStatus: {
          persistence: {
            accepted: true,
            revision: 1,
            attempted: true,
          },
        },
        historyAdvanceAllowed: true,
      };
    },
    async retryPendingGraphPersist() {
      return {
        accepted: false,
        reason: "no-pending-persist",
      };
    },
    isAbortError() {
      return false;
    },
    onManualExtractController,
    finishStageAbortController() {},
    setIsExtracting(value) {
      context.isExtracting = value;
    },
    setLastExtractionStatus(text, meta, level) {
      context.lastExtractionStatus = { text, meta, level };
      context.runtimeStatus = { text, meta, level };
    },
    toastr: {
      info() {},
      success() {},
      warning() {},
      error() {},
    },
    result: null,
  };

  await onManualExtractController(context, { drainAll: false });
  assert.equal(executeExtractionBatchCalls, 1);
  assert.notEqual(context.lastExtractionStatus.text, "等待持久化确认");
}

async function testManualRebuildSetsTerminalRuntimeStatus() {
  const chat = [{ is_user: true, mes: "u" }, { is_user: false, mes: "a" }];
  let savedHashes = null;
  let savedNeedRefresh = null;
  const context = {
    ...createBaseStatusContext(),
    __confirmHost: true,
    currentGraph: {
      historyState: {
        lastProcessedAssistantFloor: -1,
        processedMessageHashes: {},
        processedMessageHashesNeedRefresh: false,
      },
      vectorIndexState: {
        lastWarning: "",
      },
      batchJournal: [],
    },
    confirm() {
      assert.equal(this?.__confirmHost, true);
      return true;
    },
    ensureGraphMutationReady() {
      return true;
    },
    getContext() {
      return { chat };
    },
    cloneGraphSnapshot(graph) {
      return graph;
    },
    snapshotRuntimeUiState() {
      return {};
    },
    getSettings() {
      return {};
    },
    normalizeGraphRuntimeState(graph) {
      return graph;
    },
    createEmptyGraph() {
      return {
        historyState: {
          lastProcessedAssistantFloor: -1,
          processedMessageHashes: {},
          processedMessageHashesNeedRefresh: false,
        },
        vectorIndexState: {
          lastWarning: "",
        },
        batchJournal: [],
      };
    },
    getCurrentChatId() {
      return "chat-mobile";
    },
    clearInjectionState() {},
    async prepareVectorStateForReplay() {},
    async replayExtractionFromHistory() {
      context.currentGraph.historyState.lastProcessedAssistantFloor = 1;
      context.currentGraph.vectorIndexState.lastWarning = "";
      return 2;
    },
    clearHistoryDirty(graph) {
      graph.historyState.processedMessageHashes = {};
      graph.historyState.processedMessageHashesNeedRefresh = true;
    },
    buildRecoveryResult(status, extra = {}) {
      return { status, ...extra };
    },
    updateProcessedHistorySnapshot(chatInput, floor) {
      context.currentGraph.historyState.lastProcessedAssistantFloor = floor;
      context.currentGraph.historyState.processedMessageHashes = {};
      for (let index = 0; index <= floor; index += 1) {
        context.currentGraph.historyState.processedMessageHashes[index] =
          String(chatInput[index]?.mes || "");
      }
      context.currentGraph.historyState.processedMessageHashesNeedRefresh = false;
    },
    saveGraphToChat() {
      savedHashes = { ...context.currentGraph.historyState.processedMessageHashes };
      savedNeedRefresh =
        context.currentGraph.historyState.processedMessageHashesNeedRefresh;
    },
    restoreRuntimeUiState() {},
    async runWithRestoreLock(_source, _reason, task) {
      return await task();
    },
    onRebuildController,
    result: null,
  };
  vm.createContext(context);
  vm.runInContext(
    `${statusSnippet}\n${rebuildSnippet}\nresult = { onRebuild };`,
    context,
    { filename: indexPath },
  );

  await context.result.onRebuild();
  assert.equal(context.lastExtractionStatus.text, "图谱重建完成");
  assert.equal(context.runtimeStatus.text, "图谱重建完成");
  assert.equal(context.runtimeStatus.level, "success");
  assert.deepEqual(savedHashes, {
    0: "u",
    1: "a",
  });
  assert.equal(savedNeedRefresh, false);
}

testIndexDefinesLastProcessedAssistantFloorHelper();
await testVectorSyncTerminalStateUpdatesRuntime();
await testManualExtractNoBatchesDoesNotStayRunning();
await testManualExtractIgnoresSupersededPendingPersistence();
await testManualExtractContinuesWithRecoverablePendingPersistence();
await testManualExtractIgnoresFailedBatchWithoutPersistenceAttempt();
await testManualRebuildSetsTerminalRuntimeStatus();

console.log("mobile-status-regressions tests passed");
