import assert from "node:assert/strict";

import {
  BME_DB_SCHEMA_VERSION,
  BME_RUNTIME_BATCH_JOURNAL_META_KEY,
  BME_RUNTIME_HISTORY_META_KEY,
  BME_RUNTIME_RECORDS_NORMALIZED_META_KEY,
  BME_RUNTIME_VECTOR_META_KEY,
  BME_TOMBSTONE_RETENTION_MS,
  BmeDatabase,
  buildBmeDbName,
  buildGraphFromSnapshot,
  buildSnapshotFromGraph,
  ensureDexieLoaded,
} from "../sync/bme-db.js";
import { BmeChatManager } from "../sync/bme-chat-manager.js";
import { createEmptyGraph } from "../graph/graph.js";
import { getGraphPersistDirtyStateSnapshot } from "../runtime/runtime-state.js";

const PREFIX = "[ST-BME][indexeddb-persistence]";

const chatIdsForCleanup = new Set([
  "chat-a",
  "chat-b",
  "chat-manager-a",
  "chat-manager-b",
  "chat-manager-selector",
  "chat-export-without-tombstones",
  "chat-replace-reset",
]);

async function setupIndexedDbTestEnv() {
  let fakeIndexedDbLoaded = false;

  try {
    await import("fake-indexeddb/auto");
    fakeIndexedDbLoaded = true;
  } catch (error) {
    console.warn(
      `${PREFIX} fake-indexeddb 未安装，回退到当前运行时 indexedDB:`,
      error?.message || error,
    );
  }

  if (!globalThis.Dexie) {
    try {
      const imported = await import("dexie");
      globalThis.Dexie = imported?.default || imported?.Dexie || imported;
    } catch {
      await import("../lib/dexie.min.js");
    }
  }

  await ensureDexieLoaded();

  assert.equal(typeof globalThis.Dexie, "function", "Dexie 构造函数必须可用");
  assert.ok(globalThis.indexedDB, "indexedDB 必须可用");
  assert.ok(globalThis.IDBKeyRange, "IDBKeyRange 必须可用");

  return { fakeIndexedDbLoaded };
}

async function cleanupDatabases() {
  if (typeof globalThis.Dexie?.delete !== "function") return;

  for (const chatId of chatIdsForCleanup) {
    try {
      await globalThis.Dexie.delete(buildBmeDbName(chatId));
    } catch {
      // ignore
    }
  }
}

async function testBuildAndOpen() {
  assert.equal(buildBmeDbName("chat-a"), "STBME_chat-a");

  const db = new BmeDatabase("chat-a", { dexieClass: globalThis.Dexie });
  await db.open();

  const tableNames = db.db.tables.map((table) => table.name).sort();
  assert.deepEqual(tableNames, ["edges", "meta", "nodes", "tombstones"]);

  const schemaVersion = await db.getMeta("schemaVersion", 0);
  assert.equal(schemaVersion, BME_DB_SCHEMA_VERSION);

  await db.close();
}

async function testCrudAndMeta() {
  const db = new BmeDatabase("chat-a", { dexieClass: globalThis.Dexie });
  await db.open();

  const nodeResult = await db.bulkUpsertNodes([
    {
      id: "node-1",
      type: "event",
      sourceFloor: 1,
      archived: false,
      updatedAt: Date.now(),
      fields: {
        title: "第一次相遇",
      },
    },
  ]);
  assert.equal(nodeResult.upserted, 1);

  const edgeResult = await db.bulkUpsertEdges([
    {
      id: "edge-1",
      fromId: "node-1",
      toId: "node-1",
      relation: "self",
      sourceFloor: 1,
      updatedAt: Date.now(),
    },
  ]);
  assert.equal(edgeResult.upserted, 1);

  await db.setMeta("lastProcessedFloor", 7);
  assert.equal(await db.getMeta("lastProcessedFloor", -1), 7);

  await db.patchMeta({
    extractionCount: 3,
    deviceId: "device-test",
  });
  assert.equal(await db.getMeta("extractionCount", 0), 3);
  assert.equal(await db.getMeta("deviceId", ""), "device-test");

  const nodes = await db.listNodes({ includeDeleted: false, reverse: false });
  const edges = await db.listEdges({ includeDeleted: false, reverse: false });

  assert.equal(nodes.length, 1);
  assert.equal(edges.length, 1);

  await db.close();
}

async function testTransactionRollback() {
  const db = new BmeDatabase("chat-a", { dexieClass: globalThis.Dexie });
  await db.open();

  await assert.rejects(async () => {
    await db.db.transaction("rw", db.db.table("nodes"), async () => {
      await db.db.table("nodes").put({
        id: "node-rollback",
        type: "event",
        sourceFloor: 9,
        updatedAt: Date.now(),
      });
      throw new Error("simulate rollback");
    });
  });

  const rollbackNode = await db.db.table("nodes").get("node-rollback");
  assert.equal(rollbackNode, undefined);

  await db.close();
}

async function testSnapshotExportImport() {
  const db = new BmeDatabase("chat-a", { dexieClass: globalThis.Dexie });
  await db.open();

  await db.bulkUpsertNodes([
    {
      id: "node-snapshot",
      type: "event",
      sourceFloor: 2,
      archived: false,
      updatedAt: Date.now(),
    },
  ]);
  await db.bulkUpsertEdges([
    {
      id: "edge-snapshot",
      fromId: "node-snapshot",
      toId: "node-1",
      relation: "related",
      sourceFloor: 2,
      updatedAt: Date.now(),
    },
  ]);

  const exported = await db.exportSnapshot();
  assert.ok(exported.meta);
  assert.ok(Array.isArray(exported.nodes));
  assert.ok(Array.isArray(exported.edges));

  await db.clearAll();
  assert.equal((await db.listNodes()).length, 0);

  const importResult = await db.importSnapshot(exported, {
    mode: "replace",
    preserveRevision: true,
  });

  assert.equal(importResult.mode, "replace");
  assert.ok(importResult.imported.nodes >= 1);
  assert.ok((await db.listNodes()).some((item) => item.id === "node-snapshot"));

  await db.close();
}

async function testSnapshotExportWithoutTombstones() {
  const db = new BmeDatabase("chat-export-without-tombstones", {
    dexieClass: globalThis.Dexie,
  });
  await db.open();

  await db.bulkUpsertNodes([
    {
      id: "node-light-snapshot",
      type: "event",
      sourceFloor: 3,
      archived: false,
      updatedAt: Date.now(),
    },
  ]);
  await db.bulkUpsertTombstones([
    {
      id: "tomb-light-snapshot",
      kind: "node",
      targetId: "node-deleted-light-snapshot",
      deletedAt: Date.now(),
      sourceDeviceId: "device-light-snapshot",
    },
  ]);

  const exported = await db.exportSnapshot({ includeTombstones: false });
  assert.equal(exported.__stBmeTombstonesOmitted, true);
  assert.ok(Array.isArray(exported.nodes));
  assert.ok(Array.isArray(exported.edges));
  assert.deepEqual(exported.tombstones, []);
  assert.equal(exported.meta.tombstoneCount, 1);

  await db.close();
}

async function testSnapshotProbeExport() {
  const db = new BmeDatabase("chat-export-probe", {
    dexieClass: globalThis.Dexie,
  });
  await db.open();

  await db.bulkUpsertNodes([
    {
      id: "node-probe",
      type: "event",
      sourceFloor: 4,
      archived: false,
      updatedAt: Date.now(),
    },
  ]);
  await db.patchMeta({
    lastProcessedFloor: 6,
    extractionCount: 3,
    runtimeHistoryState: {
      chatId: "chat-export-probe",
      lastProcessedAssistantFloor: 6,
      extractionCount: 3,
    },
  });

  const probe = await db.exportSnapshotProbe();
  assert.equal(probe.__stBmeProbeOnly, true);
  assert.equal(probe.__stBmeTombstonesOmitted, true);
  assert.deepEqual(probe.nodes, []);
  assert.deepEqual(probe.edges, []);
  assert.deepEqual(probe.tombstones, []);
  assert.equal(probe.meta.chatId, "chat-export-probe");
  assert.equal(probe.meta.nodeCount, 1);
  assert.equal(probe.state.lastProcessedFloor, 6);
  assert.equal(probe.state.extractionCount, 3);
  assert.equal(
    probe.meta.runtimeHistoryState.lastProcessedAssistantFloor,
    6,
  );

  await db.close();
}

async function testReplaceImportResetsStaleMeta() {
  const chatId = "chat-replace-reset";
  const db = new BmeDatabase(chatId, { dexieClass: globalThis.Dexie });
  await db.open();

  await db.patchMeta({
    runtimeHistoryState: {
      chatId,
      lastProcessedAssistantFloor: 99,
      processedMessageHashes: {
        99: "stale-hash",
      },
    },
    runtimeVectorIndexState: {
      hashToNodeId: {
        "stale-hash": "node-stale",
      },
      nodeToHash: {
        "node-stale": "stale-hash",
      },
      dirty: true,
      pendingRepairFromFloor: 88,
    },
    runtimeBatchJournal: [{ id: "stale-journal", processedRange: [90, 99] }],
    runtimeLastRecallResult: { updatedAt: 123456, nodes: ["node-stale"] },
    runtimeLastProcessedSeq: 999,
    runtimeGraphVersion: 999,
    migrationCompletedAt: 987654321,
    legacyRetentionUntil: 987654321,
    customLeakField: "stale-value",
  });

  const revisionBefore = await db.getRevision();

  const importResult = await db.importSnapshot(
    {
      meta: {
        chatId,
        revision: 1,
        deviceId: "device-replace-new",
      },
      state: {
        lastProcessedFloor: 3,
        extractionCount: 2,
      },
      nodes: [],
      edges: [],
      tombstones: [],
    },
    {
      mode: "replace",
      preserveRevision: true,
      markSyncDirty: false,
    },
  );

  assert.ok(importResult.revision > revisionBefore, "replace 导入后 revision 必须单调递增");
  assert.equal(await db.getMeta("chatId", ""), chatId);
  assert.equal(await db.getMeta("lastProcessedFloor", -1), 3);
  assert.equal(await db.getMeta("extractionCount", 0), 2);
  assert.equal(await db.getMeta("deviceId", ""), "device-replace-new");
  assert.equal(await db.getMeta("migrationCompletedAt", -1), 0);
  assert.equal(await db.getMeta("legacyRetentionUntil", -1), 0);
  assert.equal(await db.getMeta("runtimeHistoryState", "__missing__"), "__missing__");
  assert.equal(await db.getMeta("runtimeVectorIndexState", "__missing__"), "__missing__");
  assert.equal(await db.getMeta("runtimeBatchJournal", "__missing__"), "__missing__");
  assert.equal(await db.getMeta("runtimeLastRecallResult", "__missing__"), "__missing__");
  assert.equal(await db.getMeta("runtimeLastProcessedSeq", "__missing__"), "__missing__");
  assert.equal(await db.getMeta("runtimeGraphVersion", "__missing__"), "__missing__");
  assert.equal(await db.getMeta("customLeakField", "__missing__"), "__missing__");
  assert.equal(await db.getMeta("syncDirty", true), false);

  await db.close();
}

async function testRevisionMonotonicity() {
  const db = new BmeDatabase("chat-a", { dexieClass: globalThis.Dexie });
  await db.open();

  const revisionBefore = await db.getRevision();

  const afterNode = await db.bulkUpsertNodes([
    {
      id: "node-rev-1",
      type: "event",
      sourceFloor: 3,
      archived: false,
      updatedAt: Date.now(),
    },
  ]);

  const afterEdge = await db.bulkUpsertEdges([
    {
      id: "edge-rev-1",
      fromId: "node-rev-1",
      toId: "node-snapshot",
      relation: "next",
      sourceFloor: 3,
      updatedAt: Date.now(),
    },
  ]);

  assert.ok(afterNode.revision > revisionBefore);
  assert.ok(afterEdge.revision > afterNode.revision);

  await db.close();
}

async function testTombstonePrune() {
  const db = new BmeDatabase("chat-a", { dexieClass: globalThis.Dexie });
  await db.open();

  const nowMs = Date.now();
  const oldDeletedAt = nowMs - BME_TOMBSTONE_RETENTION_MS - 1000;
  const freshDeletedAt = nowMs - 1000;

  await db.bulkUpsertTombstones([
    {
      id: "tomb-old",
      kind: "node",
      targetId: "node-old",
      deletedAt: oldDeletedAt,
      sourceDeviceId: "device-a",
    },
    {
      id: "tomb-fresh",
      kind: "node",
      targetId: "node-fresh",
      deletedAt: freshDeletedAt,
      sourceDeviceId: "device-a",
    },
  ]);

  const pruneResult = await db.pruneExpiredTombstones(nowMs);
  assert.equal(pruneResult.pruned, 1);

  const tombstones = await db.listTombstones({ reverse: false });
  assert.equal(tombstones.length, 1);
  assert.equal(tombstones[0].id, "tomb-fresh");

  await db.close();
}

async function testChatIsolationAndManager() {
  const dbA = new BmeDatabase("chat-a", { dexieClass: globalThis.Dexie });
  const dbB = new BmeDatabase("chat-b", { dexieClass: globalThis.Dexie });

  await dbA.open();
  await dbB.open();

  await dbA.bulkUpsertNodes([
    {
      id: "node-chat-a",
      type: "event",
      sourceFloor: 1,
      archived: false,
      updatedAt: Date.now(),
    },
  ]);

  await dbB.bulkUpsertNodes([
    {
      id: "node-chat-b",
      type: "event",
      sourceFloor: 1,
      archived: false,
      updatedAt: Date.now(),
    },
  ]);

  const nodesA = await dbA.listNodes({ reverse: false });
  const nodesB = await dbB.listNodes({ reverse: false });

  assert.ok(nodesA.some((item) => item.id === "node-chat-a"));
  assert.ok(!nodesA.some((item) => item.id === "node-chat-b"));
  assert.ok(nodesB.some((item) => item.id === "node-chat-b"));

  await dbA.close();
  await dbB.close();

  const manager = new BmeChatManager({
    databaseFactory: (chatId) => {
      chatIdsForCleanup.add(chatId);
      return new BmeDatabase(chatId, { dexieClass: globalThis.Dexie });
    },
  });

  const managerDbA = await manager.switchChat("chat-manager-a");
  assert.equal(manager.getCurrentChatId(), "chat-manager-a");
  assert.ok(managerDbA);

  await managerDbA.bulkUpsertNodes([
    {
      id: "manager-node-a",
      type: "event",
      sourceFloor: 1,
      updatedAt: Date.now(),
    },
  ]);

  const managerDbB = await manager.switchChat("chat-manager-b");
  assert.equal(manager.getCurrentChatId(), "chat-manager-b");
  await managerDbB.bulkUpsertNodes([
    {
      id: "manager-node-b",
      type: "event",
      sourceFloor: 1,
      updatedAt: Date.now(),
    },
  ]);

  const managerDbBNodes = await managerDbB.listNodes({ reverse: false });
  assert.ok(managerDbBNodes.some((item) => item.id === "manager-node-b"));

  const reopenedA = await manager.getCurrentDb("chat-manager-a");
  const reopenedANodes = await reopenedA.listNodes({ reverse: false });
  assert.ok(reopenedANodes.some((item) => item.id === "manager-node-a"));
  assert.ok(!reopenedANodes.some((item) => item.id === "manager-node-b"));

  await manager.closeAll();
  assert.equal(manager.getCurrentChatId(), "");
}

async function testManagerRecreatesDbWhenSelectorKeyChanges() {
  let selectorKey = "indexeddb:indexeddb";
  let instanceCounter = 0;
  const closeLog = [];
  const manager = new BmeChatManager({
    selectorKeyResolver: async () => selectorKey,
    databaseFactory: async (chatId) => {
      instanceCounter += 1;
      const instanceId = instanceCounter;
      return {
        chatId,
        instanceId,
        openCount: 0,
        closed: false,
        async open() {
          this.openCount += 1;
          return this;
        },
        async close() {
          this.closed = true;
          closeLog.push(instanceId);
        },
      };
    },
  });

  const dbA = await manager.getCurrentDb("chat-manager-selector");
  assert.equal(dbA.instanceId, 1);
  assert.equal(dbA.openCount, 1);

  const reopenedSameSelector = await manager.getCurrentDb("chat-manager-selector");
  assert.equal(reopenedSameSelector, dbA);
  assert.equal(dbA.openCount, 2);
  assert.deepEqual(closeLog, []);

  selectorKey = "opfs:opfs-shadow";
  const dbB = await manager.getCurrentDb("chat-manager-selector");
  assert.notEqual(dbB, dbA);
  assert.equal(dbB.instanceId, 2);
  assert.equal(dbB.openCount, 1);
  assert.equal(dbA.closed, true);
  assert.deepEqual(closeLog, [1]);

  await manager.closeAll();
  assert.equal(dbB.closed, true);
  assert.deepEqual(closeLog, [1, 2]);
}

async function testGraphSnapshotConverters() {
  const graph = createEmptyGraph();
  graph.historyState.chatId = "chat-a";
  graph.historyState.lastProcessedAssistantFloor = 9;
  graph.historyState.extractionCount = 4;
  graph.historyState.processedMessageHashes = {
    1: "hash-1",
  };
  graph.vectorIndexState.hashToNodeId = {
    "vec-hash": "node-converter",
  };
  graph.lastRecallResult = ["node-converter"];
  graph.batchJournal = [
    {
      id: "journal-1",
      processedRange: [8, 9],
    },
  ];
  graph.maintenanceJournal = [
    {
      id: "maintenance-1",
      action: "compress",
      updatedAt: 123,
    },
  ];
  graph.knowledgeState = {
    activeOwnerKey: "owner:hero",
    owners: {
      "owner:hero": {
        ownerKey: "owner:hero",
        displayName: "Hero",
      },
    },
  };
  graph.regionState = {
    activeRegion: "camp",
    knownRegions: {
      camp: {
        regionId: "camp",
        displayName: "Camp",
      },
    },
  };
  graph.timelineState = {
    activeSegmentId: "segment-1",
    segments: [
      {
        id: "segment-1",
        label: "Night 1",
      },
    ],
  };
  graph.summaryState = {
    updatedAt: 456,
    entries: [
      {
        id: "summary-1",
        text: "Summary text",
      },
    ],
  };
  graph.nodes.push({
    id: "node-converter",
    type: "event",
    sourceFloor: 9,
    fields: {
      title: "Converter Node",
    },
    updatedAt: Date.now(),
    embedding: [0.25, 0.5, 0.75],
    scope: {
      layer: "pov",
      ownerType: "character",
      ownerId: "hero",
      ownerName: "Hero",
      regionPrimary: "camp",
      regionPath: ["camp", "tent"],
      regionSecondary: ["forest"],
    },
    storyTime: {
      segmentId: "segment-1",
      label: "Dawn",
      tense: "ongoing",
      relation: "same",
      anchorLabel: "Night",
      confidence: "high",
      source: "derived",
    },
    storyTimeSpan: {
      startSegmentId: "segment-0",
      endSegmentId: "segment-1",
      startLabel: "Night",
      endLabel: "Dawn",
      mixed: false,
      source: "derived",
    },
  });

  let snapshotDiagnostics = null;
  const snapshot = buildSnapshotFromGraph(graph, {
    chatId: "chat-a",
    revision: 17,
    onDiagnostics(snapshotValue) {
      snapshotDiagnostics = snapshotValue;
    },
  });
  assert.equal(snapshot.meta.chatId, "chat-a");
  assert.equal(snapshot.meta.revision, 17);
  assert.equal(snapshot.meta[BME_RUNTIME_RECORDS_NORMALIZED_META_KEY], true);
  assert.equal(snapshot.state.lastProcessedFloor, 9);
  assert.equal(snapshot.state.extractionCount, 4);
  assert.equal(snapshot.nodes.length, 1);
  assert.equal(Number.isFinite(snapshotDiagnostics?.nodesMs), true);
  assert.equal(Number.isFinite(snapshotDiagnostics?.edgesMs), true);
  assert.equal(Number.isFinite(snapshotDiagnostics?.tombstonesMs), true);
  assert.equal(Number.isFinite(snapshotDiagnostics?.stateMs), true);
  assert.equal(Number.isFinite(snapshotDiagnostics?.metaMs), true);
  assert.equal(Number.isFinite(snapshotDiagnostics?.totalMs), true);
  assert.equal(snapshotDiagnostics?.nodeCount, 1);

  let hydrateDiagnostics = null;
  const nextGraph = buildGraphFromSnapshot(snapshot, {
    chatId: "chat-a",
    onDiagnostics(snapshotValue) {
      hydrateDiagnostics = snapshotValue;
    },
  });
  assert.equal(hydrateDiagnostics?.success, true);
  assert.equal(Number.isFinite(hydrateDiagnostics?.nodesMs), true);
  assert.equal(Number.isFinite(hydrateDiagnostics?.edgesMs), true);
  assert.equal(Number.isFinite(hydrateDiagnostics?.runtimeMetaMs), true);
  assert.equal(Number.isFinite(hydrateDiagnostics?.stateMs), true);
  assert.equal(Number.isFinite(hydrateDiagnostics?.normalizeMs), true);
  assert.equal(Number.isFinite(hydrateDiagnostics?.integrityMs), true);
  assert.equal(Number.isFinite(hydrateDiagnostics?.totalMs), true);

  let reusedSnapshotDiagnostics = null;
  const reusedSnapshot = buildSnapshotFromGraph(nextGraph, {
    chatId: "chat-a",
    revision: 18,
    baseSnapshot: snapshot,
    onDiagnostics(snapshotValue) {
      reusedSnapshotDiagnostics = snapshotValue;
    },
  });
  assert.equal(
    reusedSnapshot.nodes[0],
    snapshot.nodes[0],
    "未变化节点应直接复用 baseSnapshot 记录对象",
  );
  assert.equal(reusedSnapshotDiagnostics?.reusedNodeCount, 1);
  nextGraph.nodes[0].updatedAt = Number(nextGraph.nodes[0].updatedAt || 0) + 1;
  const changedSnapshot = buildSnapshotFromGraph(nextGraph, {
    chatId: "chat-a",
    revision: 19,
    baseSnapshot: snapshot,
  });
  assert.notEqual(
    changedSnapshot.nodes[0],
    snapshot.nodes[0],
    "节点变化后不应复用 baseSnapshot 记录对象",
  );

  const rebuilt = buildGraphFromSnapshot(snapshot, {
    chatId: "chat-a",
  });
  const legacyCompatibleSnapshot = {
    ...snapshot,
    meta: {
      ...snapshot.meta,
    },
  };
  delete legacyCompatibleSnapshot.meta[BME_RUNTIME_RECORDS_NORMALIZED_META_KEY];
  legacyCompatibleSnapshot.nodes = [
    {
      ...legacyCompatibleSnapshot.nodes[0],
      scope: undefined,
      storyTime: undefined,
      storyTimeSpan: undefined,
    },
  ];
  const rebuiltLegacyCompatible = buildGraphFromSnapshot(legacyCompatibleSnapshot, {
    chatId: "chat-a",
  });
  const malformedButFlaggedSnapshot = {
    ...legacyCompatibleSnapshot,
    meta: {
      ...legacyCompatibleSnapshot.meta,
      [BME_RUNTIME_RECORDS_NORMALIZED_META_KEY]: true,
    },
  };
  const rebuiltMalformedButFlagged = buildGraphFromSnapshot(malformedButFlaggedSnapshot, {
    chatId: "chat-a",
  });
  const scopeRepairSnapshot = {
    ...snapshot,
    meta: {
      ...snapshot.meta,
    },
    nodes: [
      {
        ...snapshot.nodes[0],
        scope: {
          layer: "objective",
          regionPrimary: "王都/钟楼",
          regionSecondary: "旧城区 / 集市 / 钟楼",
        },
      },
    ],
  };
  delete scopeRepairSnapshot.meta[BME_RUNTIME_RECORDS_NORMALIZED_META_KEY];
  const rebuiltScopeRepair = buildGraphFromSnapshot(scopeRepairSnapshot, {
    chatId: "chat-a",
  });
  const scopeRepairDirtyState = getGraphPersistDirtyStateSnapshot(
    rebuiltScopeRepair,
  );
  assert.equal(rebuilt.historyState.lastProcessedAssistantFloor, 9);
  assert.equal(rebuilt.historyState.extractionCount, 4);
  assert.equal(rebuilt.nodes.length, 1);
  assert.equal(rebuilt.nodes[0].id, "node-converter");
  assert.equal(rebuilt.nodes[0].scope?.ownerType, "character");
  assert.equal(rebuilt.nodes[0].scope?.regionPrimary, "camp");
  assert.equal(rebuilt.nodes[0].storyTime?.label, "Dawn");
  assert.equal(rebuilt.nodes[0].storyTimeSpan?.endLabel, "Dawn");
  assert.equal(rebuilt.vectorIndexState.hashToNodeId["vec-hash"], "node-converter");
  assert.equal(rebuilt.maintenanceJournal[0].id, "maintenance-1");
  assert.equal(rebuilt.knowledgeState.activeOwnerKey, "owner:hero");
  assert.equal(rebuilt.regionState.activeRegion, "camp");
  assert.equal(rebuilt.timelineState.activeSegmentId, "segment-1");
  assert.equal(rebuilt.summaryState.entries[0].id, "summary-1");
  assert.equal(rebuiltLegacyCompatible.nodes[0].scope?.layer, "objective");
  assert.equal(rebuiltLegacyCompatible.nodes[0].storyTime?.tense, "unknown");
  assert.equal(rebuiltLegacyCompatible.nodes[0].storyTimeSpan?.mixed, false);
  assert.equal(rebuiltMalformedButFlagged.nodes[0].scope?.layer, "objective");
  assert.equal(rebuiltMalformedButFlagged.nodes[0].storyTime?.tense, "unknown");
  assert.equal(rebuiltMalformedButFlagged.nodes[0].storyTimeSpan?.mixed, false);
  assert.equal(rebuiltScopeRepair.nodes[0].scope?.regionPrimary, "钟楼");
  assert.deepEqual(rebuiltScopeRepair.nodes[0].scope?.regionPath, ["王都", "钟楼"]);
  assert.deepEqual(rebuiltScopeRepair.nodes[0].scope?.regionSecondary, [
    "旧城区",
    "集市",
  ]);
  assert.equal(
    scopeRepairDirtyState?.nodeUpsertIds?.includes("node-converter"),
    true,
  );
  assert.equal(rebuiltScopeRepair.vectorIndexState?.dirty, true);
  assert.equal(
    rebuiltScopeRepair.vectorIndexState?.replayRequiredNodeIds?.includes(
      "node-converter",
    ),
    true,
  );

  rebuilt.nodes[0].fields.title = "Mutated Converter Node";
  rebuilt.nodes[0].embedding[0] = 99;
  rebuilt.historyState.processedMessageHashes[1] = "mutated-hash";
  rebuilt.vectorIndexState.hashToNodeId["vec-hash"] = "node-mutated";
  rebuilt.batchJournal[0].processedRange[0] = 99;

  assert.equal(
    snapshot.nodes[0].fields.title,
    "Converter Node",
    "buildGraphFromSnapshot 不应复用 snapshot 节点的嵌套字段引用",
  );
  assert.equal(
    snapshot.meta[BME_RUNTIME_HISTORY_META_KEY].processedMessageHashes[1],
    "hash-1",
    "buildGraphFromSnapshot 不应复用 snapshot historyState 的嵌套对象引用",
  );
  assert.equal(
    snapshot.nodes[0].embedding[0],
    0.25,
    "buildGraphFromSnapshot 不应复用 snapshot 节点的数组字段引用",
  );
  assert.equal(
    snapshot.meta[BME_RUNTIME_VECTOR_META_KEY].hashToNodeId["vec-hash"],
    "node-converter",
    "buildGraphFromSnapshot 不应复用 snapshot vectorState 的嵌套对象引用",
  );
  assert.equal(
    snapshot.meta[BME_RUNTIME_BATCH_JOURNAL_META_KEY][0].processedRange[0],
    8,
    "buildGraphFromSnapshot 不应复用 snapshot batchJournal 的嵌套数组引用",
  );
}

async function main() {
  await setupIndexedDbTestEnv();
  await cleanupDatabases();

  await testBuildAndOpen();
  await testCrudAndMeta();
  await testTransactionRollback();
  await testSnapshotExportImport();
  await testSnapshotExportWithoutTombstones();
  await testSnapshotProbeExport();
  await testReplaceImportResetsStaleMeta();
  await testRevisionMonotonicity();
  await testTombstonePrune();
  await testChatIsolationAndManager();
  await testManagerRecreatesDbWhenSelectorKeyChanges();
  await testGraphSnapshotConverters();

  await cleanupDatabases();

  console.log("indexeddb-persistence tests passed");
}

await main();
