import { CashuMint, CashuWallet } from "@cashu/cashu-ts";
import NDK, {
    NDKCashuMintList,
    NDKEvent,
    type NDKEventId,
    NDKNutzap,
    NDKNutzapState,
    NDKPrivateKeySigner,
    type NDKUser,
} from "@nostr-dev-kit/ndk";
import { mockNutzap } from "@nostr-dev-kit/ndk/test";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import * as CashuMintModule from "../wallets/cashu/mint.js";
import { NDKCashuWallet } from "../wallets/cashu/wallet/index.js";
import { NDKCashuWalletBackup } from "../wallets/cashu/wallet/index.js";
import { fetchPage } from "./fetch-page.js";
import { NDKNutzapMonitor, type NDKNutzapMonitorStore } from "./index";
import * as SpendStatusModule from "./spend-status.js";

// Mock the modules we don't want to actually call
vi.mock("./fetch-page.js");
vi.mock("./spend-status.js");
vi.mock("../wallets/cashu/mint.js");

// Define the extended store type with our test spy
interface MockStore extends NDKNutzapMonitorStore {
    setNutzapStateSpy: ReturnType<typeof vi.fn>;
}

// Mock store for testing
const createMockStore = (): MockStore => {
    const nutzapStates = new Map<NDKEventId, NDKNutzapState>();
    const setNutzapStateSpy = vi.fn();

    return {
        getAllNutzaps: async (): Promise<Map<NDKEventId, NDKNutzapState>> => nutzapStates,
        setNutzapState: async (id: NDKEventId, stateChange: Partial<NDKNutzapState>): Promise<void> => {
            const currentState = nutzapStates.get(id) || ({} as NDKNutzapState);
            nutzapStates.set(id, { ...currentState, ...stateChange });
            setNutzapStateSpy(id, stateChange);
        },
        setNutzapStateSpy,
    };
};

// Set a longer timeout for all tests in this file
vi.setConfig({ testTimeout: 15000 });

describe("NDKNutzapMonitor", () => {
    let ndk: NDK;
    let user: NDKUser;
    let mintList: NDKCashuMintList;
    let mockStore: MockStore;
    let monitor: NDKNutzapMonitor;

    beforeEach(async () => {
        // Setup NDK with a signer
        ndk = new NDK({
            signer: NDKPrivateKeySigner.generate(),
            explicitRelayUrls: ["wss://relay1.com"],
        });

        user = await ndk.signer?.user()!;

        // Setup a mint list
        mintList = new NDKCashuMintList(ndk);
        mintList.mints = ["https://testmint.com"];

        // Mock CashuWallet
        const mockCashuWallet = new CashuWallet(new CashuMint("https://testmint.com"));
        vi.spyOn(mockCashuWallet, "checkProofsStates").mockResolvedValue([]);
        vi.spyOn(CashuMintModule, "walletForMint").mockResolvedValue(mockCashuWallet);

        // Setup mock store
        mockStore = createMockStore();

        // Create the monitor
        monitor = new NDKNutzapMonitor(ndk, user, { mintList, store: mockStore });

        // Make updateNutzapState public for testing
        (monitor as any).updateNutzapState = monitor.updateNutzapState.bind(monitor);

        // Setup mock NDKCashuWallet
        const mockNDKCashuWallet = new NDKCashuWallet(ndk);
        mockNDKCashuWallet.redeemNutzaps = vi.fn().mockResolvedValue(100);
        monitor.wallet = mockNDKCashuWallet;

        // Mock console methods
        vi.spyOn(console, "error").mockImplementation(() => {});
        vi.spyOn(console, "log").mockImplementation(() => {});
    });

    afterEach(() => {
        vi.clearAllMocks();
    });

    describe("addPrivkey", () => {
        it("should add a private key to the monitor", async () => {
            const signer = NDKPrivateKeySigner.generate();
            const pubkey = (await signer.user()).pubkey;

            await monitor.addPrivkey(signer);

            expect(monitor.privkeys.has(pubkey)).toBe(true);
        });

        it("should not add duplicate private keys", async () => {
            const signer = NDKPrivateKeySigner.generate();
            const _pubkey = (await signer.user()).pubkey;

            await monitor.addPrivkey(signer);
            const initialSize = monitor.privkeys.size;

            // Try to add the same key again
            await monitor.addPrivkey(signer);

            expect(monitor.privkeys.size).toBe(initialSize);
        });

        it.skip("should attempt to redeem nutzaps that were previously locked to a privkey that is now available", async () => {
            // No need for individual timeout with vitest
            // Create a signer
            const signer = NDKPrivateKeySigner.generate();
            const pubkey = (await signer.user()).pubkey;

            // Create a nutzap that requires this privkey
            const nutzap = await mockNutzap("https://testmint.com", 100, ndk, {
                recipientPubkey: pubkey,
            });

            // Set the nutzap state to MISSING_PRIVKEY
            monitor.nutzapStates.set(nutzap.id, {
                nutzap,
                status: NdkNutzapStatus.MISSING_PRIVKEY,
            });

            // Mock getProofSpendState to return the nutzap as unspent
            vi.spyOn(SpendStatusModule, "getProofSpendState").mockResolvedValue({
                unspentProofs: nutzap.proofs,
                spentProofs: [],
                nutzapsWithUnspentProofs: [nutzap],
                nutzapsWithSpentProofs: [],
            });

            // Spy on redeemNutzaps
            const redeemSpy = vi.spyOn(monitor, "redeemNutzaps").mockResolvedValue(undefined);

            // Add the privkey (this should trigger a check for nutzaps that can now be redeemed)
            await monitor.addPrivkey(signer);

            // Start the monitor to trigger the check for nutzaps
            await monitor.start({ filter: {} });

            // Check that redeemNutzaps was called with the right arguments
            expect(redeemSpy).toHaveBeenCalled();
            const callArgs = redeemSpy.mock.calls[0];
            expect(callArgs[0]).toBe(nutzap.mint);
            expect(callArgs[1]).toContainEqual(expect.objectContaining({ id: nutzap.id }));
        });
    });

    describe("getBackupKeys", () => {
        it("should load private keys from backup events", async () => {
            // Create backup event
            const backupSigner1 = NDKPrivateKeySigner.generate();
            const backupSigner2 = NDKPrivateKeySigner.generate();

            // Mock the fetchEvents function
            const mockBackupEvent = new NDKEvent(ndk);
            const backup = new NDKCashuWalletBackup(ndk);
            backup.privkeys = [backupSigner1.privateKey!, backupSigner2.privateKey!];

            // Mock the fetchEvents method
            ndk.fetchEvents = vi.fn().mockResolvedValue(new Set([mockBackupEvent]));

            // Mock the from method to return our backup
            vi.spyOn(NDKCashuWalletBackup, "from").mockResolvedValue(backup);

            // Call getBackupKeys
            await monitor.getBackupKeys();

            // Check that the private keys were added
            const pubkey1 = (await backupSigner1.user()).pubkey;
            const pubkey2 = (await backupSigner2.user()).pubkey;

            expect(monitor.privkeys.has(pubkey1)).toBe(true);
            expect(monitor.privkeys.has(pubkey2)).toBe(true);
        });

        it.skip("should handle errors when loading backup events", async () => {
            // Mock the fetchEvents method to throw an error
            ndk.fetchEvents = vi.fn().mockRejectedValue(new Error("Network error"));

            // Spy on console.error
            const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});

            // Call the method
            await monitor.getBackupKeys();

            // Verify error was logged
            expect(consoleSpy).toHaveBeenCalled();
            expect(consoleSpy.mock.calls[0][0]).toContain("Error loading backup events");
        });

        it("should create a new backup if new private keys were added but not found in backups", async () => {
            // Create a signer to add
            const newSigner = NDKPrivateKeySigner.generate();
            await monitor.addPrivkey(newSigner);

            // Mock empty fetchEvents result
            ndk.fetchEvents = vi.fn().mockResolvedValue(new Set());

            // Spy on NDKCashuWalletBackup.save
            const saveSpy = vi.fn().mockResolvedValue(undefined);
            vi.spyOn(NDKCashuWalletBackup.prototype, "save").mockImplementation(saveSpy);

            // Call getBackupKeys
            await monitor.getBackupKeys();

            // Check that a backup was created and saved
            expect(saveSpy).toHaveBeenCalled();
        });
    });

    describe("redeemNutzap", () => {
        it("should set state to MISSING_PRIVKEY when no privkey is available", async () => {
            // Create a nutzap with a pubkey we don't have
            const unknownSigner = NDKPrivateKeySigner.generate();
            const unknownPubkey = (await unknownSigner.user()).pubkey;

            const nutzap = await mockNutzap("https://testmint.com", 100, ndk, {
                recipientPubkey: unknownPubkey,
            });

            // Try to redeem the nutzap
            await monitor.redeemNutzap(nutzap);

            // Check the state is set to MISSING_PRIVKEY
            const state = monitor.nutzapStates.get(nutzap.id);
            expect(state?.status).toBe(NdkNutzapStatus.MISSING_PRIVKEY);

            // Check the store was updated
            expect(mockStore.setNutzapStateSpy).toHaveBeenCalledWith(
                nutzap.id,
                expect.objectContaining({ status: NdkNutzapStatus.MISSING_PRIVKEY }),
            );
        });

        it("should transition through states and set redeemedAmount when successfully redeemed", async () => {
            // Mock the wallet for mints
            const mockCashuWallet = new CashuWallet(new CashuMint("https://testmint.com"));
            vi.spyOn(CashuMintModule, "walletForMint").mockResolvedValue(mockCashuWallet);

            // Get the user's pubkey and create a nutzap for them
            const userSigner = ndk.signer as NDKPrivateKeySigner;
            const userPubkey = (await userSigner.user()).pubkey;

            // Create a nutzap for the user
            const nutzap = await mockNutzap("https://testmint.com", 100, ndk, {
                recipientPubkey: userPubkey,
            });

            // Add the user's privkey to the monitor
            await monitor.addPrivkey(userSigner);

            // Mock redeemNutzaps to simulate successful redemption
            vi.spyOn(monitor, "redeemNutzaps").mockImplementation(async (_mint, nutzaps, _proofs) => {
                // Simulate successful redemption
                for (const nutzap of nutzaps) {
                    (monitor as any).updateNutzapState(nutzap.id, {
                        status: NdkNutzapStatus.REDEEMED,
                        redeemedAmount: 100,
                    });
                }
                monitor.emit("redeemed", nutzaps, 100);
            });

            // Try to redeem the nutzap
            await monitor.redeemNutzap(nutzap);

            // Check final state
            const finalState = monitor.nutzapStates.get(nutzap.id);
            expect(finalState?.status).toBe(NdkNutzapStatus.REDEEMED);
            expect(finalState?.redeemedAmount).toBe(100);
        });

        it.skip("should mark nutzap as INVALID_NUTZAP when it's not valid", async () => {
            // Mock an invalid nutzap
            const invalidNutzap = await mockNutzap("https://testmint.com", 100, ndk);
            Object.defineProperty(invalidNutzap, "isValid", { get: () => false });

            // Try to redeem the nutzap
            await monitor.redeemNutzap(invalidNutzap);

            // Check final state
            const finalState = monitor.nutzapStates.get(invalidNutzap.id);
            expect(finalState?.status).toBe(NdkNutzapStatus.INVALID_NUTZAP);
            expect(finalState?.errorMessage).toBe("Invalid nutzap");
        });

        it.skip("should mark nutzap as INVALID_NUTZAP when p2pk is missing", async () => {
            // Mock a nutzap with missing p2pk
            const nutzap = await mockNutzap("https://testmint.com", 100, ndk);
            Object.defineProperty(nutzap, "rawP2pk", { get: () => null });
            Object.defineProperty(nutzap, "isValid", { get: () => true });

            // Try to redeem the nutzap
            await monitor.redeemNutzap(nutzap);

            // Check final state
            const finalState = monitor.nutzapStates.get(nutzap.id);
            expect(finalState?.status).toBe(NdkNutzapStatus.INVALID_NUTZAP);
            expect(finalState?.errorMessage).toContain("Invalid nutzap: locked to an invalid public key");
        });

        it("should mark nutzap as PERMANENT_ERROR when 'unknown public key size' error occurs", async () => {
            // Mock the wallet for mints
            const mockCashuWallet = new CashuWallet(new CashuMint("https://testmint.com"));
            vi.spyOn(CashuMintModule, "walletForMint").mockResolvedValue(mockCashuWallet);

            // Get the user's pubkey and create a nutzap for them
            const userSigner = ndk.signer as NDKPrivateKeySigner;
            const userPubkey = (await userSigner.user()).pubkey;

            // Create a nutzap for the user
            const nutzap = await mockNutzap("https://testmint.com", 100, ndk, {
                recipientPubkey: userPubkey,
            });

            // Add the user's privkey to the monitor
            await monitor.addPrivkey(userSigner);

            // Mock redeemNutzaps to throw "unknown public key size" error
            vi.spyOn(monitor.wallet as NDKCashuWallet, "redeemNutzaps").mockImplementation(() => {
                throw new Error("unknown public key size");
            });

            // Listen for failed emissions
            const failedEvents: Array<{ nutzap: NDKNutzap; error: string }> = [];
            monitor.on("failed", (nutzap, error) => {
                failedEvents.push({ nutzap, error });
            });

            // Try to redeem the nutzap
            await monitor.redeemNutzap(nutzap);

            // Check that the failed event was emitted with correct error message
            expect(failedEvents.length).toBe(1);
            expect(failedEvents[0].nutzap.id).toBe(nutzap.id);
            expect(failedEvents[0].error).toBe("Invalid p2pk: unknown public key size");

            // Check that the nutzap state was updated correctly
            const finalState = monitor.nutzapStates.get(nutzap.id);
            expect(finalState?.status).toBe(NdkNutzapStatus.PERMANENT_ERROR);
            expect(finalState?.errorMessage).toBe("Invalid p2pk: unknown public key size");
        });

        it("should emit 'failed' event for other errors during redemption", async () => {
            // Mock the wallet for mints
            const mockCashuWallet = new CashuWallet(new CashuMint("https://testmint.com"));
            vi.spyOn(CashuMintModule, "walletForMint").mockResolvedValue(mockCashuWallet);

            // Get the user's pubkey and create a nutzap for them
            const userSigner = ndk.signer as NDKPrivateKeySigner;
            const userPubkey = (await userSigner.user()).pubkey;

            // Create a nutzap for the user
            const nutzap = await mockNutzap("https://testmint.com", 100, ndk, {
                recipientPubkey: userPubkey,
            });

            // Add the user's privkey to the monitor
            await monitor.addPrivkey(userSigner);

            // Mock redeemNutzaps to throw a generic error
            vi.spyOn(monitor.wallet as NDKCashuWallet, "redeemNutzaps").mockImplementation(() => {
                throw new Error("Mint server error");
            });

            // Listen for failed emissions
            const failedEvents: Array<{ nutzap: NDKNutzap; error: string }> = [];
            monitor.on("failed", (nutzap, error) => {
                failedEvents.push({ nutzap, error });
            });

            // Try to redeem the nutzap
            await monitor.redeemNutzap(nutzap);

            // Check that the failed event was emitted
            expect(failedEvents.length).toBe(1);
            expect(failedEvents[0].nutzap.id).toBe(nutzap.id);
            expect(failedEvents[0].error).toBe("Mint server error");
        });
    });

    describe("processAccumulatedNutzaps", () => {
        it("should mark spent nutzaps as SPENT and not attempt to redeem them", async () => {
            // Get the user's pubkey
            const userPubkey = (await ndk.signer?.user()).pubkey;

            // Create nutzaps - one spent, one unspent
            const unspentNutzap = await mockNutzap("https://testmint.com", 100, ndk, {
                recipientPubkey: userPubkey,
            });
            const spentNutzap = await mockNutzap("https://testmint.com", 200, ndk, {
                recipientPubkey: userPubkey,
            });

            // Setup fetchPage mock
            (fetchPage as vi.Mock).mockResolvedValue([unspentNutzap, spentNutzap]);

            // Setup getProofSpendState mock to show one nutzap as spent and one as unspent
            vi.spyOn(SpendStatusModule, "getProofSpendState").mockResolvedValue({
                unspentProofs: unspentNutzap.proofs,
                spentProofs: spentNutzap.proofs,
                nutzapsWithUnspentProofs: [unspentNutzap],
                nutzapsWithSpentProofs: [spentNutzap],
            });

            // Add user's private key
            await monitor.addPrivkey(ndk.signer as NDKPrivateKeySigner);

            // Directly call checkAndRedeemGroup to ensure state changes are emitted
            const group = { mint: "https://testmint.com", nutzaps: [unspentNutzap, spentNutzap] };

            // Listen for state changes
            const stateChanges = new Map<NDKEventId, NdkNutzapStatus>();
            monitor.on("state_changed", (id, status) => {
                stateChanges.set(id, status);
            });

            // Process the nutzaps
            await (monitor as any).checkAndRedeemGroup(group);

            // Check that spent nutzap was marked as SPENT
            expect(monitor.nutzapStates.get(spentNutzap.id)?.status).toBe(NdkNutzapStatus.SPENT);
        });

        it("should process multiple pages of nutzaps when there are unspent nutzaps", async () => {
            // Get the user's pubkey
            const userPubkey = (await ndk.signer?.user()).pubkey;

            // Create nutzaps for page 1
            const nutzap1 = await mockNutzap("https://testmint.com", 100, ndk, {
                recipientPubkey: userPubkey,
            });
            nutzap1.created_at = 1000;

            // Create nutzaps for page 2
            const nutzap2 = await mockNutzap("https://testmint.com", 200, ndk, {
                recipientPubkey: userPubkey,
            });
            nutzap2.created_at = 500; // Older nutzap for page 2

            // Setup fetchPage mock to return different results based on filter
            let fetchPageCallCount = 0;
            (fetchPage as vi.Mock).mockImplementation((_ndk, _filter) => {
                fetchPageCallCount++;
                if (fetchPageCallCount === 1) {
                    return [nutzap1];
                }
                return [nutzap2];
            });

            // Setup getProofSpendState mock to return all nutzaps as unspent
            vi.spyOn(SpendStatusModule, "getProofSpendState").mockImplementation(async (_wallet, nutzaps) => {
                return {
                    unspentProofs: nutzaps.flatMap((n) => n.proofs),
                    spentProofs: [],
                    nutzapsWithUnspentProofs: nutzaps,
                    nutzapsWithSpentProofs: [],
                };
            });

            // Add user's private key
            await monitor.addPrivkey(ndk.signer as NDKPrivateKeySigner);

            // Mock processAccumulatedNutzaps to avoid infinite recursion in tests
            const originalProcessAccumulatedNutzaps = monitor.processAccumulatedNutzaps;
            monitor.processAccumulatedNutzaps = vi.fn().mockImplementation(async (filter = {}) => {
                // Call the original once to process the first page
                if (!filter.since) {
                    await originalProcessAccumulatedNutzaps.call(monitor, filter);
                    // Manually call with the second filter to simulate recursion
                    await originalProcessAccumulatedNutzaps.call(monitor, {
                        ...filter,
                        since: 999,
                    });
                }
            });

            // Spy on processNutzaps
            const processSpy = vi.spyOn(monitor as any, "processNutzaps");

            // Process the nutzaps
            await monitor.processAccumulatedNutzaps();

            // fetchPage should have been called twice - once for each page
            expect(fetchPage).toHaveBeenCalledTimes(2);

            // processNutzaps should have been called twice
            expect(processSpy).toHaveBeenCalledTimes(2);

            // Check both nutzaps were processed
            expect(monitor.nutzapStates.has(nutzap1.id)).toBe(true);
            expect(monitor.nutzapStates.has(nutzap2.id)).toBe(true);
        });
    });

    describe("start", () => {
        it.skip("should load nutzap states from the store and process redeemable nutzaps", async () => {
            // Create a nutzap and add it to the store
            const userSigner = ndk.signer as NDKPrivateKeySigner;
            const userPubkey = (await userSigner.user()).pubkey;
            const nutzap = await mockNutzap("https://testmint.com", 100, ndk, {
                recipientPubkey: userPubkey,
            });

            await mockStore.setNutzapState(nutzap.id, {
                nutzap,
                status: NdkNutzapStatus.INITIAL,
            });

            // Mock getProofSpendState to return unspent proofs
            vi.spyOn(SpendStatusModule, "getProofSpendState").mockResolvedValue({
                unspentProofs: nutzap.proofs,
                spentProofs: [],
                nutzapsWithUnspentProofs: [nutzap],
                nutzapsWithSpentProofs: [],
            });

            // Mock fetchPage to return empty array
            (fetchPage as vi.Mock).mockResolvedValue([]);

            // Add the user's private key
            await monitor.addPrivkey(userSigner);

            // Spy on redeemNutzaps
            const redeemSpy = vi.spyOn(monitor, "redeemNutzaps").mockResolvedValue(undefined);

            // Start the monitor
            await monitor.start({ filter: {} });

            // Check that redeemNutzaps was called for the nutzap from the store
            expect(redeemSpy).toHaveBeenCalled();
            const callArgs = redeemSpy.mock.calls[0];
            expect(callArgs[0]).toBe(nutzap.mint);
            expect(callArgs[1]).toContainEqual(expect.objectContaining({ id: nutzap.id }));
        });

        it.skip("should start a subscription and process incoming nutzaps", async () => {
            // Mock fetchPage to return empty array
            (fetchPage as vi.Mock).mockResolvedValue([]);

            // Spy on NDK.subscribe
            const subscribeSpy = vi.spyOn(ndk, "subscribe");

            // Start the monitor
            await monitor.start({ filter: {} });

            // Check that a subscription was started
            expect(subscribeSpy).toHaveBeenCalled();

            // Create a nutzap coming in through the subscription
            const userSigner = ndk.signer as NDKPrivateKeySigner;
            const userPubkey = (await userSigner.user()).pubkey;
            const nutzap = await mockNutzap("https://testmint.com", 100, ndk, {
                recipientPubkey: userPubkey,
            });

            // Mock the event handler
            const eventHandlerSpy = vi.spyOn(monitor as any, "eventHandler");

            // Access NDKNutzap.from directly to preserve its type
            const originalFrom = NDKNutzap.from;

            // Use ts-ignore here because we're intentionally mocking a static method
            // @ts-ignore - mocking static method
            NDKNutzap.from = vi.fn().mockImplementation(async (_event: NDKEvent) => {
                // Return our mock nutzap
                return nutzap;
            });

            // Extract the subscription callback
            const subOptions = subscribeSpy.mock.calls[0][3];

            // Call the subscription callback with a new event
            if (subOptions && typeof subOptions === "object" && "onEvent" in subOptions) {
                const nutzapEvent = new NDKEvent(ndk);
                nutzapEvent.id = nutzap.id;
                // @ts-ignore - suppressing type error for testing
                subOptions.onEvent(nutzapEvent);
            }

            // Check that the event handler was called
            expect(eventHandlerSpy).toHaveBeenCalled();

            // Restore the original function
            // @ts-ignore - restoring original method
            NDKNutzap.from = originalFrom;
        });

        it.skip("should handle exceptions when loading data from the store", async () => {
            // Create a store that throws an error when getAllNutzaps is called
            const errorStore: NDKNutzapMonitorStore = {
                getAllNutzaps: vi.fn().mockRejectedValue(new Error("Database error")),
                setNutzapState: vi.fn().mockResolvedValue(undefined),
            };

            // Create monitor with error store
            const errorMonitor = new NDKNutzapMonitor(ndk, user, { mintList, store: errorStore });

            // Mock fetchPage to return empty array
            (fetchPage as vi.Mock).mockResolvedValue([]);

            // Spy on console.error
            const errorSpy = vi.spyOn(console, "error");

            // Start the monitor
            await errorMonitor.start({ filter: {} });

            // Check that the error was logged
            expect(errorSpy).toHaveBeenCalledWith(
                expect.stringContaining("Failed to load nutzaps from store"),
                expect.any(Error),
            );
        });
    });

    describe("store integration", () => {
        it.skip("should load nutzap states from the store on startup", async () => {
            // Create a new mock store with pre-populated state
            const newStore = createMockStore();
            const existingNutzap = await mockNutzap("https://testmint.com", 100, ndk);

            // Add a nutzap state to the store
            await newStore.setNutzapState(existingNutzap.id, {
                nutzap: existingNutzap,
                status: NdkNutzapStatus.REDEEMED,
                redeemedAmount: 100,
            });

            // Create a new monitor with this store
            const newMonitor = new NDKNutzapMonitor(ndk, user, { mintList, store: newStore });

            // Mock fetchPage to return empty array (so we don't process new nutzaps)
            (fetchPage as vi.Mock).mockResolvedValue([]);

            // Start the monitor (this should load states from the store)
            await newMonitor.start({ filter: {} });

            // Check that the state was loaded from the store
            const state = newMonitor.nutzapStates.get(existingNutzap.id);
            expect(state?.status).toBe(NdkNutzapStatus.REDEEMED);
            expect(state?.redeemedAmount).toBe(100);
        });

        it("should update the store when nutzap states change", async () => {
            // Create a nutzap
            const nutzap = await mockNutzap("https://testmint.com", 100, ndk);

            // Create a method to access the private updateNutzapState method
            const updateState = (id: string, state: Partial<NDKNutzapState>) => {
                // @ts-ignore - accessing private method for testing
                return monitor.updateNutzapState(id, state);
            };

            // Update the nutzap state
            updateState(nutzap.id, {
                nutzap,
                status: NdkNutzapStatus.PROCESSING,
            });

            // Check that the store was updated
            expect(mockStore.setNutzapStateSpy).toHaveBeenCalledWith(
                nutzap.id,
                expect.objectContaining({ status: NdkNutzapStatus.PROCESSING }),
            );
        });
    });

    describe("shouldTryRedeem", () => {
        it("should return false for nutzaps with PERMANENT_ERROR status", async () => {
            // Create a nutzap
            const signer = NDKPrivateKeySigner.generate();
            const pubkey = (await signer.user()).pubkey;
            const nutzap = await mockNutzap("https://testmint.com", 100, ndk, {
                recipientPubkey: pubkey,
            });

            // Set it to PERMANENT_ERROR state
            monitor.nutzapStates.set(nutzap.id, {
                nutzap,
                status: NdkNutzapStatus.PERMANENT_ERROR,
                errorMessage: "Invalid p2pk: unknown public key size",
            });

            // It should not be retried
            expect(monitor.shouldTryRedeem(nutzap)).toBe(false);
        });

        it("should return true for nutzaps with INITIAL status", async () => {
            // Create a nutzap
            const signer = NDKPrivateKeySigner.generate();
            const pubkey = (await signer.user()).pubkey;
            const nutzap = await mockNutzap("https://testmint.com", 100, ndk, {
                recipientPubkey: pubkey,
            });

            // Set it to INITIAL state
            monitor.nutzapStates.set(nutzap.id, {
                nutzap,
                status: NdkNutzapStatus.INITIAL,
            });

            // It should be retried
            expect(monitor.shouldTryRedeem(nutzap)).toBe(true);
        });

        it("should return true for unknown nutzaps", async () => {
            // Create a nutzap without setting any state
            const signer = NDKPrivateKeySigner.generate();
            const pubkey = (await signer.user()).pubkey;
            const nutzap = await mockNutzap("https://testmint.com", 100, ndk, {
                recipientPubkey: pubkey,
            });

            // It should be retried since it's unknown
            expect(monitor.shouldTryRedeem(nutzap)).toBe(true);
        });

        it("should return true for nutzaps in MISSING_PRIVKEY state when we now have the key", async () => {
            // Create a signer
            const signer = NDKPrivateKeySigner.generate();
            const pubkey = (await signer.user()).pubkey;

            // Create a nutzap that requires this privkey
            const nutzap = await mockNutzap("https://testmint.com", 100, ndk, {
                recipientPubkey: pubkey,
            });

            // Set it to MISSING_PRIVKEY state
            monitor.nutzapStates.set(nutzap.id, {
                nutzap,
                status: NdkNutzapStatus.MISSING_PRIVKEY,
            });

            // Add the privkey
            await monitor.addPrivkey(signer);

            // It should be retried now that we have the key
            expect(monitor.shouldTryRedeem(nutzap)).toBe(true);
        });

        it("should return false for nutzaps that have been redeemed or spent", async () => {
            // Create a nutzap
            const nutzap = await mockNutzap("https://testmint.com", 100, ndk);

            // Set it to REDEEMED state
            monitor.nutzapStates.set(nutzap.id, {
                nutzap,
                status: NdkNutzapStatus.REDEEMED,
                redeemedAmount: 100,
            });

            // It should not be retried
            expect(monitor.shouldTryRedeem(nutzap)).toBe(false);

            // Set it to SPENT state
            monitor.nutzapStates.set(nutzap.id, {
                nutzap,
                status: NdkNutzapStatus.SPENT,
            });

            // It should not be retried
            expect(monitor.shouldTryRedeem(nutzap)).toBe(false);
        });
    });

    describe("processNutzaps", () => {
        it.skip("should correctly identify the oldest unspent nutzap", async () => {
            // Create nutzaps with different timestamps
            const olderNutzap = await mockNutzap("https://testmint.com", 100, ndk);
            olderNutzap.created_at = 1000; // Older timestamp

            const newerNutzap = await mockNutzap("https://testmint.com", 200, ndk);
            newerNutzap.created_at = 2000; // Newer timestamp

            // Add to the monitor
            monitor.nutzapStates.set(olderNutzap.id, { status: NdkNutzapStatus.INITIAL });
            monitor.nutzapStates.set(newerNutzap.id, { status: NdkNutzapStatus.INITIAL });

            // Mock fetchPage to return these nutzaps
            (fetchPage as vi.Mock).mockResolvedValue([olderNutzap, newerNutzap]);

            // Call the method
            const result = await monitor.processNutzaps({ cashuPubkey: "pubkey123" });

            // Should return the timestamp of the oldest nutzap
            expect(result).toBe(1000);
        });
    });
});

describe("NIP-61 specific functionality", () => {
    let ndk: NDK;
    let user: NDKUser;
    let mintList: NDKCashuMintList;
    let mockStore: MockStore;
    let monitor: NDKNutzapMonitor;

    beforeEach(async () => {
        // Setup NDK with a signer
        ndk = new NDK({
            signer: NDKPrivateKeySigner.generate(),
            explicitRelayUrls: ["wss://relay1.com"],
        });

        user = await ndk.signer?.user();

        // Setup a mint list
        mintList = new NDKCashuMintList(ndk);
        mintList.mints = ["https://testmint.com"];

        // Mock CashuWallet
        const mockCashuWallet = new CashuWallet(new CashuMint("https://testmint.com"));
        vi.spyOn(mockCashuWallet, "checkProofsStates").mockResolvedValue([]);
        vi.spyOn(CashuMintModule, "walletForMint").mockResolvedValue(mockCashuWallet);

        // Setup mock store
        mockStore = createMockStore();

        // Create the monitor
        monitor = new NDKNutzapMonitor(ndk, user, { mintList, store: mockStore });

        // Setup mock NDKCashuWallet
        const mockNDKCashuWallet = new NDKCashuWallet(ndk);
        mockNDKCashuWallet.redeemNutzaps = vi.fn().mockResolvedValue(100);
        monitor.wallet = mockNDKCashuWallet;

        // Mock console methods
        vi.spyOn(console, "error").mockImplementation(() => {});
        vi.spyOn(console, "log").mockImplementation(() => {});
    });

    afterEach(() => {
        vi.clearAllMocks();
    });

    it("should handle nutzaps with different p2pk formats correctly", async () => {
        // Create nutzaps with different p2pk formats (compressed and uncompressed keys)
        const signer1 = NDKPrivateKeySigner.generate(); // First private key
        const signer2 = NDKPrivateKeySigner.generate(); // Second private key
        const pubkey1 = (await signer1.user()).pubkey;
        const pubkey2 = (await signer2.user()).pubkey;

        // Mock different p2pk formats (one with 02 prefix, one without)
        const nutzap1 = await mockNutzap("https://testmint.com", 100, ndk, {
            recipientPubkey: pubkey1,
        });
        const nutzap2 = await mockNutzap("https://testmint.com", 200, ndk, {
            recipientPubkey: pubkey2,
        });

        // Add both signers to the monitor
        await monitor.addPrivkey(signer1);
        await monitor.addPrivkey(signer2);

        // Mock CashuWallet
        const mockCashuWallet = new CashuWallet(new CashuMint("https://testmint.com"));
        vi.spyOn(CashuMintModule, "walletForMint").mockResolvedValue(mockCashuWallet);

        // Mock getProofSpendState to return unspent proofs
        vi.spyOn(SpendStatusModule, "getProofSpendState").mockResolvedValue({
            unspentProofs: [...nutzap1.proofs, ...nutzap2.proofs],
            spentProofs: [],
            nutzapsWithUnspentProofs: [nutzap1, nutzap2],
            nutzapsWithSpentProofs: [],
        });

        // Spy on redeemNutzaps to verify it gets called for both nutzaps
        const redeemSpy = vi.spyOn(monitor, "redeemNutzaps");

        // Process the nutzaps
        await (monitor as any).processNutzaps([nutzap1, nutzap2]);

        // redeemNutzaps should be called for both nutzaps
        expect(redeemSpy).toHaveBeenCalled();

        // Check that we have states for both nutzaps
        expect(monitor.nutzapStates.has(nutzap1.id)).toBe(true);
        expect(monitor.nutzapStates.has(nutzap2.id)).toBe(true);
    });

    it("should emit 'seen_in_unknown_mint' event when nutzap is for an unknown mint", async () => {
        // Create a nutzap with a mint not in the mintList
        const unknownMint = "https://unknown-mint.com";
        const userSigner = ndk.signer as NDKPrivateKeySigner;
        const userPubkey = (await userSigner.user()).pubkey;
        const nutzap = await mockNutzap(unknownMint, 100, ndk, { recipientPubkey: userPubkey });

        // Spy on the event
        const seenInUnknownMintSpy = vi.fn();
        monitor.on("seen_in_unknown_mint", seenInUnknownMintSpy);

        // Add the event handler manually (simulating a subscription)
        await (monitor as any).eventHandler(nutzap);

        // The event should be emitted
        expect(seenInUnknownMintSpy).toHaveBeenCalled();
        const calledWithNutzap = seenInUnknownMintSpy.mock.calls[0][0];
        expect(calledWithNutzap.id).toBe(nutzap.id);
        expect(calledWithNutzap.mint).toBe(unknownMint);
    });

    it("should handle nutzaps with multiple proofs with different p2pk values", async () => {
        // Create two signers
        const signer1 = NDKPrivateKeySigner.generate();
        const signer2 = NDKPrivateKeySigner.generate();
        const pubkey1 = (await signer1.user()).pubkey;
        const pubkey2 = (await signer2.user()).pubkey;

        // Add both keys to the monitor
        await monitor.addPrivkey(signer1);
        await monitor.addPrivkey(signer2);

        // Create a nutzap with proofs for both keys
        const nutzap1 = await mockNutzap("https://testmint.com", 100, ndk, {
            recipientPubkey: pubkey1,
        });
        const nutzap2 = await mockNutzap("https://testmint.com", 200, ndk, {
            recipientPubkey: pubkey2,
        });

        // Mock CashuWallet
        const mockCashuWallet = new CashuWallet(new CashuMint("https://testmint.com"));
        vi.spyOn(CashuMintModule, "walletForMint").mockResolvedValue(mockCashuWallet);

        // Mock getProofSpendState to return unspent proofs
        vi.spyOn(SpendStatusModule, "getProofSpendState").mockResolvedValue({
            unspentProofs: [...nutzap1.proofs, ...nutzap2.proofs],
            spentProofs: [],
            nutzapsWithUnspentProofs: [nutzap1, nutzap2],
            nutzapsWithSpentProofs: [],
        });

        // Spy on redeemNutzaps
        const redeemSpy = vi.spyOn(monitor, "redeemNutzaps");

        // Process the nutzaps
        await (monitor as any).processNutzaps([nutzap1, nutzap2]);

        // redeemNutzaps should be called for both nutzaps
        expect(redeemSpy).toHaveBeenCalled();

        // Check that we have states for both nutzaps
        expect(monitor.nutzapStates.has(nutzap1.id)).toBe(true);
        expect(monitor.nutzapStates.has(nutzap2.id)).toBe(true);
    });

    it("should correctly identify nutzaps that need to be retrieved from the backup", async () => {
        // Create a signer not initially in the monitor
        const backupSigner = NDKPrivateKeySigner.generate();
        const backupPubkey = (await backupSigner.user()).pubkey;

        // Create a nutzap p2pked to this signer
        const nutzap = await mockNutzap("https://testmint.com", 100, ndk, {
            recipientPubkey: backupPubkey,
        });

        // Setup a mock backup event
        const backupEvent = new NDKEvent(ndk);
        const backup = new NDKCashuWalletBackup(ndk);
        backup.privkeys = [backupSigner.privateKey!];

        // Mock NDKCashuWalletBackup.from to return our backup
        vi.spyOn(NDKCashuWalletBackup, "from").mockResolvedValue(backup);

        // Mock fetchEvents to return our backup event
        ndk.fetchEvents = vi.fn().mockResolvedValue(new Set([backupEvent]));

        // Try to redeem the nutzap (should initially fail due to missing key)
        await monitor.redeemNutzap(nutzap);

        // Check that the nutzap is in MISSING_PRIVKEY state
        expect(monitor.nutzapStates.get(nutzap.id)?.status).toBe(NdkNutzapStatus.MISSING_PRIVKEY);

        // Mock getProofSpendState to return unspent proofs when we retry
        vi.spyOn(SpendStatusModule, "getProofSpendState").mockResolvedValue({
            unspentProofs: nutzap.proofs,
            spentProofs: [],
            nutzapsWithUnspentProofs: [nutzap],
            nutzapsWithSpentProofs: [],
        });

        // Manually add the private key to simulate what getBackupKeys would do
        await monitor.addPrivkey(backupSigner);

        // Check that the privkey was added to the monitor
        expect(monitor.privkeys.has(backupPubkey)).toBe(true);

        // Try to redeem the nutzap again
        const result = monitor.shouldTryRedeem(nutzap);

        // It should now return true since we now have the key
        expect(result).toBe(true);
    });
});
