import { describe, expect, it, beforeEach, afterEach } from "vitest";
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { loadBookSession, persistBookSession, listBookSessions, renameBookSession, deleteBookSession, migrateBookSession, createAndPersistBookSession, extractFirstUserMessageTitle, SessionAlreadyMigratedError, } from "../interaction/book-session-store.js";
import { createBookSession, appendBookSessionMessage } from "../interaction/session.js";
import { appendManualSessionMessages, legacyBookSessionPath, readTranscriptEvents, sessionsDir, } from "../interaction/session-transcript.js";
import { restoreAgentMessagesFromTranscript } from "../interaction/session-transcript-restore.js";
describe("book-session-store", () => {
    let tempDir;
    beforeEach(async () => {
        tempDir = await mkdtemp(join(tmpdir(), "inkos-test-"));
    });
    afterEach(async () => {
        await rm(tempDir, { recursive: true, force: true });
    });
    describe("persistBookSession + loadBookSession", () => {
        it("round-trips a session", async () => {
            const session = createBookSession("my-book");
            await persistBookSession(tempDir, session);
            const loaded = await loadBookSession(tempDir, session.sessionId);
            expect(loaded).not.toBeNull();
            expect(loaded.sessionId).toBe(session.sessionId);
            expect(loaded.bookId).toBe("my-book");
        });
        it("returns null for non-existent session", async () => {
            const loaded = await loadBookSession(tempDir, "nonexistent");
            expect(loaded).toBeNull();
        });
        it("persists messages", async () => {
            let session = createBookSession("book");
            session = appendBookSessionMessage(session, { role: "user", content: "test", timestamp: 100 });
            await persistBookSession(tempDir, session);
            const loaded = await loadBookSession(tempDir, session.sessionId);
            expect(loaded.messages).toHaveLength(1);
            expect(loaded.messages[0].content).toBe("test");
        });
        it("createBookSession initializes title as null", () => {
            const session = createBookSession("book");
            expect(session.title).toBeNull();
        });
        it("parses old session files without title field", async () => {
            const oldFormat = {
                sessionId: "old-session",
                bookId: "book",
                messages: [],
                draftRounds: [],
                events: [],
                createdAt: 1000,
                updatedAt: 1000,
            };
            const dir = join(tempDir, ".inkos", "sessions");
            await mkdir(dir, { recursive: true });
            await writeFile(join(dir, "old-session.json"), JSON.stringify(oldFormat));
            const loaded = await loadBookSession(tempDir, "old-session");
            expect(loaded).not.toBeNull();
            expect(loaded.title).toBeNull();
        });
        it("round-trips title through persist/load", async () => {
            let session = createBookSession("book");
            session = { ...session, title: "测试标题" };
            await persistBookSession(tempDir, session);
            const loaded = await loadBookSession(tempDir, session.sessionId);
            expect(loaded.title).toBe("测试标题");
        });
        it("读取 legacy JSON 时迁移为 JSONL 并保留 UI thinking", async () => {
            const session = {
                ...createBookSession("book-a", "legacy-1"),
                messages: [
                    { role: "user", content: "继续", timestamp: 10 },
                    { role: "assistant", content: "好的", thinking: "旧思考", timestamp: 11 },
                ],
            };
            await persistBookSession(tempDir, session);
            const loaded = await loadBookSession(tempDir, "legacy-1");
            expect(loaded).toMatchObject({
                sessionId: "legacy-1",
                bookId: "book-a",
                messages: [
                    { role: "user", content: "继续" },
                    { role: "assistant", content: "好的", thinking: "旧思考" },
                ],
            });
            await expect(readFile(join(tempDir, ".inkos", "sessions", "legacy-1.jsonl"), "utf-8"))
                .resolves
                .toContain("request_committed");
        });
        it("does not duplicate legacy messages when migration runs concurrently", async () => {
            const legacy = createBookSession("book-a", "legacy-race");
            const withMessages = {
                ...legacy,
                messages: [
                    { role: "user", content: "old user", timestamp: legacy.createdAt },
                    { role: "assistant", content: "old assistant", timestamp: legacy.createdAt + 1 },
                ],
            };
            await mkdir(sessionsDir(tempDir), { recursive: true });
            await writeFile(legacyBookSessionPath(tempDir, "legacy-race"), JSON.stringify(withMessages, null, 2), "utf-8");
            const [first, second] = await Promise.all([
                loadBookSession(tempDir, "legacy-race"),
                loadBookSession(tempDir, "legacy-race"),
            ]);
            expect(first?.messages).toHaveLength(2);
            expect(second?.messages).toHaveLength(2);
            const restored = await restoreAgentMessagesFromTranscript(tempDir, "legacy-race");
            expect(restored.map((message) => message.role)).toEqual(["user", "assistant"]);
        });
        it("createAndPersistBookSession 为新 session 写 JSONL 而不是 legacy JSON", async () => {
            const session = await createAndPersistBookSession(tempDir, "book-a", "123456-abcdef");
            expect(session.sessionId).toBe("123456-abcdef");
            await expect(readFile(join(tempDir, ".inkos", "sessions", "123456-abcdef.jsonl"), "utf-8"))
                .resolves
                .toContain("session_created");
            await expect(readFile(join(tempDir, ".inkos", "sessions", "123456-abcdef.json"), "utf-8"))
                .rejects
                .toThrow();
        });
        it("does not duplicate session_created when explicit session creation races", async () => {
            await Promise.all([
                createAndPersistBookSession(tempDir, "book-a", "create-race"),
                createAndPersistBookSession(tempDir, "book-a", "create-race"),
            ]);
            const events = await readTranscriptEvents(tempDir, "create-race");
            expect(events.filter((event) => event.type === "session_created")).toHaveLength(1);
            expect(events.map((event) => event.seq)).toEqual([1]);
        });
        it("renameBookSession 追加 metadata event 并从 JSONL 派生新标题", async () => {
            await createAndPersistBookSession(tempDir, "book-a", "123456-abcdef");
            const renamed = await renameBookSession(tempDir, "123456-abcdef", "新标题");
            expect(renamed.title).toBe("新标题");
            const loaded = await loadBookSession(tempDir, "123456-abcdef");
            expect(loaded.title).toBe("新标题");
        });
        it("listBookSessions 同时列出 JSONL session 和未迁移 legacy JSON session", async () => {
            await createAndPersistBookSession(tempDir, "book-a", "123456-abcdef");
            const dir = join(tempDir, ".inkos", "sessions");
            await mkdir(dir, { recursive: true });
            const legacy = { ...createBookSession("book-a", "legacy-1"), updatedAt: 999 };
            await writeFile(join(dir, "legacy-1.json"), JSON.stringify(legacy));
            const list = await listBookSessions(tempDir, "book-a");
            expect(list.map((entry) => entry.sessionId).sort()).toEqual(["123456-abcdef", "legacy-1"]);
        });
    });
    describe("listBookSessions", () => {
        it("returns empty for no sessions", async () => {
            const list = await listBookSessions(tempDir, "no-book");
            expect(list).toEqual([]);
        });
        it("filters by bookId", async () => {
            const s1 = createBookSession("book-a");
            const s2 = createBookSession("book-b");
            const s3 = createBookSession("book-a");
            await persistBookSession(tempDir, s1);
            await persistBookSession(tempDir, s2);
            await persistBookSession(tempDir, s3);
            const listA = await listBookSessions(tempDir, "book-a");
            expect(listA).toHaveLength(2);
            expect(listA.every((s) => s.bookId === "book-a")).toBe(true);
            const listB = await listBookSessions(tempDir, "book-b");
            expect(listB).toHaveLength(1);
        });
        it("sorts by updatedAt descending", async () => {
            const s1 = { ...createBookSession("book"), updatedAt: 100 };
            const s2 = { ...createBookSession("book"), updatedAt: 300 };
            const s3 = { ...createBookSession("book"), updatedAt: 200 };
            await persistBookSession(tempDir, s1);
            await persistBookSession(tempDir, s2);
            await persistBookSession(tempDir, s3);
            const list = await listBookSessions(tempDir, "book");
            expect(list[0].updatedAt).toBe(300);
            expect(list[1].updatedAt).toBe(200);
            expect(list[2].updatedAt).toBe(100);
        });
        it("updates session activity time from committed chat messages", async () => {
            const session = await createAndPersistBookSession(tempDir, "book", "chat-activity");
            await new Promise((resolve) => setTimeout(resolve, 5));
            await appendManualSessionMessages(tempDir, session.sessionId, [{
                    role: "user",
                    content: "继续",
                    timestamp: Date.now(),
                }]);
            const loaded = await loadBookSession(tempDir, session.sessionId);
            expect(loaded?.updatedAt).toBeGreaterThan(session.updatedAt);
        });
        it("lists null bookId sessions", async () => {
            const s = createBookSession(null);
            await persistBookSession(tempDir, s);
            const list = await listBookSessions(tempDir, null);
            expect(list).toHaveLength(1);
        });
    });
    describe("renameBookSession", () => {
        it("sets title and updates updatedAt", async () => {
            const session = createBookSession("book");
            await persistBookSession(tempDir, session);
            const oldUpdatedAt = session.updatedAt;
            await new Promise((resolve) => setTimeout(resolve, 5));
            await renameBookSession(tempDir, session.sessionId, "新标题");
            const loaded = await loadBookSession(tempDir, session.sessionId);
            expect(loaded.title).toBe("新标题");
            expect(loaded.updatedAt).toBeGreaterThan(oldUpdatedAt);
        });
        it("returns null for non-existent session", async () => {
            const result = await renameBookSession(tempDir, "nonexistent", "title");
            expect(result).toBeNull();
        });
        it("assigns unique seq for concurrent metadata updates", async () => {
            await createAndPersistBookSession(tempDir, "book-a", "metadata-race");
            await Promise.all([
                renameBookSession(tempDir, "metadata-race", "标题 A"),
                renameBookSession(tempDir, "metadata-race", "标题 B"),
            ]);
            const events = await readTranscriptEvents(tempDir, "metadata-race");
            expect(events.map((event) => event.seq)).toEqual([1, 2, 3]);
            expect(new Set(events.map((event) => event.seq)).size).toBe(events.length);
        });
    });
    describe("deleteBookSession", () => {
        it("removes session file", async () => {
            const session = createBookSession("book");
            await persistBookSession(tempDir, session);
            await deleteBookSession(tempDir, session.sessionId);
            const loaded = await loadBookSession(tempDir, session.sessionId);
            expect(loaded).toBeNull();
        });
        it("does nothing for non-existent session", async () => {
            await expect(deleteBookSession(tempDir, "nonexistent")).resolves.toBeUndefined();
        });
    });
    describe("extractFirstUserMessageTitle", () => {
        it("returns null when messages array is empty", () => {
            expect(extractFirstUserMessageTitle([])).toBeNull();
        });
        it("returns null when no user message exists", () => {
            expect(extractFirstUserMessageTitle([
                { role: "assistant", content: "hi" },
                { role: "system", content: "prompt" },
            ])).toBeNull();
        });
        it("picks the first user message content", () => {
            expect(extractFirstUserMessageTitle([
                { role: "system", content: "sys" },
                { role: "user", content: "第一条提问" },
                { role: "assistant", content: "回答" },
                { role: "user", content: "第二条提问" },
            ])).toBe("第一条提问");
        });
        it("collapses whitespace into single spaces", () => {
            expect(extractFirstUserMessageTitle([
                { role: "user", content: "多行\n\n内容   有空格" },
            ])).toBe("多行 内容 有空格");
        });
        it("truncates content longer than 20 chars with ellipsis", () => {
            expect(extractFirstUserMessageTitle([
                { role: "user", content: "这是一段超过二十个字符的很长的提问内容会被截断" },
            ])).toBe("这是一段超过二十个字符的很长的提问内容会…");
        });
        it("returns null when content is only whitespace", () => {
            expect(extractFirstUserMessageTitle([
                { role: "user", content: "   \n\t   " },
            ])).toBeNull();
        });
        it("returns null for non-array input", () => {
            expect(extractFirstUserMessageTitle(null)).toBeNull();
            expect(extractFirstUserMessageTitle(undefined)).toBeNull();
            expect(extractFirstUserMessageTitle("not array")).toBeNull();
        });
    });
    describe("listBookSessions: 老 session lazy migration", () => {
        it("把 title 为 null 但已有用户消息的老 session 补写 title 并持久化", async () => {
            const session = {
                ...createBookSession("book-a"),
                title: null,
                messages: [
                    { role: "user", content: "帮我写下一章", timestamp: 100 },
                    { role: "assistant", content: "好的，正在构思...", timestamp: 200 },
                ],
            };
            await persistBookSession(tempDir, session);
            // 触发 list → 应该迁移
            const list = await listBookSessions(tempDir, "book-a");
            expect(list).toHaveLength(1);
            expect(list[0].title).toBe("帮我写下一章");
            // 验证已经落盘
            const reloaded = await loadBookSession(tempDir, session.sessionId);
            expect(reloaded.title).toBe("帮我写下一章");
        });
        it("不覆盖已有 title 的 session", async () => {
            let session = createBookSession("book-a");
            session = {
                ...session,
                title: "原有标题",
                messages: [
                    { role: "user", content: "后来的消息", timestamp: 100 },
                ],
            };
            await persistBookSession(tempDir, session);
            const list = await listBookSessions(tempDir, "book-a");
            expect(list[0].title).toBe("原有标题");
            const reloaded = await loadBookSession(tempDir, session.sessionId);
            expect(reloaded.title).toBe("原有标题");
        });
        it("没有用户消息的 session：title 保持 null，不 persist", async () => {
            const session = createBookSession("book-a");
            await persistBookSession(tempDir, session);
            const originalUpdatedAt = session.updatedAt;
            const list = await listBookSessions(tempDir, "book-a");
            expect(list[0].title).toBeNull();
            const reloaded = await loadBookSession(tempDir, session.sessionId);
            expect(reloaded.title).toBeNull();
            expect(reloaded.updatedAt).toBe(originalUpdatedAt);
        });
        it("多条老 session 同时迁移", async () => {
            const s1 = {
                ...createBookSession("book-b"),
                title: null,
                messages: [{ role: "user", content: "问题一", timestamp: 1 }],
            };
            const s2 = {
                ...createBookSession("book-b"),
                title: null,
                messages: [{ role: "user", content: "问题二", timestamp: 1 }],
            };
            await persistBookSession(tempDir, s1);
            await persistBookSession(tempDir, s2);
            const list = await listBookSessions(tempDir, "book-b");
            expect(list).toHaveLength(2);
            const titles = new Set(list.map((s) => s.title));
            expect(titles).toEqual(new Set(["问题一", "问题二"]));
        });
    });
    describe("migrateBookSession", () => {
        it("binds an orphan session to a book", async () => {
            const session = createBookSession(null);
            await persistBookSession(tempDir, session);
            const oldUpdatedAt = session.updatedAt;
            await new Promise((resolve) => setTimeout(resolve, 5));
            const migrated = await migrateBookSession(tempDir, session.sessionId, "book-1");
            expect(migrated).not.toBeNull();
            expect(migrated.bookId).toBe("book-1");
            expect(migrated.updatedAt).toBeGreaterThan(oldUpdatedAt);
        });
        it("returns null for non-existent session", async () => {
            const result = await migrateBookSession(tempDir, "nonexistent", "book-1");
            expect(result).toBeNull();
        });
        it("throws when session is already bound to a book", async () => {
            const session = createBookSession("book-1");
            await persistBookSession(tempDir, session);
            await expect(migrateBookSession(tempDir, session.sessionId, "book-2")).rejects.toBeInstanceOf(SessionAlreadyMigratedError);
        });
    });
});
//# sourceMappingURL=book-session-store.test.js.map