import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { readFileSync, mkdtempSync, mkdirSync, rmSync, writeFileSync, } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { tmpdir } from "node:os";
import { conversationArchiveEnrichRejection } from "../conversation-archive-enrich-rejection.js";
import { loadRejections, rejectionKey, rejectionsJsonlPath, } from "../conversation-archive-derive-insights.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const PACKAGE_ROOT = resolve(__dirname, "..", "..", "..");
const TOOL_PATH = resolve(PACKAGE_ROOT, "src/tools/conversation-archive-enrich-rejection.ts");
const INDEX_PATH = resolve(PACKAGE_ROOT, "src/index.ts");
const PLUGIN_PATH = resolve(PACKAGE_ROOT, "..", "PLUGIN.md");
function readTool() {
    return readFileSync(TOOL_PATH, "utf-8");
}
function readIndex() {
    return readFileSync(INDEX_PATH, "utf-8");
}
describe("conversation-archive-enrich-rejection tool", () => {
    it("exposes record + undo modes only", () => {
        const src = readTool();
        expect(src).toMatch(/RejectionMode\s*=\s*"record"\s*\|\s*"undo"/);
    });
    it("imports the JSONL path constant + key helper from the derive-insights module (single source-of-truth)", () => {
        const src = readTool();
        expect(src).toMatch(/rejectionsJsonlPath/);
        expect(src).toMatch(/rejectionKey/);
        expect(src).toMatch(/from\s+["']\.\/conversation-archive-derive-insights/);
    });
    it("uses atomic rename on undo (tmp file → rename → live path)", () => {
        const src = readTool();
        expect(src).toMatch(/renameSync/);
        expect(src).toMatch(/\.tmp/);
    });
    it("uses appendFileSync on record (O_APPEND atomic for sub-PIPE_BUF writes)", () => {
        const src = readTool();
        expect(src).toMatch(/appendFileSync/);
    });
    it("stamps rejectedAt + rejectedBySession on every line", () => {
        const src = readTool();
        expect(src).toContain("rejectedAt");
        expect(src).toContain("rejectedBySession");
    });
    it("emits stored + rejection-undone log lines per spec", () => {
        const src = readTool();
        expect(src).toMatch(/\[conversation-archive-enrich\] rejected /);
        expect(src).toMatch(/\[conversation-archive-enrich\] rejection-undone /);
    });
    it("registered in memory-mcp index.ts with boot log line", () => {
        const idx = readIndex();
        expect(idx).toContain("conversation-archive-enrich-rejection");
        expect(idx).toContain("[memory-mcp] registered tool=conversation-archive-enrich-rejection");
    });
    it("listed in PLUGIN.md tools section", () => {
        const plugin = readFileSync(PLUGIN_PATH, "utf-8");
        expect(plugin).toMatch(/-\s+name:\s+conversation-archive-enrich-rejection\b/);
    });
});
describe("conversation-archive-enrich-rejection — runtime", () => {
    let scratchDir;
    let origPlatformRoot;
    beforeEach(() => {
        scratchDir = mkdtempSync(resolve(tmpdir(), "rejection-test-"));
        origPlatformRoot = process.env.PLATFORM_ROOT;
        // The path resolver computes ../data/accounts from PLATFORM_ROOT — point
        // PLATFORM_ROOT at scratchDir/platform so the JSONL lands under scratchDir/data.
        process.env.PLATFORM_ROOT = resolve(scratchDir, "platform");
    });
    afterEach(() => {
        if (origPlatformRoot === undefined) {
            delete process.env.PLATFORM_ROOT;
        }
        else {
            process.env.PLATFORM_ROOT = origPlatformRoot;
        }
        rmSync(scratchDir, { recursive: true, force: true });
    });
    it("record then undo round-trips the JSONL count back to zero", async () => {
        const params = {
            accountId: "acct-980-test",
            archiveElementId: "4:archive:1",
            chunkElementId: "4:chunk:1",
            kind: "mention",
            contentHash: "deadbeef",
        };
        const recorded = await conversationArchiveEnrichRejection({
            ...params,
            mode: "record",
        });
        expect(recorded.beforeCount).toBe(0);
        expect(recorded.afterCount).toBe(1);
        const dupRecorded = await conversationArchiveEnrichRejection({
            ...params,
            mode: "record",
        });
        // Idempotent: second record is a no-op.
        expect(dupRecorded.beforeCount).toBe(1);
        expect(dupRecorded.afterCount).toBe(1);
        const undone = await conversationArchiveEnrichRejection({
            ...params,
            mode: "undo",
        });
        expect(undone.beforeCount).toBe(1);
        expect(undone.afterCount).toBe(0);
        const undoneAgain = await conversationArchiveEnrichRejection({
            ...params,
            mode: "undo",
        });
        // Undoing a non-existent key is a silent no-op.
        expect(undoneAgain.beforeCount).toBe(0);
        expect(undoneAgain.afterCount).toBe(0);
    });
    it("undo clears EVERY matching line, not just one (residual race window)", async () => {
        // Simulate the documented race: two record-style lines for the same key
        // bypass the idempotency guard (which only fires when the read+write are
        // observed in-process by the same caller). Write them directly to disk.
        const accountId = "acct-980-dup";
        const path = rejectionsJsonlPath(accountId);
        mkdirSync(dirname(path), { recursive: true });
        const line = JSON.stringify({
            archiveElementId: "4:archive:dup",
            chunkElementId: "4:chunk:dup",
            kind: "mention",
            contentHash: "abcd",
            rejectedAt: new Date().toISOString(),
            rejectedBySession: null,
        });
        writeFileSync(path, line + "\n" + line + "\n");
        const undone = await conversationArchiveEnrichRejection({
            accountId,
            archiveElementId: "4:archive:dup",
            chunkElementId: "4:chunk:dup",
            kind: "mention",
            contentHash: "abcd",
            mode: "undo",
        });
        expect(undone.beforeCount).toBe(2);
        // afterCount MUST report 0, not beforeCount - 1.
        expect(undone.afterCount).toBe(0);
        // And the filter side must see no remaining keys.
        expect(loadRejections(accountId).has(rejectionKey("4:chunk:dup", "mention", "abcd"))).toBe(false);
    });
    it("loadRejections returns the recorded key (round-trip with filter side)", async () => {
        const params = {
            accountId: "acct-980-rt",
            archiveElementId: "4:archive:2",
            chunkElementId: "4:chunk:42",
            kind: "preference",
            contentHash: "f00d",
        };
        await conversationArchiveEnrichRejection({
            ...params,
            mode: "record",
        });
        const set = loadRejections(params.accountId);
        expect(set.has(rejectionKey(params.chunkElementId, params.kind, params.contentHash))).toBe(true);
    });
    it("loadRejections skips a malformed line and emits rejection-parse-skip", () => {
        const accountId = "acct-980-corrupt";
        const path = rejectionsJsonlPath(accountId);
        mkdirSync(dirname(path), { recursive: true });
        writeFileSync(path, [
            '{"chunkElementId":"good","kind":"mention","contentHash":"aa"}',
            "{ this is not valid json",
            '{"chunkElementId":"alsogood","kind":"task","contentHash":"bb"}',
        ].join("\n") + "\n");
        const stderrChunks = [];
        const origWrite = process.stderr.write.bind(process.stderr);
        process.stderr.write = (s) => {
            stderrChunks.push(s);
            return true;
        };
        try {
            const set = loadRejections(accountId);
            expect(set.size).toBe(2);
        }
        finally {
            process.stderr.write = origWrite;
        }
        expect(stderrChunks.some((s) => s.includes("rejection-parse-skip"))).toBe(true);
    });
});
//# sourceMappingURL=conversation-archive-enrich-rejection.test.js.map