import { describe, expect, test, mock, beforeEach, afterAll } from "bun:test";

// Mock child_process BEFORE importing the service
const mockExec = mock((cmd: string, callback?: any) => {
    // Handle calls without callback (like exec().unref())
    if (!callback) {
        return { unref: () => { } };
    }

    // Simulate successful command execution
    let stdout = "";
    
    if (cmd.includes("git rev-parse --abbrev-ref")) {
        stdout = "main\n";
    } else if (cmd.includes("git rev-parse --short")) {
        stdout = "abc1234\n";
    } else if (cmd.includes("git rev-parse HEAD")) {
        stdout = "abc1234567890\n";
    } else if (cmd.includes("git fetch")) {
        stdout = "";
    } else if (cmd.includes("git log")) {
        stdout = "abcdef|Update 1|Author1\n123456|Update 2|Author2";
    } else if (cmd.includes("git diff")) {
        stdout = "package.json\nsrc/index.ts\nshared/lib/schema.ts";
    } else if (cmd.includes("git reset")) {
        stdout = "HEAD is now at abcdef Update 1";
    } else if (cmd.includes("bun install")) {
        stdout = "Installed dependencies";
    } else if (cmd.includes("drizzle-kit migrate")) {
        stdout = "Migrations applied";
    } else if (cmd.includes("bun run build")) {
        stdout = "Build completed";
    }

    callback(null, stdout, "");
});

mock.module("child_process", () => ({
    exec: mockExec
}));

// Mock fs/promises
const mockWriteFile = mock((_path: string, _content: string) => Promise.resolve());
const mockReadFile = mock((_path: string, _encoding: string) => Promise.resolve("{}"));
const mockUnlink = mock((_path: string) => Promise.resolve());

mock.module("fs/promises", () => ({
    writeFile: mockWriteFile,
    readFile: mockReadFile,
    unlink: mockUnlink
}));

// Mock view module to avoid import issues
mock.module("@/modules/admin/update.view", () => ({
    getPostRestartEmbed: () => ({ embeds: [{ title: "Update Complete" }], components: [] }),
    getPostRestartProgressEmbed: () => ({ title: "Progress..." }),
}));

describe("UpdateService", () => {
    let UpdateService: any;

    beforeEach(async () => {
        mockExec.mockClear();
        mockWriteFile.mockClear();
        mockReadFile.mockClear();
        mockUnlink.mockClear();

        // Dynamically import to ensure mock is used
        const module = await import("./update.service");
        UpdateService = module.UpdateService;
    });

    afterAll(() => {
        mock.restore();
    });

    describe("checkForUpdates", () => {
        test("should return updates if git log has output", async () => {
            const result = await UpdateService.checkForUpdates();

            expect(result.hasUpdates).toBe(true);
            expect(result.branch).toBe("main");
            expect(result.commits.length).toBeGreaterThan(0);
            expect(result.commits[0].message).toContain("Update 1");
        });

        test("should call git rev-parse, fetch, and log commands", async () => {
            await UpdateService.checkForUpdates();

            const calls = mockExec.mock.calls.map((c: any) => c[0]);
            expect(calls.some((cmd: string) => cmd.includes("git rev-parse"))).toBe(true);
            expect(calls.some((cmd: string) => cmd.includes("git fetch"))).toBe(true);
            expect(calls.some((cmd: string) => cmd.includes("git log"))).toBe(true);
        });

        test("should include requirements in the response", async () => {
            const result = await UpdateService.checkForUpdates();

            expect(result.requirements).toBeDefined();
            expect(result.requirements.needsRootInstall).toBe(true); // package.json is in mock
            expect(result.requirements.needsMigrations).toBe(true); // schema.ts is in mock
            expect(result.requirements.changedFiles).toContain("package.json");
        });
    });

    describe("performUpdate", () => {
        test("should run git reset --hard with correct branch", async () => {
            await UpdateService.performUpdate("main");

            const calls = mockExec.mock.calls.map((c: any) => c[0]);
            expect(calls.some((cmd: string) => cmd.includes("git reset --hard origin/main"))).toBe(true);
        });
    });

    describe("checkUpdateRequirements (deprecated)", () => {
        test("should detect package.json and schema.ts changes", async () => {
            const result = await UpdateService.checkUpdateRequirements("main");

            expect(result.needsRootInstall).toBe(true);
            expect(result.needsMigrations).toBe(true);
            expect(result.error).toBeUndefined();
        });

        test("should call git diff with correct branch", async () => {
            await UpdateService.checkUpdateRequirements("develop");

            const calls = mockExec.mock.calls.map((c: any) => c[0]);
            expect(calls.some((cmd: string) => cmd.includes("git diff HEAD..origin/develop"))).toBe(true);
        });
    });

    describe("installDependencies", () => {
        test("should run bun install for root only", async () => {
            const output = await UpdateService.installDependencies({ root: true, web: false });

            expect(output).toContain("Root");
            const calls = mockExec.mock.calls.map((c: any) => c[0]);
            expect(calls.some((cmd: string) => cmd === "bun install")).toBe(true);
        });

        test("should run bun install for both root and web in parallel", async () => {
            const output = await UpdateService.installDependencies({ root: true, web: true });

            expect(output).toContain("Root");
            expect(output).toContain("Web");
            const calls = mockExec.mock.calls.map((c: any) => c[0]);
            expect(calls.some((cmd: string) => cmd === "bun install")).toBe(true);
            expect(calls.some((cmd: string) => cmd.includes("cd web && bun install"))).toBe(true);
        });
    });

    describe("categorizeChanges", () => {
        test("should categorize files correctly", () => {
            const files = [
                "bot/commands/admin/update.ts",
                "bot/modules/admin/update.view.ts",
                "web/src/components/Button.tsx",
                "shared/lib/utils.ts",
                "package.json",
                "drizzle/0001_migration.sql"
            ];

            const categories = UpdateService.categorizeChanges(files);

            expect(categories["Commands"]).toBe(1);
            expect(categories["Modules"]).toBe(1);
            expect(categories["Web Dashboard"]).toBe(1);
            expect(categories["Library"]).toBe(1);
            expect(categories["Dependencies"]).toBe(1);
            expect(categories["Database"]).toBe(1);
        });
    });

    describe("prepareRestartContext", () => {
        test("should write context to file", async () => {
            const context = {
                channelId: "123",
                userId: "456",
                timestamp: Date.now(),
                runMigrations: true,
                installDependencies: false,
                buildWebAssets: false,
                previousCommit: "abc1234",
                newCommit: "def5678"
            };

            await UpdateService.prepareRestartContext(context);

            expect(mockWriteFile).toHaveBeenCalled();
            const lastCall = mockWriteFile.mock.lastCall as [string, string] | undefined;
            expect(lastCall).toBeDefined();
            expect(lastCall![0]).toContain("restart_context");
            expect(JSON.parse(lastCall![1])).toEqual(context);
        });
    });

    describe("saveRollbackPoint", () => {
        test("should save current commit hash to file", async () => {
            const commit = await UpdateService.saveRollbackPoint();

            expect(commit).toBeTruthy();
            expect(mockWriteFile).toHaveBeenCalled();
            const lastCall = mockWriteFile.mock.lastCall as [string, string] | undefined;
            expect(lastCall![0]).toContain("rollback_commit");
        });
    });

    describe("hasRollbackPoint", () => {
        test("should return true when rollback file exists", async () => {
            mockReadFile.mockImplementationOnce(() => Promise.resolve("abc123"));
            
            // Clear cache first
            (UpdateService as any).rollbackPointExists = null;
            
            const result = await UpdateService.hasRollbackPoint();
            expect(result).toBe(true);
        });

        test("should return false when rollback file does not exist", async () => {
            mockReadFile.mockImplementationOnce(() => Promise.reject(new Error("ENOENT")));
            
            // Clear cache first
            (UpdateService as any).rollbackPointExists = null;
            
            const result = await UpdateService.hasRollbackPoint();
            expect(result).toBe(false);
        });
    });

    describe("triggerRestart", () => {
        test("should use RESTART_COMMAND env var when set", async () => {
            const originalEnv = process.env.RESTART_COMMAND;
            process.env.RESTART_COMMAND = "pm2 restart bot";

            await UpdateService.triggerRestart();

            const calls = mockExec.mock.calls.map((c: any) => c[0]);
            expect(calls.some((cmd: string) => cmd === "pm2 restart bot")).toBe(true);

            process.env.RESTART_COMMAND = originalEnv;
        });

        test("should call process.exit when no env var is set", async () => {
            const originalEnv = process.env.RESTART_COMMAND;
            delete process.env.RESTART_COMMAND;

            // Just verify it doesn't throw - actual process.exit is mocked by setTimeout
            await UpdateService.triggerRestart();

            process.env.RESTART_COMMAND = originalEnv;
        });
    });

    describe("handlePostRestart", () => {
        const createMockClient = (channel: any = null) => ({
            channels: {
                fetch: mock(() => Promise.resolve(channel))
            }
        });

        const createMockChannel = () => ({
            isSendable: () => true,
            send: mock(() => Promise.resolve({ edit: mock(() => Promise.resolve()), delete: mock(() => Promise.resolve()) }))
        });

        test("should ignore stale context (>10 mins old)", async () => {
            const staleContext = {
                channelId: "123",
                userId: "456",
                timestamp: Date.now() - (15 * 60 * 1000), // 15 mins ago
                runMigrations: true,
                installDependencies: true,
                buildWebAssets: false,
                previousCommit: "abc",
                newCommit: "def"
            };

            mockReadFile.mockImplementationOnce(() => Promise.resolve(JSON.stringify(staleContext)));

            const mockChannel = createMockChannel();
            // Create mock with instanceof support
            const channel = Object.assign(mockChannel, { constructor: { name: "TextChannel" } });
            Object.setPrototypeOf(channel, Object.create({ constructor: { name: "TextChannel" } }));

            const mockClient = createMockClient(channel);

            await UpdateService.handlePostRestart(mockClient);

            // Should not send any message for stale context
            expect(mockChannel.send).not.toHaveBeenCalled();
            // Should clean up the context file
            expect(mockUnlink).toHaveBeenCalled();
        });

        test("should do nothing if no context file exists", async () => {
            mockReadFile.mockImplementationOnce(() => Promise.reject(new Error("ENOENT")));

            const mockClient = createMockClient();

            await UpdateService.handlePostRestart(mockClient);

            // Should not throw and not try to clean up
            expect(mockUnlink).not.toHaveBeenCalled();
        });

        test("should clean up context file after processing", async () => {
            const validContext = {
                channelId: "123",
                userId: "456",
                timestamp: Date.now(),
                runMigrations: false,
                installDependencies: false,
                buildWebAssets: false,
                previousCommit: "abc",
                newCommit: "def"
            };

            mockReadFile.mockImplementationOnce(() => Promise.resolve(JSON.stringify(validContext)));

            // Create a proper TextChannel mock
            const { TextChannel } = await import("discord.js");
            const mockChannel = Object.create(TextChannel.prototype);
            mockChannel.isSendable = () => true;
            mockChannel.send = mock(() => Promise.resolve({ edit: mock(() => Promise.resolve()), delete: mock(() => Promise.resolve()) }));

            const mockClient = createMockClient(mockChannel);

            await UpdateService.handlePostRestart(mockClient);

            expect(mockUnlink).toHaveBeenCalled();
        });
    });
});
