import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { dbInsertUsers, dbReset } from "~/utils/Test";
import { APP_ICON_URL } from "~/utils/urls";
import * as NotificationRepository from "../NotificationRepository.server";
import { clearSentNotificationsForTesting, notify } from "./notify.server";

const { mockSendNotification, mockWebPushEnabled } = vi.hoisted(() => ({
	mockSendNotification: vi.fn(),
	mockWebPushEnabled: { value: false },
}));

vi.mock("./webPush.server", () => ({
	get webPushEnabled() {
		return mockWebPushEnabled.value;
	},
	default: {
		sendNotification: mockSendNotification,
	},
}));

describe("notify()", () => {
	beforeEach(async () => {
		await dbInsertUsers(20);
		clearSentNotificationsForTesting();
	});

	afterEach(() => {
		dbReset();
	});

	test("different recipients receive same notification", async () => {
		await notify({
			userIds: [1, 2],
			notification: {
				type: "SCRIM_NEW_REQUEST",
				meta: { fromUsername: "alice" },
			},
		});

		await notify({
			userIds: [3, 4],
			notification: {
				type: "SCRIM_NEW_REQUEST",
				meta: { fromUsername: "alice" },
			},
		});

		const user1Notifications = await NotificationRepository.findByUserId(1);
		const user2Notifications = await NotificationRepository.findByUserId(2);
		const user3Notifications = await NotificationRepository.findByUserId(3);
		const user4Notifications = await NotificationRepository.findByUserId(4);

		expect(user1Notifications).toHaveLength(1);
		expect(user2Notifications).toHaveLength(1);
		expect(user3Notifications).toHaveLength(1);
		expect(user4Notifications).toHaveLength(1);

		expect(user1Notifications[0].type).toBe("SCRIM_NEW_REQUEST");
		expect(user1Notifications[0].meta).toEqual({ fromUsername: "alice" });
	});

	test("same recipients and notification deduplicates", async () => {
		await notify({
			userIds: [5, 6],
			notification: {
				type: "BADGE_ADDED",
				meta: { badgeName: "Test", badgeId: 1 },
			},
		});

		await notify({
			userIds: [5, 6],
			notification: {
				type: "BADGE_ADDED",
				meta: { badgeName: "Test", badgeId: 1 },
			},
		});

		const user5Notifications = await NotificationRepository.findByUserId(5);
		const user6Notifications = await NotificationRepository.findByUserId(6);

		expect(user5Notifications).toHaveLength(1);
		expect(user6Notifications).toHaveLength(1);
	});

	test("user ID order doesn't affect deduplication", async () => {
		await notify({
			userIds: [7, 8, 9],
			notification: {
				type: "SEASON_STARTED",
				meta: { seasonNth: 1 },
			},
		});

		await notify({
			userIds: [9, 7, 8],
			notification: {
				type: "SEASON_STARTED",
				meta: { seasonNth: 1 },
			},
		});

		const user7Notifications = await NotificationRepository.findByUserId(7);
		const user8Notifications = await NotificationRepository.findByUserId(8);
		const user9Notifications = await NotificationRepository.findByUserId(9);

		expect(user7Notifications).toHaveLength(1);
		expect(user8Notifications).toHaveLength(1);
		expect(user9Notifications).toHaveLength(1);
	});

	test("bulk notifications (>10 users) bypass deduplication", async () => {
		const userIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];

		await notify({
			userIds,
			notification: {
				type: "TO_CHECK_IN_OPENED",
				meta: { tournamentId: 1, tournamentName: "Test Tournament" },
			},
		});

		await notify({
			userIds,
			notification: {
				type: "TO_CHECK_IN_OPENED",
				meta: { tournamentId: 1, tournamentName: "Test Tournament" },
			},
		});

		const user1Notifications = await NotificationRepository.findByUserId(1);
		const user11Notifications = await NotificationRepository.findByUserId(11);

		expect(user1Notifications).toHaveLength(2);
		expect(user11Notifications).toHaveLength(2);
	});

	test("different notification types don't deduplicate", async () => {
		await notify({
			userIds: [10, 11],
			notification: {
				type: "SCRIM_SCHEDULED",
				meta: { id: 1, opponentTeamName: "Alpha" },
			},
		});

		await notify({
			userIds: [10, 11],
			notification: {
				type: "SCRIM_CANCELED",
				meta: { id: 1, opponentTeamName: "Alpha" },
			},
		});

		const user10Notifications = await NotificationRepository.findByUserId(10);
		const user11Notifications = await NotificationRepository.findByUserId(11);

		expect(user10Notifications).toHaveLength(2);
		expect(user11Notifications).toHaveLength(2);

		const types = user10Notifications.map((n) => n.type).sort();
		expect(types).toEqual(["SCRIM_CANCELED", "SCRIM_SCHEDULED"]);
	});

	test("different notification meta don't deduplicate", async () => {
		await notify({
			userIds: [12, 13],
			notification: {
				type: "SCRIM_NEW_REQUEST",
				meta: { fromUsername: "bob" },
			},
		});

		await notify({
			userIds: [12, 13],
			notification: {
				type: "SCRIM_NEW_REQUEST",
				meta: { fromUsername: "charlie" },
			},
		});

		const user12Notifications = await NotificationRepository.findByUserId(12);
		const user13Notifications = await NotificationRepository.findByUserId(13);

		expect(user12Notifications).toHaveLength(2);
		expect(user13Notifications).toHaveLength(2);

		const metas = user12Notifications.map((n) => n.meta);
		expect(metas).toContainEqual({ fromUsername: "bob" });
		expect(metas).toContainEqual({ fromUsername: "charlie" });
	});

	test("duplicate user IDs in input array are deduplicated", async () => {
		await notify({
			userIds: [14, 14, 15, 15, 15],
			notification: {
				type: "PLUS_VOTING_STARTED",
				meta: { seasonNth: 2 },
			},
		});

		const user14Notifications = await NotificationRepository.findByUserId(14);
		const user15Notifications = await NotificationRepository.findByUserId(15);

		expect(user14Notifications).toHaveLength(1);
		expect(user15Notifications).toHaveLength(1);
	});
});

describe("notify() - web push notifications", () => {
	beforeEach(async () => {
		await dbInsertUsers(20);
		clearSentNotificationsForTesting();
		mockSendNotification.mockClear();
		mockWebPushEnabled.value = false;
	});

	afterEach(() => {
		dbReset();
	});

	test("sends web push notification when user has subscription", async () => {
		const mockSubscription = {
			endpoint: "https://fcm.googleapis.com/fcm/send/test",
			keys: {
				auth: "test-auth-key",
				p256dh: "test-p256dh-key",
			},
		};

		vi.spyOn(
			NotificationRepository,
			"subscriptionsByUserIds",
		).mockResolvedValue([
			{
				id: 1,
				subscription: mockSubscription,
			},
		]);

		mockWebPushEnabled.value = true;

		await notify({
			userIds: [1],
			notification: {
				type: "SCRIM_NEW_REQUEST",
				meta: { fromUsername: "alice" },
			},
		});

		expect(mockSendNotification).toHaveBeenCalledTimes(1);
		expect(mockSendNotification).toHaveBeenCalledWith(
			mockSubscription,
			expect.any(String),
			{ urgency: "high" },
		);

		const callArgs = mockSendNotification.mock.calls[0][1];
		const payload = JSON.parse(callArgs);
		expect(payload.title).toBe("New Scrim Request");
		expect(payload.body).toBe("alice requested a scrim");
		expect(payload.data.url).toBe("/scrims");
		expect(payload.icon).toBe(APP_ICON_URL);
	});

	test("sends web push to multiple subscriptions", async () => {
		const mockSubscription1 = {
			endpoint: "https://fcm.googleapis.com/fcm/send/test1",
			keys: {
				auth: "test-auth-key-1",
				p256dh: "test-p256dh-key-1",
			},
		};

		const mockSubscription2 = {
			endpoint: "https://fcm.googleapis.com/fcm/send/test2",
			keys: {
				auth: "test-auth-key-2",
				p256dh: "test-p256dh-key-2",
			},
		};

		vi.spyOn(
			NotificationRepository,
			"subscriptionsByUserIds",
		).mockResolvedValue([
			{
				id: 1,
				subscription: mockSubscription1,
			},
			{
				id: 2,
				subscription: mockSubscription2,
			},
		]);

		mockWebPushEnabled.value = true;

		await notify({
			userIds: [1, 2],
			notification: {
				type: "BADGE_ADDED",
				meta: { badgeName: "Test", badgeId: 1 },
			},
		});

		expect(mockSendNotification).toHaveBeenCalledTimes(2);
		expect(mockSendNotification).toHaveBeenCalledWith(
			mockSubscription1,
			expect.any(String),
			{ urgency: "normal" },
		);
		expect(mockSendNotification).toHaveBeenCalledWith(
			mockSubscription2,
			expect.any(String),
			{ urgency: "normal" },
		);
	});

	test("does not send web push when webPushEnabled is false", async () => {
		const mockSubscription = {
			endpoint: "https://fcm.googleapis.com/fcm/send/test",
			keys: {
				auth: "test-auth-key",
				p256dh: "test-p256dh-key",
			},
		};

		vi.spyOn(
			NotificationRepository,
			"subscriptionsByUserIds",
		).mockResolvedValue([
			{
				id: 1,
				subscription: mockSubscription,
			},
		]);

		await notify({
			userIds: [1],
			notification: {
				type: "SCRIM_NEW_REQUEST",
				meta: { fromUsername: "alice" },
			},
		});

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

	test("includes opponent team name for scrim notifications", async () => {
		const mockSubscription = {
			endpoint: "https://fcm.googleapis.com/fcm/send/test",
			keys: {
				auth: "test-auth-key",
				p256dh: "test-p256dh-key",
			},
		};

		vi.spyOn(
			NotificationRepository,
			"subscriptionsByUserIds",
		).mockResolvedValue([
			{
				id: 1,
				subscription: mockSubscription,
			},
		]);

		mockWebPushEnabled.value = true;

		await notify({
			userIds: [1],
			notification: {
				type: "SCRIM_SCHEDULED",
				meta: { id: 1, opponentTeamName: "Sendou's pickup" },
			},
		});

		expect(mockSendNotification).toHaveBeenCalledTimes(1);

		const callArgs = mockSendNotification.mock.calls[0][1];
		const payload = JSON.parse(callArgs);

		expect(payload.title).toBe("Scrim Scheduled");
		expect(payload.body).toBe("New scrim scheduled vs. Sendou's pickup");
	});
});
