import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test";
import { triviaService } from "./trivia.service";
import { DrizzleClient } from "@shared/db/DrizzleClient";
import { users, userTimers } from "@db/schema";
import { eq, and } from "drizzle-orm";
import { config } from "@shared/lib/config";
import { TimerType } from "@shared/lib/constants";

// Mock fetch for OpenTDB API
const mockFetch = mock(() => Promise.resolve({
    json: () => Promise.resolve({
        response_code: 0,
        results: [{
            category: Buffer.from('General Knowledge').toString('base64'),
            type: 'multiple',
            difficulty: Buffer.from('medium').toString('base64'),
            question: Buffer.from('What is 2 + 2?').toString('base64'),
            correct_answer: Buffer.from('4').toString('base64'),
            incorrect_answers: [
                Buffer.from('3').toString('base64'),
                Buffer.from('5').toString('base64'),
                Buffer.from('22').toString('base64'),
            ]
        }]
    })
}));

global.fetch = mockFetch as any;

describe("TriviaService", () => {
    const TEST_USER_ID = "999999999";
    const TEST_USERNAME = "testuser";

    beforeEach(async () => {
        // Clean up test data
        await DrizzleClient.delete(userTimers)
            .where(eq(userTimers.userId, BigInt(TEST_USER_ID)));

        // Ensure test user exists with sufficient balance
        await DrizzleClient.insert(users)
            .values({
                id: BigInt(TEST_USER_ID),
                username: TEST_USERNAME,
                balance: 1000n,
                xp: 0n,
            })
            .onConflictDoUpdate({
                target: [users.id],
                set: {
                    balance: 1000n,
                }
            });
    });

    afterEach(async () => {
        // Clean up
        await DrizzleClient.delete(userTimers)
            .where(eq(userTimers.userId, BigInt(TEST_USER_ID)));
    });

    describe("fetchQuestion", () => {
        it("should fetch and decode a trivia question", async () => {
            const question = await triviaService.fetchQuestion();

            expect(question).toBeDefined();
            expect(question.question).toBe('What is 2 + 2?');
            expect(question.correctAnswer).toBe('4');
            expect(question.incorrectAnswers).toHaveLength(3);
            expect(question.type).toBe('multiple');
        });
    });

    describe("canPlayTrivia", () => {
        it("should allow playing when no cooldown exists", async () => {
            const result = await triviaService.canPlayTrivia(TEST_USER_ID);

            expect(result.canPlay).toBe(true);
            expect(result.nextAvailable).toBeUndefined();
        });

        it("should prevent playing when on cooldown", async () => {
            const futureDate = new Date(Date.now() + 60000);

            await DrizzleClient.insert(userTimers).values({
                userId: BigInt(TEST_USER_ID),
                type: TimerType.TRIVIA_COOLDOWN,
                key: 'default',
                expiresAt: futureDate,
            });

            const result = await triviaService.canPlayTrivia(TEST_USER_ID);

            expect(result.canPlay).toBe(false);
            expect(result.nextAvailable).toBeDefined();
        });

        it("should allow playing when cooldown has expired", async () => {
            const pastDate = new Date(Date.now() - 1000);

            await DrizzleClient.insert(userTimers).values({
                userId: BigInt(TEST_USER_ID),
                type: TimerType.TRIVIA_COOLDOWN,
                key: 'default',
                expiresAt: pastDate,
            });

            const result = await triviaService.canPlayTrivia(TEST_USER_ID);

            expect(result.canPlay).toBe(true);
        });
    });

    describe("startTrivia", () => {
        it("should start a trivia session and deduct entry fee", async () => {
            const session = await triviaService.startTrivia(TEST_USER_ID, TEST_USERNAME);

            expect(session).toBeDefined();
            expect(session.sessionId).toContain(TEST_USER_ID);
            expect(session.userId).toBe(TEST_USER_ID);
            expect(session.question).toBeDefined();
            expect(session.allAnswers).toHaveLength(4);
            expect(session.entryFee).toBe(config.trivia.entryFee);
            expect(session.potentialReward).toBeGreaterThan(0n);

            // Verify balance deduction
            const user = await DrizzleClient.query.users.findFirst({
                where: eq(users.id, BigInt(TEST_USER_ID))
            });

            expect(user?.balance).toBe(1000n - config.trivia.entryFee);

            // Verify cooldown was set
            const cooldown = await DrizzleClient.query.userTimers.findFirst({
                where: and(
                    eq(userTimers.userId, BigInt(TEST_USER_ID)),
                    eq(userTimers.type, TimerType.TRIVIA_COOLDOWN),
                    eq(userTimers.key, 'default')
                )
            });

            expect(cooldown).toBeDefined();
        });

        it("should throw error if user has insufficient balance", async () => {
            // Set balance to less than entry fee
            await DrizzleClient.update(users)
                .set({ balance: 10n })
                .where(eq(users.id, BigInt(TEST_USER_ID)));

            await expect(triviaService.startTrivia(TEST_USER_ID, TEST_USERNAME))
                .rejects.toThrow('Insufficient funds');
        });

        it("should throw error if user is on cooldown", async () => {
            const futureDate = new Date(Date.now() + 60000);

            await DrizzleClient.insert(userTimers).values({
                userId: BigInt(TEST_USER_ID),
                type: TimerType.TRIVIA_COOLDOWN,
                key: 'default',
                expiresAt: futureDate,
            });

            await expect(triviaService.startTrivia(TEST_USER_ID, TEST_USERNAME))
                .rejects.toThrow('cooldown');
        });
    });

    describe("submitAnswer", () => {
        it("should award prize for correct answer", async () => {
            const session = await triviaService.startTrivia(TEST_USER_ID, TEST_USERNAME);
            const balanceBefore = (await DrizzleClient.query.users.findFirst({
                where: eq(users.id, BigInt(TEST_USER_ID))
            }))!.balance!;

            const result = await triviaService.submitAnswer(session.sessionId, TEST_USER_ID, true);

            expect(result.correct).toBe(true);
            expect(result.reward).toBe(session.potentialReward);
            expect(result.correctAnswer).toBe(session.question.correctAnswer);

            // Verify balance increase
            const user = await DrizzleClient.query.users.findFirst({
                where: eq(users.id, BigInt(TEST_USER_ID))
            });

            expect(user?.balance).toBe(balanceBefore + session.potentialReward);
        });

        it("should not award prize for incorrect answer", async () => {
            const session = await triviaService.startTrivia(TEST_USER_ID, TEST_USERNAME);
            const balanceBefore = (await DrizzleClient.query.users.findFirst({
                where: eq(users.id, BigInt(TEST_USER_ID))
            }))!.balance!;

            const result = await triviaService.submitAnswer(session.sessionId, TEST_USER_ID, false);

            expect(result.correct).toBe(false);
            expect(result.reward).toBe(0n);
            expect(result.correctAnswer).toBe(session.question.correctAnswer);

            // Verify balance unchanged (already deducted at start)
            const user = await DrizzleClient.query.users.findFirst({
                where: eq(users.id, BigInt(TEST_USER_ID))
            });

            expect(user?.balance).toBe(balanceBefore);
        });

        it("should throw error if session doesn't exist", async () => {
            await expect(triviaService.submitAnswer("invalid_session", TEST_USER_ID, true))
                .rejects.toThrow('Session not found');
        });

        it("should prevent double submission", async () => {
            const session = await triviaService.startTrivia(TEST_USER_ID, TEST_USERNAME);

            await triviaService.submitAnswer(session.sessionId, TEST_USER_ID, true);

            // Try to submit again
            await expect(triviaService.submitAnswer(session.sessionId, TEST_USER_ID, true))
                .rejects.toThrow('Session not found');
        });
    });

    describe("getSession", () => {
        it("should retrieve active session", async () => {
            const session = await triviaService.startTrivia(TEST_USER_ID, TEST_USERNAME);
            const retrieved = triviaService.getSession(session.sessionId);

            expect(retrieved).toBeDefined();
            expect(retrieved?.sessionId).toBe(session.sessionId);
        });

        it("should return undefined for non-existent session", () => {
            const retrieved = triviaService.getSession("invalid_session");

            expect(retrieved).toBeUndefined();
        });
    });
});
