import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { ArchitectAgent } from "../agents/architect.js";
import { readBookRules as readStructuredBookRules } from "../agents/rules-reader.js";
import { readBookRules as readPlannerBookRules } from "../agents/planner-context.js";
import { readVolumeMap } from "../utils/outline-paths.js";
import { StateManager } from "../state/manager.js";
// ---------------------------------------------------------------------------
// Phase 5 cleanup (4) — verifies the post-cleanup invariants:
//   (1) volume_outline.md mirror is NOT produced by the architect
//       — readVolumeMap() still resolves through outline/volume_map.md
//   (2) particle_ledger.md / subplot_board.md are NOT seeded by the architect
//   (3) book_rules.md's YAML frontmatter now lives on story_frame.md; the
//       legacy file becomes a compat shim, and readBookRules() prefers the
//       new location
// ---------------------------------------------------------------------------
const ZERO_USAGE = {
    promptTokens: 0,
    completionTokens: 0,
    totalTokens: 0,
};
const SAMPLE_RESPONSE = [
    "=== SECTION: story_frame ===",
    "## 主题与基调",
    "一段测试用的主题散文。",
    "## 主角弧线",
    "主角从 A 走向 B。",
    "## 核心冲突与对手",
    "对手是 X。",
    "## 世界观底色",
    "湿冷的小镇。",
    "## 终局方向",
    "最后一个镜头。",
    "",
    "=== SECTION: volume_map ===",
    "## 各卷主题",
    "卷一压卷二放。",
    "## 关键节点章",
    "第 10 章转折。",
    "## 卷间钩子与回收",
    "钩子 H01。",
    "## 角色阶段性目标",
    "卷一：定调。",
    "## 卷尾必须发生的改变",
    "身份暴露。",
    "## 节奏意图",
    "前 10 章压。",
    "",
    "=== SECTION: rhythm_principles ===",
    "## 原则 1",
    "每 8 章一个高潮。",
    "",
    "=== SECTION: roles ===",
    "---ROLE---",
    "tier: major",
    "name: 主角甲",
    "---CONTENT---",
    "## 核心标签",
    "沉默、执拗",
    "## 反差细节",
    "会给流浪狗留罐头",
    "## 人物小传",
    "测试用小传。",
    "## 当前现状",
    "旧书店账房。",
    "## 关系网络",
    "无。",
    "## 内在驱动",
    "查清真相。",
    "## 成长弧光",
    "从单打独斗到托付他人。",
    "",
    "=== SECTION: book_rules ===",
    "---",
    "version: \"1.0\"",
    "protagonist:",
    "  name: 主角甲",
    "  personalityLock: [沉默, 执拗]",
    "  behavioralConstraints: [不对长辈失礼]",
    "prohibitions:",
    "  - 不得美化体制暴力",
    "chapterTypesOverride: []",
    "fatigueWordsOverride: []",
    "additionalAuditDimensions: []",
    "enableFullCastTracking: false",
    "---",
    "## 叙事视角",
    "第三人称单一视角。",
    "",
    "=== SECTION: current_state ===",
    "| 字段 | 值 |",
    "| --- | --- |",
    "| 当前章节 | 0 |",
    "",
    "=== SECTION: pending_hooks ===",
    "| hook_id | 起始章节 | 类型 | 状态 | 最近推进 | 预期回收 | 回收节奏 | 备注 |",
    "| --- | --- | --- | --- | --- | --- | --- | --- |",
    "| H01 | 1 | 主线 | 未开启 | 0 | 32章 | 中程 | 测试钩子 |",
].join("\n");
function buildAgent() {
    return new ArchitectAgent({
        client: {
            provider: "openai",
            apiFormat: "chat",
            stream: false,
            defaults: {
                temperature: 0.7,
                maxTokens: 4096,
                thinkingBudget: 0,
                maxTokensCap: null,
                extra: {},
            },
        },
        model: "test-model",
        projectRoot: process.cwd(),
    });
}
function baseBook() {
    return {
        id: "cleanup-book",
        title: "清理测试书",
        platform: "other",
        genre: "urban",
        status: "active",
        targetChapters: 40,
        chapterWordCount: 2000,
        language: "zh",
        createdAt: "2026-04-15T00:00:00.000Z",
        updatedAt: "2026-04-15T00:00:00.000Z",
    };
}
describe("Phase 5 cleanup (1) — volume_outline.md mirror removed", () => {
    let bookDir;
    beforeEach(async () => {
        bookDir = await mkdtemp(join(tmpdir(), "inkos-cleanup-1-"));
    });
    afterEach(async () => {
        await rm(bookDir, { recursive: true, force: true });
        vi.restoreAllMocks();
    });
    it("does not write volume_outline.md when running a fresh architect foundation", async () => {
        const agent = buildAgent();
        vi.spyOn(agent, "chat")
            .mockResolvedValue({ content: SAMPLE_RESPONSE, usage: ZERO_USAGE });
        const output = await agent.generateFoundation(baseBook());
        await agent.writeFoundationFiles(bookDir, output, false, "zh");
        await expect(readFile(join(bookDir, "story/volume_outline.md"), "utf-8")).rejects.toThrow();
        const newOutline = await readFile(join(bookDir, "story/outline/volume_map.md"), "utf-8");
        expect(newOutline).toContain("卷一压卷二放");
    });
    it("readVolumeMap resolves the new path without needing the legacy mirror", async () => {
        await mkdir(join(bookDir, "story/outline"), { recursive: true });
        await writeFile(join(bookDir, "story/outline/volume_map.md"), "NEW map content", "utf-8");
        const content = await readVolumeMap(bookDir, "(missing)");
        expect(content).toBe("NEW map content");
    });
    it("readVolumeMap still falls back to legacy volume_outline.md for pre-cleanup books", async () => {
        await mkdir(join(bookDir, "story"), { recursive: true });
        await writeFile(join(bookDir, "story/volume_outline.md"), "LEGACY outline content", "utf-8");
        const content = await readVolumeMap(bookDir, "(missing)");
        expect(content).toBe("LEGACY outline content");
    });
    it("isCompleteBookDirectory accepts the new outline/ layout (no legacy mirror needed)", async () => {
        const projectRoot = await mkdtemp(join(tmpdir(), "inkos-cleanup-1-proj-"));
        try {
            const targetBookDir = join(projectRoot, "books", "cleanup-book");
            const storyDir = join(targetBookDir, "story");
            await mkdir(join(storyDir, "outline"), { recursive: true });
            await mkdir(join(targetBookDir, "chapters"), { recursive: true });
            await Promise.all([
                writeFile(join(targetBookDir, "book.json"), "{}", "utf-8"),
                writeFile(join(storyDir, "outline/story_frame.md"), "# frame", "utf-8"),
                writeFile(join(storyDir, "outline/volume_map.md"), "# map", "utf-8"),
                writeFile(join(storyDir, "book_rules.md"), "# rules", "utf-8"),
                writeFile(join(storyDir, "current_state.md"), "# state", "utf-8"),
                writeFile(join(storyDir, "pending_hooks.md"), "# hooks", "utf-8"),
                writeFile(join(targetBookDir, "chapters/index.json"), "[]", "utf-8"),
            ]);
            const state = new StateManager(projectRoot);
            await expect(state.isCompleteBookDirectory(targetBookDir)).resolves.toBe(true);
        }
        finally {
            await rm(projectRoot, { recursive: true, force: true });
        }
    });
    it("isCompleteBookDirectory still accepts the pre-cleanup layout (legacy flat files)", async () => {
        const projectRoot = await mkdtemp(join(tmpdir(), "inkos-cleanup-1-legacy-"));
        try {
            const targetBookDir = join(projectRoot, "books", "legacy-book");
            const storyDir = join(targetBookDir, "story");
            await mkdir(storyDir, { recursive: true });
            await mkdir(join(targetBookDir, "chapters"), { recursive: true });
            await Promise.all([
                writeFile(join(targetBookDir, "book.json"), "{}", "utf-8"),
                writeFile(join(storyDir, "story_bible.md"), "# bible", "utf-8"),
                writeFile(join(storyDir, "volume_outline.md"), "# outline", "utf-8"),
                writeFile(join(storyDir, "book_rules.md"), "# rules", "utf-8"),
                writeFile(join(storyDir, "current_state.md"), "# state", "utf-8"),
                writeFile(join(storyDir, "pending_hooks.md"), "# hooks", "utf-8"),
                writeFile(join(targetBookDir, "chapters/index.json"), "[]", "utf-8"),
            ]);
            const state = new StateManager(projectRoot);
            await expect(state.isCompleteBookDirectory(targetBookDir)).resolves.toBe(true);
        }
        finally {
            await rm(projectRoot, { recursive: true, force: true });
        }
    });
});
describe("Phase 5 cleanup (2) — architect no longer seeds runtime log files", () => {
    let bookDir;
    beforeEach(async () => {
        bookDir = await mkdtemp(join(tmpdir(), "inkos-cleanup-2-"));
    });
    afterEach(async () => {
        await rm(bookDir, { recursive: true, force: true });
        vi.restoreAllMocks();
    });
    it("does not write particle_ledger.md or subplot_board.md even when the genre wants a numerical system", async () => {
        const agent = buildAgent();
        vi.spyOn(agent, "chat")
            .mockResolvedValue({ content: SAMPLE_RESPONSE, usage: ZERO_USAGE });
        const output = await agent.generateFoundation(baseBook());
        // numericalSystem=true would previously seed particle_ledger.md
        await agent.writeFoundationFiles(bookDir, output, true, "zh");
        await expect(readFile(join(bookDir, "story/particle_ledger.md"), "utf-8")).rejects.toThrow();
        await expect(readFile(join(bookDir, "story/subplot_board.md"), "utf-8")).rejects.toThrow();
    });
});
describe("Phase 5 cleanup (3) — book_rules YAML moved to story_frame.md frontmatter", () => {
    let bookDir;
    beforeEach(async () => {
        bookDir = await mkdtemp(join(tmpdir(), "inkos-cleanup-3-"));
    });
    afterEach(async () => {
        await rm(bookDir, { recursive: true, force: true });
        vi.restoreAllMocks();
    });
    it("writes the YAML frontmatter onto story_frame.md (not book_rules.md)", async () => {
        const agent = buildAgent();
        vi.spyOn(agent, "chat")
            .mockResolvedValue({ content: SAMPLE_RESPONSE, usage: ZERO_USAGE });
        const output = await agent.generateFoundation(baseBook());
        await agent.writeFoundationFiles(bookDir, output, false, "zh");
        const storyFrame = await readFile(join(bookDir, "story/outline/story_frame.md"), "utf-8");
        // story_frame.md now starts with YAML frontmatter
        expect(storyFrame.trimStart().startsWith("---")).toBe(true);
        expect(storyFrame).toContain("protagonist:");
        expect(storyFrame).toContain("主角甲");
        expect(storyFrame).toContain("prohibitions:");
        // prose body still present
        expect(storyFrame).toContain("主题与基调");
        // book_rules.md is now a compat shim, not the full YAML file
        const bookRulesShim = await readFile(join(bookDir, "story/book_rules.md"), "utf-8");
        expect(bookRulesShim).toContain("兼容指针");
        expect(bookRulesShim).toContain("story_frame.md");
        // Shim carries the narrative body excerpt but not the YAML frontmatter
        expect(bookRulesShim).toContain("叙事视角");
        expect(bookRulesShim).not.toMatch(/^---\s*\nversion:/m);
    });
    it("readBookRules() prefers story_frame.md frontmatter and parses the rules", async () => {
        const agent = buildAgent();
        vi.spyOn(agent, "chat")
            .mockResolvedValue({ content: SAMPLE_RESPONSE, usage: ZERO_USAGE });
        const output = await agent.generateFoundation(baseBook());
        await agent.writeFoundationFiles(bookDir, output, false, "zh");
        const parsed = await readStructuredBookRules(bookDir);
        expect(parsed).not.toBeNull();
        expect(parsed?.rules.protagonist?.name).toBe("主角甲");
        expect(parsed?.rules.protagonist?.personalityLock).toEqual(["沉默", "执拗"]);
        expect(parsed?.rules.prohibitions).toEqual(["不得美化体制暴力"]);
    });
    it("readBookRules() falls back to legacy book_rules.md when story_frame.md has no frontmatter", async () => {
        const storyDir = join(bookDir, "story");
        await mkdir(join(storyDir, "outline"), { recursive: true });
        // story_frame.md exists but has NO frontmatter (pre-cleanup book)
        await writeFile(join(storyDir, "outline/story_frame.md"), "# Story Frame\n\nPure prose with no YAML.\n", "utf-8");
        // Legacy book_rules.md carries the real frontmatter
        await writeFile(join(storyDir, "book_rules.md"), "---\nversion: \"1.0\"\nprotagonist:\n  name: LegacyHero\n  personalityLock: [stoic]\n  behavioralConstraints: []\nprohibitions:\n  - No lazy tropes\n---\n", "utf-8");
        const parsed = await readStructuredBookRules(bookDir);
        expect(parsed?.rules.protagonist?.name).toBe("LegacyHero");
        expect(parsed?.rules.prohibitions).toEqual(["No lazy tropes"]);
    });
    it("readBookRules() returns null when neither source exists", async () => {
        const parsed = await readStructuredBookRules(bookDir);
        expect(parsed).toBeNull();
    });
    it("planner-context readBookRules renders structured fields as a markdown block", async () => {
        const storyDir = join(bookDir, "story");
        await mkdir(join(storyDir, "outline"), { recursive: true });
        await writeFile(join(storyDir, "outline/story_frame.md"), [
            "---",
            "version: \"1.0\"",
            "protagonist:",
            "  name: 林辞",
            "  personalityLock: [沉默, 执拗]",
            "  behavioralConstraints: [不对长辈失礼]",
            "prohibitions:",
            "  - 不得美化体制暴力",
            "  - 不得神化主角",
            "---",
            "",
            "# Story Frame",
            "",
            "主题：测试",
            "",
        ].join("\n"), "utf-8");
        const rendered = await readPlannerBookRules(storyDir);
        expect(rendered).toContain("林辞");
        expect(rendered).toContain("沉默");
        expect(rendered).toContain("执拗");
        expect(rendered).toContain("不得美化体制暴力");
        expect(rendered).toContain("不得神化主角");
        expect(rendered).toContain("不对长辈失礼");
    });
    it("planner-context readBookRules returns empty string when no rules source exists", async () => {
        const storyDir = join(bookDir, "story");
        await mkdir(storyDir, { recursive: true });
        const rendered = await readPlannerBookRules(storyDir);
        expect(rendered).toBe("");
    });
});
//# sourceMappingURL=phase5-cleanup.test.js.map