import { describe, it, expect, mock, beforeEach } from "bun:test";
import { triviaService } from "./trivia.service";
import { users, userTimers, transactions } from "@db/schema";
import { TimerType } from "@shared/lib/constants";

// Define mock functions
const mockFindFirst = mock();
const mockFindMany = mock(() => Promise.resolve([]));
const mockInsert = mock();
const mockUpdate = mock();
const mockDelete = mock();
const mockValues = mock();
const mockReturning = mock();
const mockSet = mock();
const mockWhere = mock();
const mockOnConflictDoUpdate = mock();
// Chain setup
mockInsert.mockReturnValue({ values: mockValues });
mockValues.mockReturnValue({
    returning: mockReturning,
    onConflictDoUpdate: mockOnConflictDoUpdate
});
mockOnConflictDoUpdate.mockResolvedValue({});

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 },
            userQuests: { findMany: mockFindMany },
        },
        insert: mockInsert,
        update: mockUpdate,
        delete: mockDelete,
    });

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

// Mock Config
mock.module("@shared/lib/config", () => ({
    config: {
        trivia: {
            entryFee: 50n,
            rewardMultiplier: 2.0,
            timeoutSeconds: 300,
            cooldownMs: 60000,
            categories: [9],
            difficulty: 'medium'
        }
    }
}));

// Mock Events (trivia service emits domain events instead of calling dashboardService directly)
const mockEmit = mock(() => true);
mock.module("@shared/lib/events", () => ({
    systemEvents: {
        emit: mockEmit,
    },
    EVENTS: {
        DOMAIN: {
            TRIVIA_STARTED: "domain:trivia_started",
            TRIVIA_WON: "domain:trivia_won",
        }
    }
}));

// Mock fetch for OpenTDB
global.fetch = 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'),
            ]
        }]
    })
})) as any;

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

    beforeEach(() => {
        mockFindFirst.mockReset();
        mockInsert.mockClear();
        mockUpdate.mockClear();
        mockDelete.mockClear();
        mockValues.mockClear();
        mockReturning.mockClear();
        mockSet.mockClear();
        mockWhere.mockClear();
        mockOnConflictDoUpdate.mockClear();
        mockEmit.mockClear();
        // Clear active sessions
        (triviaService as any).activeSessions.clear();
    });

    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);
        });
    });

    describe("canPlayTrivia", () => {
        it("should allow playing when no cooldown exists", async () => {
            mockFindFirst.mockResolvedValue(undefined);
            const result = await triviaService.canPlayTrivia(TEST_USER_ID);
            expect(result.canPlay).toBe(true);
        });

        it("should prevent playing when on cooldown", async () => {
            const future = new Date(Date.now() + 60000);
            mockFindFirst.mockResolvedValue({ expiresAt: future });

            const result = await triviaService.canPlayTrivia(TEST_USER_ID);
            expect(result.canPlay).toBe(false);
            expect(result.nextAvailable).toBe(future);
        });

        it("should allow playing when cooldown has expired", async () => {
            const past = new Date(Date.now() - 1000);
            mockFindFirst.mockResolvedValue({ expiresAt: past });

            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 () => {
            // Mock cooldown check (first call) and balance check (second call)
            mockFindFirst
                .mockResolvedValueOnce(undefined) // No cooldown
                .mockResolvedValueOnce({ id: 1n, balance: 1000n }); // User balance

            const session = await triviaService.startTrivia(TEST_USER_ID, TEST_USERNAME);

            expect(session).toBeDefined();
            expect(session.userId).toBe(TEST_USER_ID);
            expect(session.entryFee).toBe(50n);

            // Check deduction
            expect(mockUpdate).toHaveBeenCalledWith(users);
            expect(mockSet).toHaveBeenCalledWith(expect.objectContaining({
                // sql templating makes exact match hard, checking general invocation
            }));

            // Check transactions
            expect(mockInsert).toHaveBeenCalledWith(transactions);

            // Check cooldown set
            expect(mockInsert).toHaveBeenCalledWith(userTimers);
            expect(mockOnConflictDoUpdate).toHaveBeenCalled();

            // Check domain event emitted
            expect(mockEmit).toHaveBeenCalled();
        });

        it("should throw error if user has insufficient balance", async () => {
            mockFindFirst
                .mockResolvedValueOnce(undefined) // No cooldown
                .mockResolvedValueOnce({ id: 1n, balance: 10n }); // Insufficient balance

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

        it("should throw error if user is on cooldown", async () => {
            mockFindFirst.mockResolvedValueOnce({ expiresAt: new Date(Date.now() + 60000) });

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

    describe("submitAnswer", () => {
        it("should award prize for correct answer", async () => {
            // Setup an active session manually
            const session = {
                sessionId: "test_session",
                userId: TEST_USER_ID,
                question: { correctAnswer: "4" },
                potentialReward: 100n
            };
            (triviaService as any).activeSessions.set("test_session", session);

            // Mock user balance fetch for reward update
            mockFindFirst.mockResolvedValue({ id: 1n, balance: 950n });

            const result = await triviaService.submitAnswer("test_session", TEST_USER_ID, true);

            expect(result.correct).toBe(true);
            expect(result.reward).toBe(100n);

            // Verify balance update
            expect(mockUpdate).toHaveBeenCalledWith(users);
            expect(mockInsert).toHaveBeenCalledWith(transactions);
            expect(mockEmit).toHaveBeenCalled();
        });

        it("should not award prize for incorrect answer", async () => {
            const session = {
                sessionId: "test_session",
                userId: TEST_USER_ID,
                question: { correctAnswer: "4" },
                potentialReward: 100n
            };
            (triviaService as any).activeSessions.set("test_session", session);

            const result = await triviaService.submitAnswer("test_session", TEST_USER_ID, false);

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

            // No balance update
            expect(mockUpdate).not.toHaveBeenCalled();
        });

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

        it("should prevent double submission", async () => {
            const session = {
                sessionId: "test_session",
                userId: TEST_USER_ID,
                question: { correctAnswer: "4" },
                potentialReward: 100n
            };
            (triviaService as any).activeSessions.set("test_session", session);

            // Mock user for first success
            mockFindFirst.mockResolvedValue({ id: 1n, balance: 950n });

            await triviaService.submitAnswer("test_session", TEST_USER_ID, true);

            // Second try
            expect(triviaService.submitAnswer("test_session", TEST_USER_ID, true))
                .rejects.toThrow("Session not found");
        });
    });
});
