import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { EventGenerator } from "../../test";
import type { NDKEvent } from "../events";
import { NDK } from "../ndk";
import { NDKUser, type NDKUserParams, type ProfilePointer } from "./index.js";
import * as Nip05 from "./nip05.js";

describe("NDKUser", () => {
    let ndk: NDK;

    const FROZEN_TIME = new Date("2020-01-01T00:00:00.000Z");
    const NOW_SEC = Math.floor(FROZEN_TIME.getTime() / 1000);

    beforeEach(() => {
        vi.clearAllMocks();

        vi.useFakeTimers();
        vi.setSystemTime(FROZEN_TIME);

        ndk = new NDK();
        EventGenerator.setNDK(ndk);
    });

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

    describe("constructor", () => {
        it("sets npub from provided npub", () => {
            const opts: NDKUserParams = {
                npub: "npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft",
            };

            const user = new NDKUser(opts);

            expect(user.npub).toEqual("npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft");
        });

        it("sets npub from provided hexpubkey", () => {
            const opts: NDKUserParams = {
                pubkey: "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52",
            };

            const user = new NDKUser(opts);
            expect(user.npub).toEqual("npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft");
        });

        it("sets relayUrls from provided relayUrls", () => {
            const opts: NDKUserParams = {
                relayUrls: ["url1", "url2"],
            };

            const user = new NDKUser(opts);

            expect(user.relayUrls).toEqual(["url1", "url2"]);
        });

        it("sets pubkey and relayUrls from provided nprofile", () => {
            const opts: NDKUserParams = {
                nprofile:
                    "nprofile1qqs04xzt6ldm9qhs0ctw0t58kf4z57umjzmjg6jywu0seadwtqqc75spr9mhxue69uhhq7tjv9kkjepwve5kzar2v9nzucm0d5qscamnwvaz7tmxxaazu6t0f6uyq5",
            };

            const user = new NDKUser(opts);

            expect(user.pubkey).toEqual("fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52");
            expect(user.relayUrls).toEqual(["wss://pyramid.fiatjaf.com", "wss://f7z.io"]);
        });
    });

    describe("pubkey", () => {
        it("returns the decoded pubkey", () => {
            const user = new NDKUser({
                npub: "npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft",
            });

            const pubkey = user.pubkey;

            expect(pubkey).toEqual("fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52");
        });
    });

    describe("fetchProfile", () => {
        let newEvent: NDKEvent;
        let oldEvent: NDKEvent;
        let user: NDKUser;
        let pubkey: string;

        beforeEach(() => {
            user = new NDKUser({
                npub: "npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft",
            });
            user.ndk = ndk;
            pubkey = user.pubkey;
        });

        it("profile returns metadata event", async () => {
            const event = EventGenerator.createEvent(
                0,
                JSON.stringify({
                    name: "Jeff",
                    picture: "https://image.url",
                }),
                pubkey,
            );
            event.created_at = NOW_SEC - 7200;

            ndk.fetchEvent = vi.fn().mockResolvedValueOnce(event);

            const profile = await user.fetchProfile();
            const metadataevent = JSON.parse(profile.profileEvent);

            expect(metadataevent.id).toEqual(event.id);
        });

        it("duplicate fetching of profile", async () => {
            const event = EventGenerator.createEvent(
                0,
                JSON.stringify({
                    name: "Jeff",
                    picture: "https://image.url",
                }),
                pubkey,
            );
            event.created_at = NOW_SEC - 7200;

            ndk.fetchEvent = vi.fn().mockResolvedValueOnce(event);

            const profile = await user.fetchProfile();
            expect(profile?.name).toEqual("Jeff");
            expect(profile?.picture).toEqual("https://image.url");

            const profile2 = await user.fetchProfile();
            expect(profile2?.name).toEqual("Jeff");
            expect(profile2?.picture).toEqual("https://image.url");
        });

        it("newer profile overwrites older profile (two fetches)", async () => {
            oldEvent = EventGenerator.createEvent(
                0,
                JSON.stringify({
                    name: "Jeff_OLD",
                    picture: "https://image.url.old",
                    about: "About jeff OLD",
                }),
                pubkey,
            );
            oldEvent.created_at = NOW_SEC - 7200;

            ndk.fetchEvent = vi.fn().mockResolvedValueOnce(oldEvent);

            await user.fetchProfile();
            expect(user.profile?.name).toEqual("Jeff_OLD");
            expect(user.profile?.picture).toEqual("https://image.url.old");
            expect(user.profile?.about).toEqual("About jeff OLD");

            newEvent = EventGenerator.createEvent(
                0,
                JSON.stringify({
                    name: "Jeff",
                    picture: "https://image.url",
                    about: "About jeff",
                }),
                pubkey,
            );
            newEvent.created_at = NOW_SEC;

            ndk.fetchEvent = vi.fn().mockResolvedValueOnce(newEvent);
            await user.fetchProfile();
            expect(user.profile?.name).toEqual("Jeff");
            expect(user.profile?.picture).toEqual("https://image.url");
            expect(user.profile?.about).toEqual("About jeff");
        });

        it.skip("older profile does not overwrite newer profile (two fetches)", async () => {
            newEvent = EventGenerator.createEvent(
                0,
                JSON.stringify({
                    name: "Jeff",
                    picture: "https://image.url",
                    about: "About jeff",
                }),
                pubkey,
            );
            newEvent.created_at = NOW_SEC - 3600;

            oldEvent = EventGenerator.createEvent(
                0,
                JSON.stringify({
                    name: "Jeff_OLD",
                    picture: "https://image.url.old",
                    about: "About jeff OLD",
                }),
                pubkey,
            );
            oldEvent.created_at = NOW_SEC - 7200;

            ndk.fetchEvent = vi.fn().mockResolvedValue(newEvent);

            await user.fetchProfile();
            expect(user.profile?.name).toEqual("Jeff");
            expect(user.profile?.picture).toEqual("https://image.url");
            expect(user.profile?.about).toEqual("About jeff");

            ndk.fetchEvent = vi.fn().mockResolvedValue(oldEvent);
            await user.fetchProfile();
            expect(user.profile?.name).toEqual("Jeff");
            expect(user.profile?.picture).toEqual("https://image.url");
            expect(user.profile?.about).toEqual("About jeff");
        });

        it("Returns updated fields", async () => {
            // Use EventGenerator to create profile events
            newEvent = EventGenerator.createEvent(
                0,
                JSON.stringify({
                    displayName: "JeffG",
                    name: "Jeff",
                    picture: "https://image.url",
                    banner: "https://banner.url",
                    bio: "Some bio info",
                    nip05: "_@jeffg.fyi",
                    lud06: "lud06value",
                    lud16: "lud16value",
                    about: "About jeff",
                }),
                pubkey,
            );
            newEvent.created_at = NOW_SEC - 3600;

            oldEvent = EventGenerator.createEvent(
                0,
                JSON.stringify({
                    displayName: "JeffG_OLD",
                    name: "Jeff_OLD",
                    picture: "https://image.url.old",
                    banner: "https://banner.url.old",
                    bio: "Some OLD bio info",
                    nip05: "OLD@jeffg.fyi",
                    lud06: "lud06value OLD",
                    lud16: "lud16value OLD",
                    about: "About jeff OLD",
                }),
                pubkey,
            );
            oldEvent.created_at = Math.floor(Date.now() / 1000) - 7200;

            ndk.fetchEvent = vi.fn().mockResolvedValue(newEvent);

            await user.fetchProfile();
            expect(user.profile?.displayName).toEqual("JeffG");
            expect(user.profile?.name).toEqual("Jeff");
            expect(user.profile?.picture).toEqual("https://image.url");
            expect(user.profile?.banner).toEqual("https://banner.url");
            expect(user.profile?.bio).toEqual("Some bio info");
            expect(user.profile?.nip05).toEqual("_@jeffg.fyi");
            expect(user.profile?.lud06).toEqual("lud06value");
            expect(user.profile?.lud16).toEqual("lud16value");
            expect(user.profile?.about).toEqual("About jeff");
        });

        // "displayName" is ignored, we only look at the "display_name" field in the user profile
        it("Display name is set properly", async () => {
            newEvent = EventGenerator.createEvent(
                0,
                JSON.stringify({
                    displayName: "JeffG",
                    display_name: "James",
                }),
                pubkey,
            );
            newEvent.created_at = NOW_SEC - 3600;

            oldEvent = EventGenerator.createEvent(
                0,
                JSON.stringify({
                    displayName: "Bob",
                }),
                pubkey,
            );
            oldEvent.created_at = NOW_SEC - 7200;

            ndk.fetchEvent = vi.fn().mockResolvedValue(newEvent);

            await user.fetchProfile();
            expect(user.profile?.displayName).toEqual("James");
        });

        // Both "image" and "picture" are set to the "image" field in the user profile
        it("Image is set properly", async () => {
            newEvent = EventGenerator.createEvent(
                0,
                JSON.stringify({
                    picture: "https://set-from-picture-field.url",
                }),
                pubkey,
            );
            newEvent.created_at = NOW_SEC - 3600;

            oldEvent = EventGenerator.createEvent(
                0,
                JSON.stringify({
                    picture: "https://set-from-image-field.url",
                }),
                pubkey,
            );
            oldEvent.created_at = NOW_SEC - 7200;

            ndk.fetchEvent = vi.fn().mockResolvedValue(newEvent);

            await user.fetchProfile();
            expect(user.profile?.picture).toEqual("https://set-from-picture-field.url");
        });

        it("Allows for arbitrary values to be set on user profiles", async () => {
            newEvent = EventGenerator.createEvent(
                0,
                JSON.stringify({
                    customField: "custom NEW",
                }),
                pubkey,
            );
            newEvent.created_at = NOW_SEC - 3600;

            oldEvent = EventGenerator.createEvent(
                0,
                JSON.stringify({
                    customField: "custom OLD",
                }),
                pubkey,
            );
            oldEvent.created_at = NOW_SEC - 7200;

            ndk.fetchEvent = vi.fn().mockResolvedValue(newEvent);

            await user.fetchProfile();
            expect(user.profile?.customField).toEqual("custom NEW");
        });
    });

    describe("validateNip05", () => {
        it("validates the NIP-05 for users", async () => {
            const user = await ndk.fetchUser("1739d937dc8c0c7370aa27585938c119e25c41f6c441a5d34c6d38503e3136ef");

            // Valid NIP-05
            const validNip05 = "_@jeffg.fyi";
            vi.spyOn(Nip05, "getNip05For").mockResolvedValue({
                pubkey: "1739d937dc8c0c7370aa27585938c119e25c41f6c441a5d34c6d38503e3136ef",
            } as ProfilePointer);
            expect(await user.validateNip05(validNip05)).toEqual(true);

            // Invalid NIP-05
            const invalidNip05 = "_@f7z.io";
            vi.spyOn(Nip05, "getNip05For").mockResolvedValue({
                pubkey: "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52",
            } as ProfilePointer);
            expect(await user.validateNip05(invalidNip05)).toEqual(false);

            // Random NIP-05
            const randomNip05 = "bobby@globalhypermeganet.com";
            vi.spyOn(Nip05, "getNip05For").mockResolvedValue(null);
            expect(await user.validateNip05(randomNip05)).toEqual(null);
        });
    });
});
