import { describe, it, expect, mock, beforeEach, afterEach, spyOn } from "bun:test";
import { questService } from "@shared/modules/quest/quest.service";
import { userQuests } from "@db/schema";
import { eq, and } from "drizzle-orm";
import { economyService } from "@shared/modules/economy/economy.service";
import { levelingService } from "@shared/modules/leveling/leveling.service";

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

// Chain setup
mockInsert.mockReturnValue({ values: mockValues });
mockValues.mockReturnValue({
    onConflictDoNothing: mockOnConflictDoNothing
});
mockOnConflictDoNothing.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: {
            userQuests: { findFirst: mockFindFirst, findMany: mockFindMany },
            quests: { findMany: mockFindMany },
        },
        insert: mockInsert,
        update: mockUpdate,
        delete: mockDelete,
    });

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

describe("questService", () => {
    let mockModifyUserBalance: any;
    let mockAddXp: any;

    beforeEach(() => {
        mockFindFirst.mockReset();
        mockFindMany.mockReset();
        mockInsert.mockClear();
        mockUpdate.mockClear();
        mockValues.mockClear();
        mockReturning.mockClear();
        mockSet.mockClear();
        mockWhere.mockClear();
        mockOnConflictDoNothing.mockClear();

        // Setup Spies
        mockModifyUserBalance = spyOn(economyService, 'modifyUserBalance').mockResolvedValue({} as any);
        mockAddXp = spyOn(levelingService, 'addXp').mockResolvedValue({} as any);
    });

    afterEach(() => {
        mockModifyUserBalance.mockRestore();
        mockAddXp.mockRestore();
    });

    describe("assignQuest", () => {
        it("should assign quest", async () => {
            mockReturning.mockResolvedValue([{ userId: 1n, questId: 101 }]);

            const result = await questService.assignQuest("1", 101);

            expect(result).toEqual([{ userId: 1n, questId: 101 }] as any);
            expect(mockInsert).toHaveBeenCalledWith(userQuests);
            expect(mockValues).toHaveBeenCalledWith({
                userId: 1n,
                questId: 101,
                progress: 0
            });
        });
    });

    describe("updateProgress", () => {
        it("should update progress", async () => {
            mockReturning.mockResolvedValue([{ userId: 1n, questId: 101, progress: 50 }]);

            const result = await questService.updateProgress("1", 101, 50);

            expect(result).toEqual([{ userId: 1n, questId: 101, progress: 50 }] as any);
            expect(mockUpdate).toHaveBeenCalledWith(userQuests);
            expect(mockSet).toHaveBeenCalledWith({ progress: 50 });
        });
    });

    describe("completeQuest", () => {
        it("should complete quest and grant rewards", async () => {
            const mockUserQuest = {
                userId: 1n,
                questId: 101,
                completedAt: null,
                quest: {
                    rewards: { balance: 100, xp: 50 }
                }
            };
            mockFindFirst.mockResolvedValue(mockUserQuest);

            const result = await questService.completeQuest("1", 101);

            expect(result.success).toBe(true);
            expect(result.rewards.balance).toBe(100n);
            expect(result.rewards.xp).toBe(50n);

            // Check updates
            expect(mockUpdate).toHaveBeenCalledWith(userQuests);
            expect(mockSet).toHaveBeenCalledWith({ completedAt: expect.any(Date) });

            // Check service calls
            expect(mockModifyUserBalance).toHaveBeenCalledWith("1", 100n, 'QUEST_REWARD', expect.any(String), null, expect.anything());
            expect(mockAddXp).toHaveBeenCalledWith("1", 50n, expect.anything());
        });

        it("should throw if quest not assigned", async () => {
            mockFindFirst.mockResolvedValue(null);
            expect(questService.completeQuest("1", 101)).rejects.toThrow("Quest not assigned");
        });

        it("should throw if already completed", async () => {
            mockFindFirst.mockResolvedValue({ completedAt: new Date() });
            expect(questService.completeQuest("1", 101)).rejects.toThrow("Quest already completed");
        });
    });

    describe("getUserQuests", () => {
        it("should return user quests", async () => {
            const mockData = [{ questId: 1 }, { questId: 2 }];
            mockFindMany.mockResolvedValue(mockData);

            const result = await questService.getUserQuests("1");

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

    describe("getAvailableQuests", () => {
        it("should return quests not yet accepted by user", async () => {
            // First call to findMany (userQuests) returns accepted quest IDs
            // Second call to findMany (quests) returns available quests
            mockFindMany
                .mockResolvedValueOnce([{ questId: 1 }]) // userQuests
                .mockResolvedValueOnce([{ id: 2, name: "New Quest" }]); // quests

            const result = await questService.getAvailableQuests("1");

            expect(result).toEqual([{ id: 2, name: "New Quest" }] as any);
            expect(mockFindMany).toHaveBeenCalledTimes(2);
        });

        it("should return all quests if user has no assigned quests", async () => {
            mockFindMany
                .mockResolvedValueOnce([]) // userQuests
                .mockResolvedValueOnce([{ id: 1 }, { id: 2 }]); // quests

            const result = await questService.getAvailableQuests("1");

            expect(result).toEqual([{ id: 1 }, { id: 2 }] as any);
        });
    });

    describe("handleEvent", () => {
        it("should progress a quest with sub-events", async () => {
            const mockUserQuest = {
                userId: 1n,
                questId: 101,
                progress: 0,
                completedAt: null,
                quest: { triggerEvent: "ITEM_USE:101", requirements: { target: 5 } }
            };
            mockFindMany.mockResolvedValue([mockUserQuest]);
            mockReturning.mockResolvedValue([{ userId: 1n, questId: 101, progress: 1 }]);

            await questService.handleEvent("1", "ITEM_USE:101", 1);

            expect(mockUpdate).toHaveBeenCalled();
            expect(mockSet).toHaveBeenCalledWith({ progress: 1 });
        });

        it("should complete a quest when target reached using sub-events", async () => {
            const mockUserQuest = {
                userId: 1n,
                questId: 101,
                progress: 4,
                completedAt: null,
                quest: { 
                    triggerEvent: "ITEM_COLLECT:505", 
                    requirements: { target: 5 },
                    rewards: { balance: 100 }
                }
            };
            mockFindMany.mockResolvedValue([mockUserQuest]);
            mockFindFirst.mockResolvedValue(mockUserQuest); // For completeQuest

            await questService.handleEvent("1", "ITEM_COLLECT:505", 1);

            // Verify completeQuest was called (it will update completedAt)
            expect(mockUpdate).toHaveBeenCalled();
            expect(mockSet).toHaveBeenCalledWith({ completedAt: expect.any(Date) });
        });

        it("should progress a quest with generic events", async () => {
            const mockUserQuest = {
                userId: 1n,
                questId: 102,
                progress: 0,
                completedAt: null,
                quest: { triggerEvent: "ITEM_COLLECT", requirements: { target: 5 } }
            };
            mockFindMany.mockResolvedValue([mockUserQuest]);
            mockReturning.mockResolvedValue([{ userId: 1n, questId: 102, progress: 1 }]);

            await questService.handleEvent("1", "ITEM_COLLECT:505", 1);

            expect(mockUpdate).toHaveBeenCalled();
            expect(mockSet).toHaveBeenCalledWith({ progress: 1 });
        });

        it("should ignore events that are not prefix matches", async () => {
            const mockUserQuest = {
                userId: 1n,
                questId: 103,
                progress: 0,
                completedAt: null,
                quest: { triggerEvent: "ITEM_COLLECT", requirements: { target: 5 } }
            };
            mockFindMany.mockResolvedValue([mockUserQuest]);

            await questService.handleEvent("1", "ITEM_COLLECT_UNRELATED", 1);

            expect(mockUpdate).not.toHaveBeenCalled();
        });

        it("should not progress a specific quest with a different specific event", async () => {
            const mockUserQuest = {
                userId: 1n,
                questId: 104,
                progress: 0,
                completedAt: null,
                quest: { triggerEvent: "ITEM_COLLECT:101", requirements: { target: 5 } }
            };
            mockFindMany.mockResolvedValue([mockUserQuest]);

            await questService.handleEvent("1", "ITEM_COLLECT:202", 1);

            expect(mockUpdate).not.toHaveBeenCalled();
        });

        it("should not progress a specific quest with a generic event", async () => {
            const mockUserQuest = {
                userId: 1n,
                questId: 105,
                progress: 0,
                completedAt: null,
                quest: { triggerEvent: "ITEM_COLLECT:101", requirements: { target: 5 } }
            };
            mockFindMany.mockResolvedValue([mockUserQuest]);

            await questService.handleEvent("1", "ITEM_COLLECT", 1);

            expect(mockUpdate).not.toHaveBeenCalled();
        });

        it("should ignore irrelevant events", async () => {
            const mockUserQuest = {
                userId: 1n,
                questId: 101,
                progress: 0,
                completedAt: null,
                quest: { triggerEvent: "DIFFERENT_EVENT", requirements: { target: 5 } }
            };
            mockFindMany.mockResolvedValue([mockUserQuest]);

            await questService.handleEvent("1", "TEST_EVENT", 1);

            expect(mockUpdate).not.toHaveBeenCalled();
        });
    });
});
