import { describe, it, expect, mock, beforeEach, afterEach, spyOn } from "bun:test";
import { inventoryService } from "@shared/modules/inventory/inventory.service";
import { inventory, userTimers } from "@db/schema";
// Helper to mock resolved value for spyOn
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 mockSelect = mock();
const mockFrom = mock();
const mockOnConflictDoUpdate = mock();
const mockInnerJoin = mock();
const mockLimit = 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 });

mockDelete.mockReturnValue({ where: mockWhere });

mockSelect.mockReturnValue({ from: mockFrom });
mockFrom.mockReturnValue({ where: mockWhere, innerJoin: mockInnerJoin });
mockInnerJoin.mockReturnValue({ where: mockWhere });
mockWhere.mockReturnValue({ returning: mockReturning, limit: mockLimit });
mockLimit.mockResolvedValue([]);

// Mock DrizzleClient
mock.module("@shared/db/DrizzleClient", () => {
    const createMockTx = () => ({
        query: {
            inventory: { findFirst: mockFindFirst, findMany: mockFindMany },
            items: { findFirst: mockFindFirst },
            userTimers: { findFirst: mockFindFirst },
            userQuests: { findMany: mockFindMany, findFirst: mockFindFirst },
            quests: { findMany: mockFindMany },
        },
        insert: mockInsert,
        update: mockUpdate,
        delete: mockDelete,
        select: mockSelect,
    });

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

mock.module("@shared/lib/config", () => ({
    config: {
        inventory: {
            maxStackSize: 100n,
            maxSlots: 10
        }
    }
}));

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

    beforeEach(() => {
        mockFindFirst.mockReset();
        mockFindMany.mockReset();
        mockFindMany.mockResolvedValue([]);
        mockInsert.mockClear();
        mockUpdate.mockClear();
        mockDelete.mockClear();
        mockValues.mockClear();
        mockReturning.mockClear();
        mockSet.mockClear();
        mockWhere.mockClear();
        mockSelect.mockClear();
        mockFrom.mockClear();
        mockOnConflictDoUpdate.mockClear();

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

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

    describe("addItem", () => {
        it("should add new item if slot available", async () => {
            // Check existing (none) -> Check count (0) -> Insert
            mockFindFirst.mockResolvedValue(null);
            const mockCountResult = mock().mockResolvedValue([{ count: 0 }]);
            mockFrom.mockReturnValue({ where: mockCountResult });

            mockReturning.mockResolvedValue([{ itemId: 1, quantity: 5n }]);

            const result = await inventoryService.addItem("1", 1, 5n);

            expect(result).toEqual({ itemId: 1, quantity: 5n } as any);
            expect(mockInsert).toHaveBeenCalledWith(inventory);
            expect(mockValues).toHaveBeenCalledWith({
                userId: 1n,
                itemId: 1,
                quantity: 5n
            });
        });

        it("should stack existing item up to limit", async () => {
            // Check existing (found with 10)
            mockFindFirst.mockResolvedValue({ quantity: 10n });
            mockReturning.mockResolvedValue([{ itemId: 1, quantity: 15n }]);

            const result = await inventoryService.addItem("1", 1, 5n);

            expect(result).toBeDefined();
            expect(result?.quantity).toBe(15n);
            expect(mockUpdate).toHaveBeenCalledWith(inventory);
            expect(mockSet).toHaveBeenCalledWith({ quantity: 15n });
        });

        it("should throw if max stack exceeded", async () => {
            mockFindFirst.mockResolvedValue({ quantity: 99n });
            // Max is 100
            expect(inventoryService.addItem("1", 1, 5n)).rejects.toThrow("Cannot exceed max stack size");
        });

        it("should throw if inventory full", async () => {
            mockFindFirst.mockResolvedValue(null);

            const mockCountResult = mock().mockResolvedValue([{ count: 10 }]); // Max slots 10
            mockFrom.mockReturnValue({ where: mockCountResult });

            expect(inventoryService.addItem("1", 1, 1n)).rejects.toThrow("Inventory full");
        });
    });

    describe("removeItem", () => {
        it("should decrease quantity if enough", async () => {
            mockFindFirst.mockResolvedValue({ quantity: 10n });
            mockReturning.mockResolvedValue([{ quantity: 5n }]);

            await inventoryService.removeItem("1", 1, 5n);

            expect(mockUpdate).toHaveBeenCalledWith(inventory);
            // mockSet uses sql template, hard to check exact value, checking call presence
            expect(mockSet).toHaveBeenCalled();
        });

        it("should delete item if quantity becomes 0", async () => {
            mockFindFirst.mockResolvedValue({ quantity: 5n });

            const result = await inventoryService.removeItem("1", 1, 5n);

            expect(mockDelete).toHaveBeenCalledWith(inventory);
            expect(result).toBeDefined();
            expect(result?.quantity).toBe(0n);
        });

        it("should throw if insufficient quantity", async () => {
            mockFindFirst.mockResolvedValue({ quantity: 2n });
            expect(inventoryService.removeItem("1", 1, 5n)).rejects.toThrow("Insufficient item quantity");
        });
    });

    describe("buyItem", () => {
        it("should buy item successfully", async () => {
            const mockItem = { id: 1, name: "Potion", price: 100n };
            mockFindFirst.mockResolvedValue(mockItem);

            // For addItem internal call, we need to mock findFirst again or ensure it works.
            // DrizzleClient.transaction calls callback.
            // buyItem calls findFirst for item.
            // buyItem calls modifyUserBalance.
            // buyItem calls addItem.

            // addItem calls findFirst for inventory.

            // So mockFindFirst needs to return specific values in sequence.
            mockFindFirst
                .mockResolvedValueOnce(mockItem) // Item check
                .mockResolvedValueOnce(null);    // addItem -> existing check (null = new)

            // addItem -> count check
            const mockCountResult = mock().mockResolvedValue([{ count: 0 }]);
            mockFrom.mockReturnValue({ where: mockCountResult });

            mockReturning.mockResolvedValue([{}]);

            const result = await inventoryService.buyItem("1", 1, 2n);

            expect(result.success).toBe(true);
            expect(mockModifyUserBalance).toHaveBeenCalledWith("1", -200n, 'PURCHASE', expect.stringContaining("Bought 2x"), null, expect.anything());
            expect(mockInsert).toHaveBeenCalledWith(inventory); // from addItem
        });
    });

    describe("useItem", () => {
        it("should apply effects and consume item", async () => {
            const mockItem = {
                id: 1,
                name: "XP Potion",
                usageData: {
                    consume: true,
                    effects: [
                        { type: "ADD_XP", amount: 100 },
                        { type: "XP_BOOST", durationMinutes: 60, multiplier: 2.0 }
                    ]
                }
            };

            // inventory entry
            mockFindFirst.mockResolvedValue({ quantity: 1n, item: mockItem });

            // For removeItem:
            // removeItem calls findFirst (inventory).
            // So sequence: 
            // 1. useItem -> findFirst (inventory + item)
            // 2. removeItem -> findFirst (inventory)

            mockFindFirst
                .mockResolvedValueOnce({ quantity: 1n, item: mockItem }) // useItem check
                .mockResolvedValueOnce({ quantity: 1n }); // removeItem check

            const result = await inventoryService.useItem("1", 1);

            expect(result.success).toBe(true);
            expect(mockAddXp).toHaveBeenCalledWith("1", 100n, expect.anything());
            expect(mockInsert).toHaveBeenCalledWith(userTimers); // XP Boost
            expect(mockDelete).toHaveBeenCalledWith(inventory); // Consume
        });
    });

    describe("getAutocompleteItems", () => {
        it("should return formatted autocomplete results with rarity", async () => {
            const mockItems = [
                {
                    item: { id: 1, name: "Common Sword", rarity: "Common", usageData: { effects: [{}] } },
                    quantity: 5n
                },
                {
                    item: { id: 2, name: "Epic Shield", rarity: "Epic", usageData: { effects: [{}] } },
                    quantity: 1n
                }
            ];

            mockLimit.mockResolvedValue(mockItems);

            // Restore mocks that might have been polluted by other tests
            mockFrom.mockReturnValue({ where: mockWhere, innerJoin: mockInnerJoin });
            mockWhere.mockReturnValue({ returning: mockReturning, limit: mockLimit });

            const result = await inventoryService.getAutocompleteItems("1", "Sw");

            expect(mockSelect).toHaveBeenCalled();
            expect(mockFrom).toHaveBeenCalledWith(inventory);
            expect(mockInnerJoin).toHaveBeenCalled(); // checks join
            expect(mockWhere).toHaveBeenCalled(); // checks filters
            expect(mockLimit).toHaveBeenCalledWith(20);

            expect(result).toHaveLength(2);
            expect(result[0]?.name).toBe("Common Sword (5) [Common]");
            expect(result[0]?.value).toBe(1);
            expect(result[1]?.name).toBe("Epic Shield (1) [Epic]");
            expect(result[1]?.value).toBe(2);
        });
    });
});
