import { describe, it, expect, mock, beforeEach, afterEach, setSystemTime } from "bun:test";
import { levelingService } from "@shared/modules/leveling/leveling.service";
import { users, userTimers } from "@db/schema";

// Mock dependencies
const mockFindFirst = mock();
const mockUpdate = mock();
const mockSet = mock();
const mockWhere = mock();
const mockReturning = mock();
const mockInsert = mock();
const mockValues = mock();
const mockOnConflictDoUpdate = mock();

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

mockInsert.mockReturnValue({ values: mockValues });
mockValues.mockReturnValue({ onConflictDoUpdate: mockOnConflictDoUpdate });
mockOnConflictDoUpdate.mockResolvedValue({});

mock.module("@shared/db/DrizzleClient", () => {
    const createMockTx = () => ({
        query: {
            users: { findFirst: mockFindFirst },
            userTimers: { findFirst: mockFindFirst },
        },
        update: mockUpdate,
        insert: mockInsert,
    });

    return {
        DrizzleClient: {
            ...createMockTx(),
            transaction: async (cb: any) => cb(createMockTx()),
        }
    };
});

mock.module("@shared/lib/config", () => ({
    config: {
        leveling: {
            base: 100,
            exponent: 1.5,
            chat: {
                minXp: 10,
                maxXp: 20,
                cooldownMs: 60000
            }
        }
    }
}));

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

    beforeEach(() => {
        mockFindFirst.mockReset();
        mockUpdate.mockClear();
        mockSet.mockClear();
        mockWhere.mockClear();
        mockReturning.mockClear();
        mockInsert.mockClear();
        mockValues.mockClear();
        mockOnConflictDoUpdate.mockClear();
        originalRandom = Math.random;
    });

    afterEach(() => {
        Math.random = originalRandom;
    });

    describe("getXpForLevel", () => {
        it("should calculate correct XP", () => {
            // base 100, exp 1.5
            // lvl 1: 100 * 1^1.5 = 100
            // lvl 2: 100 * 2^1.5 = 100 * 2.828 = 282
            expect(levelingService.getXpForNextLevel(1)).toBe(100);
            expect(levelingService.getXpForNextLevel(2)).toBe(282);
        });
    });

    describe("addXp", () => {
        it("should add XP without level up", async () => {
            // User current: level 1, xp 0
            // Add 50
            // Next level (1) needed: 100. (Note: Logic in service seems to use currentLevel for calculation of next step.
            // Service implementation:
            // let xpForNextLevel = ... getXpForLevel(currentLevel)
            // wait, if I am level 1, I need X XP to reach level 2?
            // Service code: 
            // while (newXp >= xpForNextLevel) { ... currentLevel++ }
            // So if I am level 1, calling getXpForLevel(1) returns 100.
            // If I have 100 XP, I level up to 2.

            mockFindFirst.mockResolvedValue({ xp: 0n, level: 1 });
            mockReturning.mockResolvedValue([{ xp: 50n, level: 1 }]);

            const result = await levelingService.addXp("1", 50n);

            expect(result.levelUp).toBe(false);
            expect(result.currentLevel).toBe(1);
            expect(mockUpdate).toHaveBeenCalledWith(users);
            expect(mockSet).toHaveBeenCalledWith({ xp: 50n, level: 1 });
        });

        it("should level up if XP sufficient", async () => {
            // Current: Lvl 1, XP 0. Next Lvl needed: 100.
            // Add 120.
            // newXp = 120.
            // 120 >= 100.
            // newXp -= 100 -> 20.
            // currentLevel -> 2.
            // Next needed for Lvl 2 -> 282.
            // 20 < 282. Loop ends.

            mockFindFirst.mockResolvedValue({ xp: 0n, level: 1 });
            mockReturning.mockResolvedValue([{ xp: 20n, level: 2 }]);

            const result = await levelingService.addXp("1", 120n);

            expect(result.levelUp).toBe(true);
            expect(result.currentLevel).toBe(2);
            expect(mockSet).toHaveBeenCalledWith({ xp: 120n, level: 2 });
        });

        it("should handle multiple level ups", async () => {
            // Lvl 1 (100 needed). Lvl 2 (282 needed). Total for Lvl 3 = 100 + 282 = 382.
            // Add 400.
            // 400 >= 100 -> rem 300, Lvl 2.
            // 300 >= 282 -> rem 18, Lvl 3.

            mockFindFirst.mockResolvedValue({ xp: 0n, level: 1 });
            mockReturning.mockResolvedValue([{ xp: 18n, level: 3 }]);

            const result = await levelingService.addXp("1", 400n);

            expect(result.currentLevel).toBe(3);
        });

        it("should throw if user not found", async () => {
            mockFindFirst.mockResolvedValue(undefined);
            expect(levelingService.addXp("1", 50n)).rejects.toThrow("User not found");
        });
    });

    describe("processChatXp", () => {
        beforeEach(() => {
            setSystemTime(new Date("2023-01-01T12:00:00Z"));
        });

        it("should award XP if no cooldown", async () => {
            mockFindFirst
                .mockResolvedValueOnce(undefined) // Cooldown check
                .mockResolvedValueOnce(undefined) // XP Boost check
                .mockResolvedValueOnce({ xp: 0n, level: 1 }); // addXp -> getUser

            mockReturning.mockResolvedValue([{ xp: 15n, level: 1 }]); // addXp -> update

            Math.random = () => 0.5; // mid range? 10-20. 
            // floor(0.5 * (20 - 10 + 1)) + 10 = floor(0.5 * 11) + 10 = floor(5.5) + 10 = 15.

            const result = await levelingService.processChatXp("1");

            expect(result.awarded).toBe(true);
            expect((result as any).amount).toBe(15n);
            expect(mockInsert).toHaveBeenCalledWith(userTimers); // Cooldown set
        });

        it("should respect cooldown", async () => {
            const future = new Date("2023-01-01T12:00:10Z");
            mockFindFirst.mockResolvedValue({ expiresAt: future });

            const result = await levelingService.processChatXp("1");

            expect(result.awarded).toBe(false);
            expect(result.reason).toBe("cooldown");
            expect(mockUpdate).not.toHaveBeenCalled();
        });

        it("should apply XP boost", async () => {
            const now = new Date();
            const future = new Date(now.getTime() + 10000);

            mockFindFirst
                .mockResolvedValueOnce(undefined) // Cooldown
                .mockResolvedValueOnce({ expiresAt: future, metadata: { multiplier: 2.0 } }) // Boost
                .mockResolvedValueOnce({ xp: 0n, level: 1 }); // User

            Math.random = () => 0.0; // Min value = 10.
            // Boost 2x -> 20.

            mockReturning.mockResolvedValue([{ xp: 20n, level: 1 }]);

            const result = await levelingService.processChatXp("1");

            // Check if amount passed to addXp was boosted
            // Wait, result.amount is the returned amount from addXp ??
            // processChatXp returns { awarded: true, amount, ...resultFromAddXp }
            // So result.amount is the calculated amount.

            expect((result as any).amount).toBe(20n);
            // Implementation: amount = floor(amount * multiplier)
            // min 10 * 2 = 20.
        });
    });
});
