import { statSync } from "node:fs";
import mockFs from "mock-fs";
import {
    FileInfo,
    fileEquals,
    findFiles,
    findFilesSync,
    readAllText,
    readAllTextSync,
    readFile,
    readFileSync,
} from "@/utils/io/file-info";

beforeEach(() => {
    mockFs({
        "path/to": {
            "test.txt": "test",
            "test.json": JSON.stringify({ foo: 42 }),
        },
    });
});

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

describe("FileInfo", () => {
    describe("constructor", () => {
        test("constructs a new instance with the given path", () => {
            const info = new FileInfo("path/to/test.txt");

            expect(info.path).toBe("path/to/test.txt");
        });
    });

    describe("of", () => {
        test("constructs a new instance from the given path", () => {
            const info = FileInfo.of("test.txt");

            expect(info).toBeInstanceOf(FileInfo);
            expect(info.path).toBe("test.txt");
        });

        test("returns the same instance for a FileInfo object", () => {
            const info1 = new FileInfo("test.txt");
            const info2 = FileInfo.of(info1);

            expect(info2).toBe(info1);
        });
    });

    describe("name", () => {
        test("returns the file name", () => {
            const info = new FileInfo("path/to/test.txt");

            expect(info.name).toBe("test.txt");
        });
    });

    describe("directoryName", () => {
        test("returns the directory name of the file", () => {
            const info = new FileInfo("path/to/test.txt");

            expect(info.directoryName).toBe("path/to");
        });
    });

    describe("path", () => {
        test("returns the file path", () => {
            const info = new FileInfo("path/to/test.txt");

            expect(info.path).toBe("path/to/test.txt");
        });
    });

    describe("exists", () => {
        test("returns true for existing files", () => {
            const info = new FileInfo("path/to/test.txt");

            expect(info.exists).toBe(true);
        });

        test("returns false for non-existing files", () => {
            const info = new FileInfo("path/to/not-test.txt");

            expect(info.exists).toBe(false);
        });
    });

    describe("size", () => {
        test("returns the file size", () => {
            const info = new FileInfo("path/to/test.txt");

            expect(info.size).toBe(statSync("path/to/test.txt").size);
        });

        test("throws if the file does not exist", () => {
            const info = new FileInfo("path/to/not-test.txt");

            expect(() => info.size).toThrow();
        });
    });

    describe("stream", () => {
        test("creates a readable stream", () => {
            const info = new FileInfo("path/to/test.txt");
            const stream = info.stream();

            expect(stream).toBeDefined();
            expect(stream.readable).toBe(true);

            stream.close();
        });
    });

    describe("buffer", () => {
        test("returns a promise that resolves to a buffer", async () => {
            const info = new FileInfo("path/to/test.txt");
            const buffer = await info.buffer();

            expect(Buffer.isBuffer(buffer)).toBe(true);
            expect(buffer.toString()).toBe("test");
        });

        test("throws if the file does not exist", async () => {
            const info = new FileInfo("path/to/not-test.txt");

            await expect(info.buffer()).rejects.toThrow();
        });
    });

    describe("text", () => {
        test("returns a promise that resolves to a string", async () => {
            const info = new FileInfo("path/to/test.txt");
            const text = await info.text();

            expect(text).toBe("test");
        });

        test("throws if the file does not exist", async () => {
            const info = new FileInfo("path/to/not-test.txt");

            await expect(info.text()).rejects.toThrow();
        });
    });

    describe("json", () => {
        test("returns a promise that resolves to a json object", async () => {
            const info = new FileInfo("path/to/test.json");
            const json = await info.json();

            expect(json).toEqual({ foo: 42 });
        });

        test("throws if the file does not exist", async () => {
            const info = new FileInfo("path/to/not-test.json");

            await expect(info.json()).rejects.toThrow();
        });
    });

    describe("toString", () => {
        test("returns the file path", () => {
            const info = new FileInfo("path/to/test.txt");

            expect(info.toString()).toBe("path/to/test.txt");
        });
    });

    test("should be converted to JSON as a file path string", () => {
        const info = new FileInfo("path/to/test.txt");

        expect(JSON.stringify(info)).toBe("\"path/to/test.txt\"");
    });
});

describe("fileEquals", () => {
    test("returns true for equal file paths", () => {
        expect(fileEquals("path/to/test.txt", "path/to/test.txt")).toBe(true);
        expect(fileEquals(FileInfo.of("path/to/test.txt"), "path/to/test.txt")).toBe(true);
        expect(fileEquals("path/to/test.txt", FileInfo.of("path/to/test.txt"))).toBe(true);
        expect(fileEquals(FileInfo.of("path/to/test.txt"), FileInfo.of("path/to/test.txt"))).toBe(true);
    });

    test("returns false for different file paths", () => {
        expect(fileEquals("path/to/test.txt", "path/to/not-test.txt")).toBe(false);
        expect(fileEquals(FileInfo.of("path/to/test.txt"), "path/to/not-test.txt")).toBe(false);
        expect(fileEquals("path/to/test.txt", FileInfo.of("path/to/not-test.txt"))).toBe(false);
        expect(fileEquals(FileInfo.of("path/to/test.txt"), FileInfo.of("path/to/not-test.txt"))).toBe(false);
    });
});

describe("findFiles", () => {
    test("returns matching files for given pattern", async () => {
        const files = await findFiles("path/to/test.*");
        const paths = files.map(file => file.path);

        expect(paths).toEqual(expect.arrayContaining([
            "path/to/test.txt",
            "path/to/test.json",
        ]));
    });

    test("respects the order of the given patterns", async () => {
        const paths = ["path/to/test.json", "path/to/test.txt"];
        const variants = [paths, [...paths].reverse()];
        for (const variant of variants) {
            const files = await findFiles(variant);
            expect(files.map(x => x.path)).toEqual(variant);
        }
    });
});

describe("findFilesSync", () => {
    test("returns matching files for given pattern", () => {
        const files = findFilesSync("path/to/test.*");
        const paths = files.map(file => file.path);

        expect(paths).toEqual(expect.arrayContaining([
            "path/to/test.txt",
            "path/to/test.json",
        ]));
    });

    test("respects the order of the given patterns", () => {
        const paths = ["path/to/test.json", "path/to/test.txt"];
        const variants = [paths, [...paths].reverse()];
        for (const variant of variants) {
            const files = findFilesSync(variant);
            expect(files.map(x => x.path)).toEqual(variant);
        }
    });
});

describe("readFile", () => {
    test("reads the contents of the first matching file", async () => {
        const content = await readFile("path/to/*.txt");

        expect(Buffer.isBuffer(content)).toBe(true);
        expect(content.toString()).toEqual("test");
    });

    test("throws if no files were found", async () => {
        await expect(readFile("path/from/*.txt")).rejects.toThrow(/path\/from\/\*\.txt/);
    });
});

describe("readFileSync", () => {
    test("reads the contents of the first matching file", () => {
        const content = readFileSync("path/to/*.txt");

        expect(Buffer.isBuffer(content)).toBe(true);
        expect(content.toString()).toEqual("test");
    });

    test("throws if no files were found", () => {
        expect(() => readFileSync("path/from/*.txt")).toThrow(/path\/from\/\*\.txt/);
    });
});

describe("readAllText", () => {
    test("reads the contents of the first matching file as text", async () => {
        const content = await readAllText("path/to/*.txt");

        expect(content).toEqual("test");
    });

    test("throws if no files were found", async () => {
        await expect(readAllText("path/from/*.txt")).rejects.toThrow(/path\/from\/\*\.txt/);
    });
});

describe("readAllTextSync", () => {
    test("reads the contents of the first matching file as text", () => {
        const content = readAllTextSync("path/to/*.txt");

        expect(content).toEqual("test");
    });

    test("throws if no files were found", () => {
        expect(() => readAllTextSync("path/from/*.txt")).toThrow(/path\/from\/\*\.txt/);
    });
});