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

// Define mock functions
const mockFindMany = mock();
const mockFindFirst = mock();
const mockInsert = mock();
const mockUpdate = mock();
const mockDelete = mock();
const mockValues = mock();
const mockReturning = mock();
const mockSet = mock();
const mockWhere = mock();
const mockOnConflictDoUpdate = mock();

// Chainable mock setup
mockInsert.mockReturnValue({ values: mockValues });
mockValues.mockReturnValue({
    returning: mockReturning,
    onConflictDoUpdate: mockOnConflictDoUpdate // For claimDaily chain
});
mockOnConflictDoUpdate.mockResolvedValue({}); // Terminate the chain

mockUpdate.mockReturnValue({ set: mockSet });
mockSet.mockReturnValue({ where: mockWhere });
mockWhere.mockReturnValue({ returning: mockReturning });

// Mock DrizzleClient
mock.module("@/lib/DrizzleClient", () => {
    // Mock Transaction Object Structure
    const createMockTx = () => ({
        query: {
            users: { findFirst: mockFindFirst },
            userTimers: { findFirst: mockFindFirst },
        },
        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) => {
                return cb(createMockTx());
            }
        },
    };
});

// Mock Config
mock.module("@/lib/config", () => ({
    config: {
        economy: {
            daily: {
                amount: 100n,
                streakBonus: 10n,
                weeklyBonus: 50n,
                cooldownMs: 86400000, // 24 hours
            }
        }
    }
}));

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

    describe("transfer", () => {
        it("should transfer amount successfully", async () => {
            const sender = { id: 1n, balance: 200n };
            mockFindFirst.mockResolvedValue(sender);

            const result = await economyService.transfer("1", "2", 50n);

            expect(result).toEqual({ success: true, amount: 50n });

            // Check sender update
            expect(mockUpdate).toHaveBeenCalledWith(users);
            // We can check if mockSet was called twice
            expect(mockSet).toHaveBeenCalledTimes(2);

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

        it("should throw if amount is non-positive", async () => {
            expect(economyService.transfer("1", "2", 0n)).rejects.toThrow("Amount must be positive");
            expect(economyService.transfer("1", "2", -10n)).rejects.toThrow("Amount must be positive");
        });

        it("should throw if transferring to self", async () => {
            expect(economyService.transfer("1", "1", 50n)).rejects.toThrow("Cannot transfer to self");
        });

        it("should throw if sender not found", async () => {
            mockFindFirst.mockResolvedValue(undefined);
            expect(economyService.transfer("1", "2", 50n)).rejects.toThrow("Sender not found");
        });

        it("should throw if insufficient funds", async () => {
            mockFindFirst.mockResolvedValue({ id: 1n, balance: 20n });
            expect(economyService.transfer("1", "2", 50n)).rejects.toThrow("Insufficient funds");
        });
    });

    describe("claimDaily", () => {
        beforeEach(() => {
            setSystemTime(new Date("2023-01-01T12:00:00Z"));
        });

        it("should claim daily reward successfully", async () => {
            const recentPast = new Date("2023-01-01T11:00:00Z"); // 1 hour ago

            // First call finds cooldown (expired recently), second finds user
            mockFindFirst
                .mockResolvedValueOnce({ expiresAt: recentPast }) // Cooldown check - expired -> ready
                .mockResolvedValueOnce({ id: 1n, dailyStreak: 5, balance: 1000n }); // User check

            const result = await economyService.claimDaily("1");

            expect(result.claimed).toBe(true);
            // Streak should increase: 5 + 1 = 6
            expect(result.streak).toBe(6);
            // Base 100 + (6-1)*10 = 150
            expect(result.amount).toBe(150n);
            expect(result.isWeekly).toBe(false);

            // Check updates
            expect(mockUpdate).toHaveBeenCalledWith(users);
            expect(mockInsert).toHaveBeenCalledWith(userTimers);
            expect(mockInsert).toHaveBeenCalledWith(transactions);
        });

        it("should claim weekly bonus correctly on 7th day", async () => {
            const recentPast = new Date("2023-01-01T11:00:00Z");

            mockFindFirst
                .mockResolvedValueOnce({ expiresAt: recentPast })
                .mockResolvedValueOnce({ id: 1n, dailyStreak: 6, balance: 1000n }); // User currently at 6 days

            const result = await economyService.claimDaily("1");

            expect(result.claimed).toBe(true);
            // Streak should increase: 6 + 1 = 7
            expect(result.streak).toBe(7);

            // Base: 100
            // Streak Bonus: (7-1)*10 = 60
            // Weekly Bonus: 50
            // Total: 210
            expect(result.amount).toBe(210n);
            expect(result.isWeekly).toBe(true);
            expect(result.weeklyBonus).toBe(50n);
        });

        it("should throw if cooldown is active", async () => {
            const future = new Date("2023-01-02T12:00:00Z"); // +24h
            mockFindFirst.mockResolvedValue({ expiresAt: future });
            expect(economyService.claimDaily("1")).rejects.toThrow("Daily already claimed");
        });

        it("should set cooldown to next UTC midnight", async () => {
            // 2023-01-01T12:00:00Z -> Should be 2023-01-02T00:00:00Z

            mockFindFirst
                .mockResolvedValueOnce(undefined) // No cooldown
                .mockResolvedValueOnce({ id: 1n, dailyStreak: 5, balance: 1000n });

            const result = await economyService.claimDaily("1");

            const expectedReset = new Date("2023-01-02T00:00:00Z");
            expect(result.nextReadyAt.toISOString()).toBe(expectedReset.toISOString());
        });

        it("should reset streak if missed a day (long time gap)", async () => {
            // Expired 3 days ago
            const past = new Date("2023-01-01T00:00:00Z"); // now is 12:00
            // Wait, logic says: if (timeSinceReady > 24h)
            // now - expiresAt. 
            // If cooldown expired 2022-12-30. Now is 2023-01-01. Gap is > 24h.

            const expiredAt = new Date("2022-12-30T12:00:00Z");

            mockFindFirst
                .mockResolvedValueOnce({ expiresAt: expiredAt })
                .mockResolvedValueOnce({ id: 1n, dailyStreak: 5 });

            const result = await economyService.claimDaily("1");

            // timeSinceReady = 48h. 
            // streak should reset to 1
            expect(result.streak).toBe(1);
        });

        it("should preserve streak if cooldown is missing but user has a streak", async () => {
            mockFindFirst
                .mockResolvedValueOnce(undefined) // No cooldown
                .mockResolvedValueOnce({ id: 1n, dailyStreak: 10 });

            const result = await economyService.claimDaily("1");
            expect(result.streak).toBe(11);
        });

        it("should prevent weekly bonus exploit by resetting streak", async () => {
            // Mock user at streak 7.
            // Mock time as 24h + 1m after expiry.

            const expiredAt = new Date("2023-01-01T11:59:00Z"); // now is 12:00 next day, plus 1 min gap? 
            // no, 'now' is 2023-01-01T12:00:00Z set in beforeEach

            // We want gap > 24h. 
            // If expiry was yesterday 11:59:59. Gap is 24h + 1s.

            const expiredAtExploit = new Date("2022-12-31T11:59:00Z"); // Over 24h ago

            mockFindFirst
                .mockResolvedValueOnce({ expiresAt: expiredAtExploit })
                .mockResolvedValueOnce({ id: 1n, dailyStreak: 7 });

            const result = await economyService.claimDaily("1");

            // Should reset to 1
            expect(result.streak).toBe(1);
            expect(result.isWeekly).toBe(false);
        });
    });

    describe("modifyUserBalance", () => {
        it("should add balance successfully", async () => {
            mockReturning.mockResolvedValue([{ id: 1n, balance: 150n }]);

            const result = await economyService.modifyUserBalance("1", 50n, "TEST", "Test add");

            expect(mockUpdate).toHaveBeenCalledWith(users);
            expect(mockInsert).toHaveBeenCalledWith(transactions);
        });

        it("should throw if insufficient funds when negative", async () => {
            mockFindFirst.mockResolvedValue({ id: 1n, balance: 20n });

            expect(economyService.modifyUserBalance("1", -50n, "TEST", "Test sub")).rejects.toThrow("Insufficient funds");
        });
    });
});
