import { describe, expect, it, vi } from "vitest";
import {
    discardLiveBranch,
    discardUnreadableLiveRevision,
    inspectFileRepair,
} from "./fileRepair";

function createCore() {
    const current = {
        _id: "f:note",
        _rev: "3-current",
        _conflicts: ["2-conflict"],
        _revs_info: [{ rev: "3-current", status: "available" }],
        path: "note.md",
        ctime: 1,
        mtime: 3,
        size: 7,
        type: "plain",
        children: ["h:current"],
        deleted: false,
        eden: {},
    };
    const conflict = {
        ...current,
        _rev: "2-conflict",
        _conflicts: undefined,
        _revs_info: [{ rev: "2-conflict", status: "available" }],
        mtime: 2,
        children: ["h:missing-conflict"],
    };
    const deleteRevisionFromDB = vi.fn(async () => true);
    const core = {
        settings: {
            syncInternalFiles: false,
            syncInternalFilesIgnorePatterns: "",
            syncInternalFilesTargetPatterns: "",
        },
        storageAccess: {
            isExistsIncludeHidden: vi.fn(async () => true),
            statHidden: vi.fn(async () => ({
                ctime: 1,
                mtime: 3,
                size: 7,
                type: "file",
            })),
            readHiddenFileBinary: vi.fn(async () => new TextEncoder().encode("current").buffer),
            getFileNames: vi.fn(async () => ["note.md"]),
            getFilesIncludeHidden: vi.fn(async () => ["note.md"]),
        },
        localDatabase: {
            localDatabase: {
                get: vi.fn(async (_id: string, options?: { rev?: string }) =>
                    options?.rev === "2-conflict" ? conflict : current
                ),
            },
            allDocsRaw: vi.fn(async ({ keys }: { keys: string[] }) => ({
                rows: keys.includes("h:current")
                    ? [
                          {
                              id: "h:current",
                              key: "h:current",
                              value: { rev: "1-current" },
                          },
                      ]
                    : [],
            })),
            getDBEntryFromMeta: vi.fn(async (meta: typeof current) => ({
                ...meta,
                data: [meta._rev === "3-current" ? "current" : "conflict"],
            })),
            getDBEntry: vi.fn(async () => false),
            findAllDocs: vi.fn(async function* () {
                yield current;
            }),
        },
        fileHandler: {
            deleteRevisionFromDB,
        },
        services: {
            path: {
                path2id: vi.fn(async () => "f:note"),
            },
            UI: {
                confirm: {},
            },
        },
    };
    return {
        conflict,
        core,
        current,
        deleteRevisionFromDB,
    };
}

describe("file repair inspection", () => {
    it("shows the winner and every conflict revision independently", async () => {
        const { core } = createCore();

        const inspection = await inspectFileRepair(core as never, "note.md");

        expect(inspection.revisions).toEqual([
            expect.objectContaining({
                role: "winner",
                contentReadable: true,
                contentMatchesStorage: true,
                metadata: expect.objectContaining({
                    revision: "3-current",
                }),
            }),
            expect.objectContaining({
                role: "conflict",
                contentReadable: false,
                contentMatchesStorage: null,
                metadata: expect.objectContaining({
                    revision: "2-conflict",
                }),
            }),
        ]);
        expect(inspection.requiresAttention).toBe(true);
    });

    it("omits a logical deletion which already matches an absent Vault file", async () => {
        const { core, current } = createCore();
        current.deleted = true;
        current._conflicts = [];
        current.children = [];
        core.storageAccess.isExistsIncludeHidden.mockResolvedValue(false);
        core.storageAccess.statHidden.mockResolvedValue(null as never);

        const inspection = await inspectFileRepair(core as never, "note.md");

        expect(inspection.revisions).toEqual([
            expect.objectContaining({
                role: "winner",
                contentReadable: true,
                metadata: expect.objectContaining({
                    deleted: true,
                    revision: "3-current",
                }),
            }),
        ]);
        expect(inspection.requiresAttention).toBe(false);
    });

    it("rechecks liveness and readability before discarding an exact revision", async () => {
        const { core, deleteRevisionFromDB } = createCore();

        await expect(
            discardUnreadableLiveRevision(core as never, "note.md", "2-conflict")
        ).resolves.toBe("discarded");
        await expect(
            discardUnreadableLiveRevision(core as never, "note.md", "3-current")
        ).resolves.toBe("revision-is-readable");

        expect(deleteRevisionFromDB).toHaveBeenCalledOnce();
        expect(deleteRevisionFromDB).toHaveBeenCalledWith("note.md", "2-conflict");
    });

    it("allows an exact unreadable generation-one winner to be discarded explicitly", async () => {
        const { core, current, deleteRevisionFromDB } = createCore();
        current._rev = "1-root";
        current._conflicts = [];
        current.children = ["h:missing-root"];
        core.localDatabase.allDocsRaw.mockResolvedValue({ rows: [] });

        const inspection = await inspectFileRepair(core as never, "note.md");

        expect(inspection.revisions).toEqual([
            expect.objectContaining({
                role: "winner",
                contentReadable: false,
                metadata: expect.objectContaining({
                    revision: "1-root",
                }),
            }),
        ]);
        await expect(
            discardUnreadableLiveRevision(core as never, "note.md", "1-root")
        ).resolves.toBe("discarded");
        expect(deleteRevisionFromDB).toHaveBeenCalledWith("note.md", "1-root");
    });

    it("refuses to discard a revision which stopped being a live leaf", async () => {
        const { core, current, deleteRevisionFromDB } = createCore();
        core.localDatabase.localDatabase.get.mockResolvedValue({
            ...current,
            _conflicts: [],
        });

        await expect(
            discardUnreadableLiveRevision(core as never, "note.md", "2-conflict")
        ).resolves.toBe("no-longer-live");

        expect(deleteRevisionFromDB).not.toHaveBeenCalled();
    });

    it("discards an exact readable winner while another live branch remains", async () => {
        const { core, deleteRevisionFromDB } = createCore();

        await expect(
            discardLiveBranch(core as never, "note.md", "3-current")
        ).resolves.toBe("discarded");

        expect(deleteRevisionFromDB).toHaveBeenCalledWith("note.md", "3-current");
    });

    it("refuses to discard the only live branch", async () => {
        const { core, current, deleteRevisionFromDB } = createCore();
        current._conflicts = [];

        await expect(
            discardLiveBranch(core as never, "note.md", "3-current")
        ).resolves.toBe("only-live-revision");

        expect(deleteRevisionFromDB).not.toHaveBeenCalled();
    });

    it("refuses to discard a branch which is no longer live", async () => {
        const { core, deleteRevisionFromDB } = createCore();

        await expect(
            discardLiveBranch(core as never, "note.md", "1-stale")
        ).resolves.toBe("no-longer-live");

        expect(deleteRevisionFromDB).not.toHaveBeenCalled();
    });
});
