import { zipFile } from "@/../tests/utils/zip-utils";
import { LoaderType } from "@/loaders/loader-type";
import mockFs from "mock-fs";
import {
    LoaderMetadataReader,
    combineLoaderMetadataReaders,
    createLoaderMetadataReader,
    createDefaultLoaderMetadataReader,
} from "@/loaders/loader-metadata-reader";

beforeEach(async () => {
    mockFs({
        "fabric.jar": await zipFile([__dirname, "../../content/fabric/fabric.mod.json"]),
        "quilt.jar": await zipFile([__dirname, "../../content/quilt/quilt.mod.json"]),
        "forge.jar": await zipFile([__dirname, "../../content/forge/mods.toml"], "META-INF/mods.toml"),
        "neoforge.jar": await zipFile([__dirname, "../../content/neoforge/mods.toml"], "META-INF/mods.toml"),
        "text.txt": "",
    });
});

afterEach(() => {
    mockFs.restore();
});

describe("combineLoaderMetadataReaders", () => {
    test("combined reader returns metadata from the first underlying reader that successfully reads the metadata", async () => {
        const reader1 = { readMetadataFile: jest.fn().mockImplementation(x => x === "1" ? Promise.resolve({ id: "1" }) : Promise.reject(new Error("Unknown id"))) } as LoaderMetadataReader;
        const reader2 = { readMetadataFile: jest.fn().mockImplementation(x => Promise.resolve(x === "2" ? { id: "2" } : undefined)) } as LoaderMetadataReader;

        const combined = combineLoaderMetadataReaders([reader1, reader2]);

        const metadata1 = await combined.readMetadataFile("1");
        expect(metadata1).toEqual({ id: "1" });
        expect(reader1.readMetadataFile).toHaveBeenCalledTimes(1);
        expect(reader1.readMetadataFile).toHaveBeenCalledWith("1");
        expect(reader2.readMetadataFile).not.toHaveBeenCalled();

        const metadata2 = await combined.readMetadataFile("2");
        expect(metadata2).toEqual({ id: "2" });
        expect(reader1.readMetadataFile).toHaveBeenCalledTimes(2);
        expect(reader1.readMetadataFile).toHaveBeenCalledWith("2");
        expect(reader2.readMetadataFile).toHaveBeenCalledTimes(1);
        expect(reader2.readMetadataFile).toHaveBeenCalledWith("2");
    });

    test("combined reader returns undefined when no reader can read the metadata", async () => {
        const combined = combineLoaderMetadataReaders([]);
        const metadata = await combined.readMetadataFile("test");

        expect(metadata).toBeUndefined();
    });

    test("combined reader returns undefined instead of throwing", async () => {
        const reader1 = { readMetadataFile: jest.fn().mockRejectedValue(new Error("Cannot read the metadata file")) } as LoaderMetadataReader;

        const combined = combineLoaderMetadataReaders([reader1]);
        const metadata = await combined.readMetadataFile("test");

        expect(metadata).toBeUndefined();
    });
});

describe("createLoaderMetadataReader", () => {
    test("creates a reader for every known loader", () => {
        for (const loader of LoaderType.values()) {
            expect(createLoaderMetadataReader(loader)).toBeDefined();
        }
    });

    test("created reader is able to read metadata of its supported loader", async () => {
        for (const loader of LoaderType.values()) {
            const reader = createLoaderMetadataReader(loader);
            const metadata = await reader.readMetadataFile(`${loader}.jar`);

            expect(metadata).toBeDefined();
            expect(metadata.version).toBe("0.1.0");
        }
    });

    test("created reader returns undefined for unsupported metadata files", async () => {
        for (const loader of LoaderType.values()) {
            const reader = createLoaderMetadataReader(loader);
            const metadata = await reader.readMetadataFile("text.txt");

            expect(metadata).toBeUndefined();
        }
    });

    test("created reader returns undefined for non-existing files", async () => {
        for (const loader of LoaderType.values()) {
            const reader = createLoaderMetadataReader(loader);
            const metadata = await reader.readMetadataFile("text.json");

            expect(metadata).toBeUndefined();
        }
    });

    test("throws an error when unknown loader is provided", () => {
        expect(() => createLoaderMetadataReader("unknown" as LoaderType)).toThrow("Unknown mod loader 'unknown'.");
    });
});

describe("createDefaultLoaderMetadataReader", () => {
    test("creates a reader that can read metadata from all known loaders", async () => {
        const reader = createDefaultLoaderMetadataReader();
        for (const loader of LoaderType.values()) {
            const metadata = await reader.readMetadataFile(`${loader}.jar`);

            expect(metadata).toBeDefined();
            expect(metadata.version).toBe("0.1.0");
        }
    });
});