import { describe, it, expect, mock, beforeEach, setSystemTime } from "bun:test";
import { examService, ExamStatus } from "@shared/modules/economy/exam.service";
import { users, userTimers, transactions } from "@db/schema";

// Define mock functions
const mockFindFirst = mock();
const mockInsert = mock();
const mockUpdate = 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 });

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

    return {
        DrizzleClient: {
            query: {
                users: { findFirst: mockFindFirst },
                userTimers: { findFirst: mockFindFirst },
            },
            insert: mockInsert,
            update: mockUpdate,
            transaction: async (cb: any) => {
                return cb(createMockTx());
            }
        },
    };
});

// Mock withTransaction
mock.module("@/lib/db", () => ({
    withTransaction: async (cb: any, tx?: any) => {
        if (tx) return cb(tx);
        return cb({
            query: {
                users: { findFirst: mockFindFirst },
                userTimers: { findFirst: mockFindFirst },
            },
            insert: mockInsert,
            update: mockUpdate,
        });
    }
}));

// Mock Config
mock.module("@shared/lib/config", () => ({
    config: {
        economy: {
            exam: {
                multMin: 1.0,
                multMax: 2.0,
            }
        }
    }
}));

// Mock User Service
mock.module("@shared/modules/user/user.service", () => ({
    userService: {
        getOrCreateUser: mock()
    }
}));

// Mock Dashboard Service
mock.module("@shared/modules/dashboard/dashboard.service", () => ({
    dashboardService: {
        recordEvent: mock()
    }
}));

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

    describe("getExamStatus", () => {
        it("should return NOT_REGISTERED if no timer exists", async () => {
            mockFindFirst.mockResolvedValue(undefined);
            const status = await examService.getExamStatus("1");
            expect(status.status).toBe(ExamStatus.NOT_REGISTERED);
        });

        it("should return COOLDOWN if now < expiresAt", async () => {
            const now = new Date("2024-01-10T12:00:00Z");
            setSystemTime(now);
            const future = new Date("2024-01-11T00:00:00Z");
            
            mockFindFirst.mockResolvedValue({ 
                expiresAt: future,
                metadata: { examDay: 3, lastXp: "100" }
            });

            const status = await examService.getExamStatus("1");
            expect(status.status).toBe(ExamStatus.COOLDOWN);
            expect(status.nextExamAt?.getTime()).toBe(future.setHours(0,0,0,0));
        });

        it("should return MISSED if it is the wrong day", async () => {
            const now = new Date("2024-01-15T12:00:00Z"); // Monday (1)
            setSystemTime(now);
            const past = new Date("2024-01-10T00:00:00Z"); // Wednesday (3) last week
            
            mockFindFirst.mockResolvedValue({ 
                expiresAt: past,
                metadata: { examDay: 3, lastXp: "100" } // Registered for Wednesday
            });

            const status = await examService.getExamStatus("1");
            expect(status.status).toBe(ExamStatus.MISSED);
            expect(status.examDay).toBe(3);
        });

        it("should return AVAILABLE if it is the correct day", async () => {
            const now = new Date("2024-01-17T12:00:00Z"); // Wednesday (3)
            setSystemTime(now);
            const past = new Date("2024-01-10T00:00:00Z");
            
            mockFindFirst.mockResolvedValue({ 
                expiresAt: past,
                metadata: { examDay: 3, lastXp: "100" }
            });

            const status = await examService.getExamStatus("1");
            expect(status.status).toBe(ExamStatus.AVAILABLE);
            expect(status.examDay).toBe(3);
            expect(status.lastXp).toBe(100n);
        });
    });

    describe("registerForExam", () => {
        it("should create user and timer correctly", async () => {
            const now = new Date("2024-01-15T12:00:00Z"); // Monday (1)
            setSystemTime(now);

            const { userService } = await import("@shared/modules/user/user.service");
            (userService.getOrCreateUser as any).mockResolvedValue({ id: 1n, xp: 500n });

            const result = await examService.registerForExam("1", "testuser");

            expect(result.status).toBe(ExamStatus.NOT_REGISTERED);
            expect(result.examDay).toBe(1);
            
            expect(mockInsert).toHaveBeenCalledWith(userTimers);
            expect(mockInsert).toHaveBeenCalledTimes(1);
        });
    });

    describe("takeExam", () => {
        it("should return NOT_REGISTERED if not registered", async () => {
            mockFindFirst.mockResolvedValueOnce({ id: 1n }) // user check
                         .mockResolvedValueOnce(undefined); // timer check

            const result = await examService.takeExam("1");
            expect(result.status).toBe(ExamStatus.NOT_REGISTERED);
        });

        it("should handle missed exam and schedule for next exam day", async () => {
            const now = new Date("2024-01-15T12:00:00Z"); // Monday (1)
            setSystemTime(now);
            const past = new Date("2024-01-10T00:00:00Z");
            
            mockFindFirst.mockResolvedValueOnce({ id: 1n, xp: 600n }) // user
                         .mockResolvedValueOnce({ 
                             expiresAt: past,
                             metadata: { examDay: 3, lastXp: "500" } // Registered for Wednesday
                         }); // timer

            const result = await examService.takeExam("1");

            expect(result.status).toBe(ExamStatus.MISSED);
            expect(result.examDay).toBe(3);
            
            // Should set next exam to next Wednesday
            // Monday (1) + 2 days = Wednesday (3)
            const expected = new Date("2024-01-17T00:00:00Z");
            expect(result.nextExamAt!.getTime()).toBe(expected.getTime());
            
            expect(mockUpdate).toHaveBeenCalledWith(userTimers);
        });

        it("should calculate rewards and update state when passed", async () => {
            const now = new Date("2024-01-17T12:00:00Z"); // Wednesday (3)
            setSystemTime(now);
            const past = new Date("2024-01-10T00:00:00Z");
            
            mockFindFirst.mockResolvedValueOnce({ id: 1n, username: "testuser", xp: 1000n, balance: 0n }) // user
                         .mockResolvedValueOnce({ 
                             expiresAt: past,
                             metadata: { examDay: 3, lastXp: "500" }
                         }); // timer

            const result = await examService.takeExam("1");

            expect(result.status).toBe(ExamStatus.AVAILABLE);
            expect(result.xpDiff).toBe(500n);
            // Multiplier is between 1.0 and 2.0 based on mock config
            expect(result.multiplier).toBeGreaterThanOrEqual(1.0);
            expect(result.multiplier).toBeLessThanOrEqual(2.0);
            expect(result.reward).toBeGreaterThanOrEqual(500n);
            expect(result.reward).toBeLessThanOrEqual(1000n);

            expect(mockUpdate).toHaveBeenCalledWith(userTimers);
            expect(mockUpdate).toHaveBeenCalledWith(users);
            
            // Verify transaction
            expect(mockInsert).toHaveBeenCalledWith(transactions);
            expect(mockValues).toHaveBeenCalledWith(expect.objectContaining({
                amount: result.reward,
                userId: 1n,
                type: expect.anything()
            }));
        });
    });
});
