import path from "node:path";

import * as exec from "@actions/exec";
import * as io from "@actions/io";
import { describe, expect, it, vi } from "vitest";

import { run } from "../clippy";
import type { ParsedInput } from "../input";
import * as report from "../reporter";
import type { CompilerMessage } from "../schema";

vi.mock("@actions/core");

describe("clippy", () => {
    it("runs with cargo", async () => {
        using execSpy = vi.spyOn(exec, "exec").mockResolvedValue(0);

        using whichSpy = vi.spyOn(io, "which").mockImplementation((tool, _check) => {
            return Promise.resolve(tool);
        });

        const actionInput: ParsedInput = {
            toolchain: "stable",
            args: [],
            useCross: false,
            workingDirectory: undefined,
        };

        await expect(run(actionInput)).resolves.toBeUndefined();

        expect(whichSpy).toHaveBeenCalledWith("cargo", true);

        expect(execSpy).toHaveBeenCalledTimes(4);
    });

    it("runs with cross", async () => {
        using execSpy = vi.spyOn(exec, "exec").mockResolvedValue(0);

        using whichSpy = vi.spyOn(io, "which").mockImplementation((tool, _check) => {
            return Promise.resolve(tool);
        });

        const actionInput: ParsedInput = {
            toolchain: "stable",
            args: [],
            useCross: true,
            workingDirectory: undefined,
        };

        await expect(run(actionInput)).resolves.toBeUndefined();

        expect(execSpy).toHaveBeenCalledTimes(4);

        expect(whichSpy).toHaveBeenCalledWith("cross", true);
    });

    it("reports when clippy fails", async () => {
        vi.spyOn(exec, "exec").mockImplementation((_commandline: string, arguments_?: string[]) => {
            const expected = ["clippy", "--message-format=json"];

            if (
                (arguments_ ?? []).length > 0 &&
                expected.every((c) => {
                    return arguments_?.includes(c) ?? false;
                })
            ) {
                return Promise.resolve(101);
            }

            return Promise.resolve(0);
        });

        using whichSpy = vi.spyOn(io, "which").mockImplementation((tool, _check) => {
            return Promise.resolve(tool);
        });

        const actionInput: ParsedInput = {
            toolchain: "stable",
            args: [],
            useCross: false,
            workingDirectory: undefined,
        };

        await expect(run(actionInput)).rejects.toThrow(/Clippy had exited with the (?<exit_code>\d)+ exit code/v);

        expect(whichSpy).toHaveBeenCalledWith("cargo", true);
    });

    it("reports when clippy fails with a non-default working directory", async () => {
        using execSpy = vi.spyOn(exec, "exec").mockImplementation((_commandline: string, arguments_?: string[]) => {
            const expected = ["clippy", "--message-format=json"];

            if (
                (arguments_ ?? []).length > 0 &&
                expected.every((c) => {
                    return arguments_?.includes(c) ?? false;
                })
            ) {
                return Promise.resolve(101);
            }

            return Promise.resolve(0);
        });

        using whichSpy = vi.spyOn(io, "which").mockImplementation((tool, _check) => {
            return Promise.resolve(tool);
        });

        const actionInput: ParsedInput = {
            toolchain: "stable",
            args: [],
            useCross: false,
            workingDirectory: "./my/sources/are/here",
        };

        await expect(run(actionInput)).rejects.toThrow(/Clippy had exited with the (?<exit_code>\d)+ exit code/v);

        expect(whichSpy).toHaveBeenCalledWith("cargo", true);

        const expectedCwd = path.join(process.cwd(), "./my/sources/are/here");

        expect(execSpy).toHaveBeenCalledWith(
            "cargo",
            ["+stable", "clippy", "--message-format=json"],
            expect.objectContaining({ cwd: expectedCwd }),
        );
    });

    it("records versions with toolchain", async () => {
        vi.spyOn(exec, "exec").mockImplementation(
            (commandline: string, arguments_?: string[], options?: exec.ExecOptions) => {
                if (commandline.endsWith("cargo")) {
                    if (arguments_?.[0] === "+nightly" && arguments_[1] === "-V") {
                        options?.listeners?.stdout?.(Buffer.from("cargo version"));
                    } else if (arguments_?.[0] === "+nightly" && arguments_[1] === "clippy" && arguments_[2] === "-V") {
                        options?.listeners?.stdout?.(Buffer.from("clippy version"));
                    }
                } else if (commandline === "rustc" && arguments_?.[0] === "+nightly" && arguments_[1] === "-V") {
                    options?.listeners?.stdout?.(Buffer.from("rustc version"));
                }
                return Promise.resolve(0);
            },
        );

        using reportSpy = vi.spyOn(report, "report");

        using whichSpy = vi.spyOn(io, "which").mockImplementation((tool, _check) => {
            return Promise.resolve(tool);
        });

        const actionInput: ParsedInput = {
            toolchain: "nightly",
            args: [],
            useCross: false,
            workingDirectory: undefined,
        };

        await expect(run(actionInput)).resolves.toBeUndefined();

        expect(reportSpy).toHaveBeenCalledWith({ error: 0, help: 0, ice: 0, note: 0, warning: 0 }, [], {
            cargo: "cargo version",
            clippy: "clippy version",
            rustc: "rustc version",
        });

        expect(whichSpy).toHaveBeenCalledWith("cargo", true);
    });

    it("records versions", async () => {
        vi.spyOn(exec, "exec").mockImplementation(
            (commandline: string, arguments_?: string[], options?: exec.ExecOptions) => {
                if (commandline.endsWith("cargo")) {
                    if (arguments_?.[0] === "-V") {
                        options?.listeners?.stdout?.(Buffer.from("cargo version"));
                    } else if (arguments_?.[0] === "clippy" && arguments_[1] === "-V") {
                        options?.listeners?.stdout?.(Buffer.from("clippy version"));
                    }
                } else if (commandline === "rustc" && arguments_?.[0] === "-V") {
                    options?.listeners?.stdout?.(Buffer.from("rustc version"));
                }
                return Promise.resolve(0);
            },
        );

        using reportSpy = vi.spyOn(report, "report");

        using whichSpy = vi.spyOn(io, "which").mockImplementation((tool, _check) => {
            return Promise.resolve(tool);
        });

        const actionInput: ParsedInput = {
            toolchain: undefined,
            args: [],
            useCross: false,
            workingDirectory: undefined,
        };

        await expect(run(actionInput)).resolves.toBeUndefined();

        expect(whichSpy).toHaveBeenCalledWith("cargo", true);

        expect(reportSpy).toHaveBeenCalledWith({ error: 0, help: 0, ice: 0, note: 0, warning: 0 }, [], {
            cargo: "cargo version",
            clippy: "clippy version",
            rustc: "rustc version",
        });
    });

    it("clippy captures stdout", async () => {
        vi.spyOn(exec, "exec").mockImplementation(
            (_commandline: string, arguments_?: string[], options?: exec.ExecOptions) => {
                const expected = ["clippy", "--message-format=json"];

                if (
                    (arguments_ ?? []).length > 0 &&
                    expected.every((c) => {
                        return arguments_?.includes(c) ?? false;
                    })
                ) {
                    const data: CompilerMessage = {
                        reason: "compiler-message",
                        message: {
                            code: "500",
                            level: "warning",
                            message: "message",
                            rendered: "rendered",
                            spans: [
                                {
                                    is_primary: true,
                                    file_name: "main.rs",
                                    line_start: 12,
                                    line_end: 12,
                                    column_start: 30,
                                    column_end: 45,
                                },
                            ],
                        },
                    };
                    options?.listeners?.stdout?.(Buffer.from(`${JSON.stringify(data)}\n`));
                }

                return Promise.resolve(0);
            },
        );

        using reportSpy = vi.spyOn(report, "report");

        using whichSpy = vi.spyOn(io, "which").mockImplementation((tool, _check) => {
            return Promise.resolve(tool);
        });

        const actionInput: ParsedInput = {
            toolchain: "stable",
            args: [],
            useCross: false,
            workingDirectory: "./my/sources/are/here",
        };

        await expect(run(actionInput)).resolves.toBeUndefined();

        expect(reportSpy).toHaveBeenCalledWith(
            { error: 0, help: 0, ice: 0, note: 0, warning: 1 },
            [
                {
                    level: 1,
                    message: "rendered",
                    properties: {
                        endColumn: 45,
                        endLine: 12,
                        file: "my/sources/are/here/main.rs",
                        startColumn: 30,
                        startLine: 12,
                        title: "message",
                    },
                },
            ],
            {
                cargo: "",
                clippy: "",
                rustc: "",
            },
        );

        expect(whichSpy).toHaveBeenCalledWith("cargo", true);
    });
});
