import { assert } from "chai";
import {
  loadAllConversationHistory,
  loadConversationHistoryScope,
} from "../src/modules/contextPanel/historyLoader";
import { loadAllClaudeConversationHistory } from "../src/claudeCode/historyLoader";
import { loadAllCodexConversationHistory } from "../src/codexAppServer/historyLoader";
import {
  CLAUDE_GLOBAL_CONVERSATION_KEY_BASE,
  CLAUDE_PAPER_CONVERSATION_KEY_BASE,
  CODEX_GLOBAL_CONVERSATION_KEY_BASE,
  CODEX_PAPER_CONVERSATION_KEY_BASE,
} from "../src/shared/conversationKeySpace";
import { buildConversationID } from "../src/shared/conversationRegistry";
import {
  createGlobalConversation,
  getLatestEmptyGlobalConversation,
  listGlobalConversations,
  listPaperConversations,
  touchEmptyGlobalConversation,
  touchEmptyPaperConversation,
} from "../src/utils/chatStore";

describe("historyLoader", function () {
  const globalScope = globalThis as typeof globalThis & {
    Zotero?: Record<string, unknown>;
  };
  const originalZotero = globalScope.Zotero;

  afterEach(function () {
    globalScope.Zotero = originalZotero;
  });

  it("loads normalized open-chat history rows including drafts", async function () {
    globalScope.Zotero = {
      ...(originalZotero || {}),
      DB: {
        queryAsync: async (sql: string) => {
          if (sql.includes("FROM llm_for_zotero_global_conversations gc")) {
            return [
              {
                conversationKey: 2_000_000_001,
                libraryID: 1,
                createdAt: 100,
                title: "First chat",
                lastActivityAt: 300,
                userTurnCount: 2,
              },
              {
                conversationKey: 2_000_000_002,
                libraryID: 1,
                createdAt: 400,
                title: "",
                lastActivityAt: 400,
                userTurnCount: 0,
              },
            ];
          }
          return [];
        },
      },
    };

    const rows = await loadConversationHistoryScope({
      mode: "open",
      libraryID: 1,
      limit: 20,
    });

    assert.deepEqual(rows, [
      {
        mode: "open",
        conversationID: buildConversationID({
          conversationKey: 2_000_000_001,
          system: "upstream",
          kind: "global",
          libraryID: 1,
        }),
        conversationKey: 2_000_000_001,
        title: "First chat",
        createdAt: 100,
        lastActivityAt: 300,
        userTurnCount: 2,
        isDraft: false,
      },
      {
        mode: "open",
        conversationID: buildConversationID({
          conversationKey: 2_000_000_002,
          system: "upstream",
          kind: "global",
          libraryID: 1,
        }),
        conversationKey: 2_000_000_002,
        title: "New chat",
        createdAt: 400,
        lastActivityAt: 400,
        userTurnCount: 0,
        isDraft: true,
      },
    ]);
  });

  it("loads normalized paper-chat history rows", async function () {
    globalScope.Zotero = {
      ...(originalZotero || {}),
      DB: {
        queryAsync: async (sql: string, params?: unknown[]) => {
          const normalizedParams = Array.isArray(params) ? params : [];
          if (
            sql.includes(
              "INSERT OR IGNORE INTO llm_for_zotero_paper_conversations",
            )
          ) {
            return [];
          }
          if (
            sql.includes("FROM llm_for_zotero_paper_conversations pc") &&
            sql.includes("WHERE pc.conversation_key = ?")
          ) {
            return [
              {
                conversationKey: 321,
                libraryID: normalizedParams[0] === 321 ? 3 : 0,
                paperItemID: 321,
                sessionVersion: 1,
                createdAt: 200,
                title: "",
                lastActivityAt: 200,
                userTurnCount: 0,
              },
            ];
          }
          if (
            sql.includes("FROM llm_for_zotero_paper_conversations pc") &&
            sql.includes("WHERE pc.library_id = ?")
          ) {
            return [
              {
                conversationKey: 321,
                libraryID: 3,
                paperItemID: 321,
                sessionVersion: 1,
                createdAt: 200,
                title: "Paper thread",
                lastActivityAt: 250,
                userTurnCount: 1,
              },
            ];
          }
          return [];
        },
      },
    };

    const rows = await loadConversationHistoryScope({
      mode: "paper",
      libraryID: 3,
      paperItemID: 321,
      limit: 20,
    });

    assert.deepEqual(rows, [
      {
        mode: "paper",
        conversationID: buildConversationID({
          conversationKey: 321,
          system: "upstream",
          kind: "paper",
          libraryID: 3,
          paperItemID: 321,
        }),
        conversationKey: 321,
        title: "Paper thread",
        createdAt: 200,
        lastActivityAt: 250,
        userTurnCount: 1,
        isDraft: false,
        sessionVersion: 1,
        paperItemID: 321,
      },
    ]);
  });

  it("loads all upstream conversations across paper and library chat", async function () {
    globalScope.Zotero = {
      ...(originalZotero || {}),
      DB: {
        queryAsync: async (sql: string) => {
          if (
            sql.includes("FROM llm_for_zotero_paper_conversations pc") &&
            sql.includes("WHERE pc.library_id = ?") &&
            !sql.includes("pc.paper_item_id = ?")
          ) {
            return [
              {
                conversationKey: 1_500_000_010,
                libraryID: 1,
                paperItemID: 10,
                sessionVersion: 2,
                createdAt: 100,
                title: "Paper conversation",
                lastActivityAt: 400,
                userTurnCount: 1,
              },
            ];
          }
          if (sql.includes("FROM llm_for_zotero_global_conversations gc")) {
            return [
              {
                conversationKey: 2_000_000_001,
                libraryID: 1,
                createdAt: 200,
                title: "Library conversation",
                lastActivityAt: 300,
                userTurnCount: 1,
              },
            ];
          }
          return [];
        },
      },
    };

    const rows = await loadAllConversationHistory({ libraryID: 1, limit: 10 });

    assert.deepEqual(
      rows.map((row) => ({
        mode: row.mode,
        conversationKey: row.conversationKey,
        paperItemID: row.paperItemID,
      })),
      [
        {
          mode: "paper",
          conversationKey: 1_500_000_010,
          paperItemID: 10,
        },
        {
          mode: "open",
          conversationKey: 2_000_000_001,
          paperItemID: undefined,
        },
      ],
    );
  });

  it("can load all upstream search candidates without a recent-history limit", async function () {
    const queries: string[] = [];
    globalScope.Zotero = {
      ...(originalZotero || {}),
      DB: {
        queryAsync: async (sql: string) => {
          queries.push(sql);
          if (
            sql.includes("FROM llm_for_zotero_paper_conversations pc") &&
            sql.includes("WHERE pc.library_id = ?") &&
            !sql.includes("pc.paper_item_id = ?")
          ) {
            return [
              {
                conversationKey: 1_500_000_010,
                libraryID: 1,
                paperItemID: 10,
                sessionVersion: 2,
                createdAt: 100,
                title: "Old paper conversation",
                lastActivityAt: 100,
                userTurnCount: 1,
              },
            ];
          }
          if (sql.includes("FROM llm_for_zotero_global_conversations gc")) {
            return [
              {
                conversationKey: 2_000_000_001,
                libraryID: 1,
                createdAt: 200,
                title: "Old library conversation",
                lastActivityAt: 200,
                userTurnCount: 1,
              },
            ];
          }
          return [];
        },
      },
    };

    const rows = await loadAllConversationHistory({ libraryID: 1, limit: null });

    assert.lengthOf(rows, 2);
    assert.isFalse(
      queries
        .filter(
          (sql) =>
            sql.includes("FROM llm_for_zotero_paper_conversations pc") ||
            sql.includes("FROM llm_for_zotero_global_conversations gc"),
        )
        .some((sql) => sql.includes("LIMIT ?")),
    );
  });

  it("loads all Claude Code conversations across paper and global chat", async function () {
    const claudePaperKey = CLAUDE_PAPER_CONVERSATION_KEY_BASE + 10;
    const claudeGlobalKey = CLAUDE_GLOBAL_CONVERSATION_KEY_BASE + 1;
    globalScope.Zotero = {
      ...(originalZotero || {}),
      DB: {
        queryAsync: async (sql: string, params?: unknown[]) => {
          if (
            sql.includes("FROM llm_for_zotero_conversation_registry") &&
            sql.includes("WHERE conversation_key = ?")
          ) {
            const conversationKey = Number(params?.[0]);
            return [
              {
                conversationKey,
                system: "claude_code",
                kind: conversationKey === claudePaperKey ? "paper" : "global",
                profileSignature: "profile-default",
                libraryID: 1,
                paperItemID: conversationKey === claudePaperKey ? 10 : null,
                valid: 1,
              },
            ];
          }
          if (
            sql.includes("FROM llm_for_zotero_claude_conversations c") &&
            sql.includes("c.kind = 'paper'")
          ) {
            return [
              {
                conversationKey: claudePaperKey,
                libraryID: 1,
                kind: "paper",
                paperItemID: 10,
                createdAt: 100,
                updatedAt: 500,
                title: "Claude paper conversation",
                userTurnCount: 2,
              },
            ];
          }
          if (
            sql.includes("FROM llm_for_zotero_claude_conversations c") &&
            sql.includes("c.kind = 'global'")
          ) {
            return [
              {
                conversationKey: claudeGlobalKey,
                libraryID: 1,
                kind: "global",
                createdAt: 200,
                updatedAt: 300,
                title: "Claude global conversation",
                userTurnCount: 1,
              },
            ];
          }
          return [];
        },
      },
    };

    const rows = await loadAllClaudeConversationHistory({
      libraryID: 1,
      limit: 10,
    });

    assert.deepEqual(
      rows.map((row) => ({
        kind: row.kind,
        conversationKey: row.conversationKey,
        paperItemID: row.paperItemID,
      })),
      [
        {
          kind: "paper",
          conversationKey: claudePaperKey,
          paperItemID: 10,
        },
        {
          kind: "global",
          conversationKey: claudeGlobalKey,
          paperItemID: undefined,
        },
      ],
    );
  });

  it("loads all Codex conversations across paper and global chat", async function () {
    const codexPaperKey = CODEX_PAPER_CONVERSATION_KEY_BASE + 10;
    const codexGlobalKey = CODEX_GLOBAL_CONVERSATION_KEY_BASE + 1;
    globalScope.Zotero = {
      ...(originalZotero || {}),
      DB: {
        queryAsync: async (sql: string, params?: unknown[]) => {
          if (
            sql.includes("FROM llm_for_zotero_conversation_registry") &&
            sql.includes("WHERE conversation_key = ?")
          ) {
            const conversationKey = Number(params?.[0]);
            return [
              {
                conversationKey,
                system: "codex",
                kind: conversationKey === codexPaperKey ? "paper" : "global",
                profileSignature: "profile-default",
                libraryID: 1,
                paperItemID: conversationKey === codexPaperKey ? 10 : null,
                valid: 1,
              },
            ];
          }
          if (
            sql.includes("FROM llm_for_zotero_codex_conversations c") &&
            sql.includes("c.kind = 'paper'")
          ) {
            return [
              {
                conversationKey: codexPaperKey,
                libraryID: 1,
                kind: "paper",
                paperItemID: 10,
                createdAt: 100,
                updatedAt: 500,
                title: "Codex paper conversation",
                userTurnCount: 2,
              },
            ];
          }
          if (
            sql.includes("FROM llm_for_zotero_codex_conversations c") &&
            sql.includes("c.kind = 'global'")
          ) {
            return [
              {
                conversationKey: codexGlobalKey,
                libraryID: 1,
                kind: "global",
                createdAt: 200,
                updatedAt: 300,
                title: "Codex global conversation",
                userTurnCount: 1,
              },
            ];
          }
          return [];
        },
      },
    };

    const rows = await loadAllCodexConversationHistory({
      libraryID: 1,
      limit: 10,
    });

    assert.deepEqual(
      rows.map((row) => ({
        kind: row.kind,
        conversationKey: row.conversationKey,
        paperItemID: row.paperItemID,
      })),
      [
        {
          kind: "paper",
          conversationKey: codexPaperKey,
          paperItemID: 10,
        },
        {
          kind: "global",
          conversationKey: codexGlobalKey,
          paperItemID: undefined,
        },
      ],
    );
  });

  it("creates a fresh global draft instead of reusing an older empty draft", async function () {
    const conversations = [
      {
        conversationKey: 2_000_000_001,
        libraryID: 1,
        createdAt: 100,
        title: "",
      },
    ];
    globalScope.Zotero = {
      ...(originalZotero || {}),
      DB: {
        executeTransaction: async (fn: () => Promise<number>) => fn(),
        queryAsync: async (sql: string, params?: unknown[]) => {
          const normalizedParams = Array.isArray(params) ? params : [];
          if (
            sql.includes("SELECT conversation_key AS conversationKey") &&
            sql.includes("FROM llm_for_zotero_global_conversations") &&
            sql.includes("WHERE conversation_key = ?")
          ) {
            return conversations
              .filter(
                (row) => row.conversationKey === Number(normalizedParams[0]),
              )
              .map((row) => ({ conversationKey: row.conversationKey }));
          }
          if (
            sql.includes("SELECT MAX(conversation_key)") &&
            sql.includes("FROM llm_for_zotero_global_conversations")
          ) {
            return [
              {
                maxConversationKey: Math.max(
                  ...conversations.map((row) => row.conversationKey),
                ),
              },
            ];
          }
          if (sql.includes("INSERT INTO llm_for_zotero_global_conversations")) {
            conversations.push({
              conversationKey: Number(normalizedParams[1]),
              libraryID: Number(normalizedParams[2]),
              createdAt: Number(normalizedParams[3]),
              title: "",
            });
            return [];
          }
          if (
            sql.includes("FROM llm_for_zotero_global_conversations gc") &&
            sql.includes("COALESCE(gc.user_turn_count, 0) = 0")
          ) {
            return conversations
              .filter((row) => row.libraryID === Number(normalizedParams[0]))
              .sort((a, b) => b.createdAt - a.createdAt)
              .slice(0, 1)
              .map((row) => ({
                conversationKey: row.conversationKey,
                libraryID: row.libraryID,
                createdAt: row.createdAt,
                title: row.title,
                lastActivityAt: row.createdAt,
                userTurnCount: 0,
              }));
          }
          if (sql.includes("FROM llm_for_zotero_global_conversations gc")) {
            return conversations
              .filter((row) => row.libraryID === Number(normalizedParams[0]))
              .sort((a, b) => b.createdAt - a.createdAt)
              .map((row) => ({
                conversationKey: row.conversationKey,
                libraryID: row.libraryID,
                createdAt: row.createdAt,
                title: row.title,
                lastActivityAt: row.createdAt,
                userTurnCount: 0,
              }));
          }
          return [];
        },
      },
    };

    const oldDraft = await getLatestEmptyGlobalConversation(1);
    const newKey = await createGlobalConversation(1);
    const rows = await listGlobalConversations(1, 10, true);

    assert.equal(oldDraft?.conversationKey, 2_000_000_001);
    assert.isAbove(newKey, 2_000_000_001);
    assert.equal(rows[0]?.conversationKey, newKey);
  });

  it("touches an empty global draft so it sorts as the latest draft", async function () {
    const conversations = [
      {
        conversationKey: 2_000_000_001,
        libraryID: 1,
        createdAt: 100,
        title: "",
      },
      {
        conversationKey: 2_000_000_002,
        libraryID: 1,
        createdAt: 300,
        title: "",
      },
    ];
    globalScope.Zotero = {
      ...(originalZotero || {}),
      DB: {
        queryAsync: async (sql: string, params?: unknown[]) => {
          const normalizedParams = Array.isArray(params) ? params : [];
          if (
            sql.includes("UPDATE llm_for_zotero_global_conversations") &&
            sql.includes("NOT EXISTS")
          ) {
            const row = conversations.find(
              (entry) => entry.conversationKey === Number(normalizedParams[2]),
            );
            if (row) row.createdAt = Number(normalizedParams[0]);
            return [];
          }
          if (sql.includes("FROM llm_for_zotero_global_conversations gc")) {
            return conversations
              .filter((row) => row.libraryID === Number(normalizedParams[0]))
              .sort((a, b) => b.createdAt - a.createdAt)
              .map((row) => ({
                conversationKey: row.conversationKey,
                libraryID: row.libraryID,
                createdAt: row.createdAt,
                title: row.title,
                lastActivityAt: row.createdAt,
                userTurnCount: 0,
              }));
          }
          return [];
        },
      },
    };

    await touchEmptyGlobalConversation(2_000_000_001, 500);
    const rows = await listGlobalConversations(1, 10, true);

    assert.equal(rows[0]?.conversationKey, 2_000_000_001);
    assert.equal(rows[0]?.lastActivityAt, 500);
  });

  it("touches an empty paper draft so it sorts as the latest draft", async function () {
    const conversations = [
      {
        conversationKey: 1_500_000_001,
        libraryID: 1,
        paperItemID: 42,
        sessionVersion: 1,
        createdAt: 100,
        title: "",
      },
      {
        conversationKey: 1_500_000_002,
        libraryID: 1,
        paperItemID: 42,
        sessionVersion: 1,
        createdAt: 300,
        title: "",
      },
    ];
    globalScope.Zotero = {
      ...(originalZotero || {}),
      DB: {
        queryAsync: async (sql: string, params?: unknown[]) => {
          const normalizedParams = Array.isArray(params) ? params : [];
          if (
            sql.includes("UPDATE llm_for_zotero_paper_conversations") &&
            sql.includes("NOT EXISTS")
          ) {
            const row = conversations.find(
              (entry) => entry.conversationKey === Number(normalizedParams[2]),
            );
            if (row) row.createdAt = Number(normalizedParams[0]);
            return [];
          }
          if (sql.includes("FROM llm_for_zotero_paper_conversations pc")) {
            return conversations
              .filter(
                (row) =>
                  row.libraryID === Number(normalizedParams[0]) &&
                  row.paperItemID === Number(normalizedParams[1]),
              )
              .sort((a, b) => b.createdAt - a.createdAt)
              .map((row) => ({
                conversationKey: row.conversationKey,
                libraryID: row.libraryID,
                paperItemID: row.paperItemID,
                sessionVersion: row.sessionVersion,
                createdAt: row.createdAt,
                title: row.title,
                lastActivityAt: row.createdAt,
                userTurnCount: 0,
              }));
          }
          return [];
        },
      },
    };

    await touchEmptyPaperConversation(1_500_000_001, 500);
    const rows = await listPaperConversations(1, 42, 10, true);

    assert.equal(rows[0]?.conversationKey, 1_500_000_001);
    assert.equal(rows[0]?.lastActivityAt, 500);
  });
});
