
import { describe, it, expect, mock, beforeEach } from "bun:test";
import { userService } from "@shared/modules/user/user.service";

// Define mock functions outside so we can control them in tests
const mockFindFirst = mock();
const mockInsert = mock();
const mockUpdate = mock();
const mockDelete = mock();
const mockValues = mock();
const mockReturning = mock();
const mockSet = mock();
const mockWhere = mock();

// Chainable mock setup
mockInsert.mockReturnValue({ values: mockValues });
mockValues.mockReturnValue({ returning: mockReturning });

mockUpdate.mockReturnValue({ set: mockSet });
mockSet.mockReturnValue({ where: mockWhere });
mockWhere.mockReturnValue({ returning: mockReturning });

mockDelete.mockReturnValue({ where: mockWhere });

// Mock DrizzleClient
mock.module("@shared/db/DrizzleClient", () => {
    return {
        DrizzleClient: {
            query: {
                users: {
                    findFirst: mockFindFirst,
                },
            },
            insert: mockInsert,
            update: mockUpdate,
            delete: mockDelete,
            transaction: async (cb: any) => {
                // Pass the mock client itself as the transaction object
                // This simplifies things as we use the same structure for tx and client
                return cb({
                    query: {
                        users: {
                            findFirst: mockFindFirst,
                        },
                    },
                    insert: mockInsert,
                    update: mockUpdate,
                    delete: mockDelete,
                });
            }
        },
    };
});

// Mock withTransaction helper to use the same pattern as DrizzleClient.transaction
mock.module("@/lib/db", () => {
    return {
        withTransaction: async (callback: any, tx?: any) => {
            if (tx) {
                return callback(tx);
            }
            // Simulate transaction by calling the callback with mock db
            return callback({
                query: {
                    users: {
                        findFirst: mockFindFirst,
                    },
                },
                insert: mockInsert,
                update: mockUpdate,
                delete: mockDelete,
            });
        }
    };
});


describe("userService", () => {
    beforeEach(() => {
        mockFindFirst.mockReset();
        mockInsert.mockClear();
        mockValues.mockClear();
        mockReturning.mockClear();
        mockUpdate.mockClear();
        mockSet.mockClear();
        mockWhere.mockClear();
        mockDelete.mockClear();
    });

    describe("getUserById", () => {
        it("should return a user when found", async () => {
            const mockUser = { id: 123n, username: "testuser", class: null };
            mockFindFirst.mockResolvedValue(mockUser);

            const result = await userService.getUserById("123");

            expect(result).toEqual(mockUser as any);
            expect(mockFindFirst).toHaveBeenCalledTimes(1);
        });

        it("should return undefined when user not found", async () => {
            mockFindFirst.mockResolvedValue(undefined);

            const result = await userService.getUserById("999");

            expect(result).toBeUndefined();
            expect(mockFindFirst).toHaveBeenCalledTimes(1);
        });
    });

    describe("getUserByUsername", () => {
        it("should return user when username exists", async () => {
            const mockUser = { id: 456n, username: "alice", balance: 100n };
            mockFindFirst.mockResolvedValue(mockUser);

            const result = await userService.getUserByUsername("alice");

            expect(result).toEqual(mockUser as any);
            expect(mockFindFirst).toHaveBeenCalledTimes(1);
        });

        it("should return undefined when username not found", async () => {
            mockFindFirst.mockResolvedValue(undefined);

            const result = await userService.getUserByUsername("nonexistent");

            expect(result).toBeUndefined();
        });
    });

    describe("getUserClass", () => {
        it("should return user class when user has a class", async () => {
            const mockClass = { id: 1n, name: "Warrior", emoji: "⚔️" };
            const mockUser = { id: 123n, username: "testuser", class: mockClass };
            mockFindFirst.mockResolvedValue(mockUser);

            const result = await userService.getUserClass("123");

            expect(result).toEqual(mockClass as any);
        });

        it("should return null when user has no class", async () => {
            const mockUser = { id: 123n, username: "testuser", class: null };
            mockFindFirst.mockResolvedValue(mockUser);

            const result = await userService.getUserClass("123");

            expect(result).toBeNull();
        });

        it("should return undefined when user not found", async () => {
            mockFindFirst.mockResolvedValue(undefined);

            const result = await userService.getUserClass("999");

            expect(result).toBeUndefined();
        });
    });

    describe("getOrCreateUser (withTransaction)", () => {
        it("should return existing user if found", async () => {
            const mockUser = { id: 123n, username: "existinguser", class: null };
            mockFindFirst.mockResolvedValue(mockUser);

            const result = await userService.getOrCreateUser("123", "existinguser");

            expect(result).toEqual(mockUser as any);
            expect(mockFindFirst).toHaveBeenCalledTimes(1);
            expect(mockInsert).not.toHaveBeenCalled();
        });

        it("should create new user if not found", async () => {
            const newUser = { id: 789n, username: "newuser", classId: null };

            // First call returns undefined (user not found)
            // Second call returns the newly created user (after insert + re-query)
            mockFindFirst
                .mockResolvedValueOnce(undefined)
                .mockResolvedValueOnce({ id: 789n, username: "newuser", class: null });

            mockReturning.mockResolvedValue([newUser]);

            const result = await userService.getOrCreateUser("789", "newuser");

            expect(mockInsert).toHaveBeenCalledTimes(1);
            expect(mockValues).toHaveBeenCalledWith({
                id: 789n,
                username: "newuser"
            });
            // Should query twice: once to check, once after insert
            expect(mockFindFirst).toHaveBeenCalledTimes(2);
        });
    });

    describe("createUser (withTransaction)", () => {
        it("should create and return a new user", async () => {
            const newUser = { id: 456n, username: "newuser", classId: null };
            mockReturning.mockResolvedValue([newUser]);

            const result = await userService.createUser("456", "newuser");

            expect(result).toEqual(newUser as any);
            expect(mockInsert).toHaveBeenCalledTimes(1);
            expect(mockValues).toHaveBeenCalledWith({
                id: 456n,
                username: "newuser",
                classId: undefined
            });
        });

        it("should create user with classId when provided", async () => {
            const newUser = { id: 999n, username: "warrior", classId: 5n };
            mockReturning.mockResolvedValue([newUser]);

            const result = await userService.createUser("999", "warrior", 5n);

            expect(result).toEqual(newUser as any);
            expect(mockValues).toHaveBeenCalledWith({
                id: 999n,
                username: "warrior",
                classId: 5n
            });
        });
    });

    describe("updateUser (withTransaction)", () => {
        it("should update user data", async () => {
            const updatedUser = { id: 123n, username: "testuser", balance: 500n };
            mockReturning.mockResolvedValue([updatedUser]);

            const result = await userService.updateUser("123", { balance: 500n });

            expect(result).toEqual(updatedUser as any);
            expect(mockUpdate).toHaveBeenCalledTimes(1);
            expect(mockSet).toHaveBeenCalledWith({ balance: 500n });
        });

        it("should update multiple fields", async () => {
            const updatedUser = { id: 456n, username: "alice", xp: 100n, level: 5 };
            mockReturning.mockResolvedValue([updatedUser]);

            const result = await userService.updateUser("456", { xp: 100n, level: 5 });

            expect(result).toEqual(updatedUser as any);
            expect(mockSet).toHaveBeenCalledWith({ xp: 100n, level: 5 });
        });
    });

    describe("deleteUser (withTransaction)", () => {
        it("should delete user from database", async () => {
            mockWhere.mockResolvedValue(undefined);

            await userService.deleteUser("123");

            expect(mockDelete).toHaveBeenCalledTimes(1);
            expect(mockWhere).toHaveBeenCalledTimes(1);
        });
    });
});
