import { add } from "date-fns";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import * as CalendarRepository from "~/features/calendar/CalendarRepository.server";
import { clearAllTournamentDataCache } from "~/features/tournament-bracket/core/Tournament.server";
import { dateToDatabaseTimestamp } from "~/utils/dates";
import { dbInsertUsers, dbReset } from "~/utils/Test";
import { NotifyCheckInStartRoutine } from "./notifyCheckInStart";

const { mockNotify } = vi.hoisted(() => ({
	mockNotify: vi.fn(),
}));

vi.mock("~/features/notifications/core/notify.server", () => ({
	notify: mockNotify,
}));

async function createTestTournament({
	name,
	startTime,
	authorId = 1,
	discordInviteCode = "test-discord",
}: {
	name: string;
	startTime: Date;
	authorId?: number;
	discordInviteCode?: string;
}) {
	return CalendarRepository.create({
		isFullTournament: true,
		authorId,
		badges: [],
		bracketUrl: "https://example.com/bracket",
		description: null,
		discordInviteCode,
		name,
		organizationId: null,
		rules: null,
		startTimes: [dateToDatabaseTimestamp(startTime)],
		tags: null,
		bracketProgression: [
			{
				name: "Bracket",
				type: "single_elimination",
				requiresCheckIn: false,
				settings: {
					thirdPlaceMatch: false,
				},
			},
		],
		mapPickingStyle: "TO",
		mapPoolMaps: ([1, 2, 3, 4, 5] as const).map((id) => ({
			mode: "SZ",
			stageId: id,
		})),
	});
}

describe("NotifyCheckInStartRoutine", () => {
	beforeEach(async () => {
		vi.useFakeTimers();
		vi.setSystemTime(new Date("2025-01-15T12:00:00Z"));
		dbReset();
		clearAllTournamentDataCache();
		await dbInsertUsers(5);
		mockNotify.mockClear();
	});

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

	test("sends notification for tournament starting exactly 1 hour from now", async () => {
		const now = new Date();
		const oneHourFromNow = add(now, { hours: 1 });

		await createTestTournament({
			name: "Tournament 1 Hour Away",
			startTime: oneHourFromNow,
		});

		await NotifyCheckInStartRoutine.run();

		expect(mockNotify).toHaveBeenCalledTimes(1);
		expect(mockNotify).toHaveBeenCalledWith(
			expect.objectContaining({
				notification: expect.objectContaining({
					type: "TO_CHECK_IN_OPENED",
					meta: expect.objectContaining({
						tournamentName: "Tournament 1 Hour Away",
					}),
				}),
			}),
		);
	});

	test("does NOT send notification for tournament starting exactly now", async () => {
		const now = new Date();

		await createTestTournament({
			name: "Tournament Starting Now",
			startTime: now,
		});

		await NotifyCheckInStartRoutine.run();

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

	test("sends notification for tournament starting 30 minutes from now", async () => {
		const now = new Date();
		const thirtyMinutesFromNow = add(now, { minutes: 30 });

		await createTestTournament({
			name: "Tournament 30 Minutes Away",
			startTime: thirtyMinutesFromNow,
		});

		await NotifyCheckInStartRoutine.run();

		expect(mockNotify).toHaveBeenCalledTimes(1);
		expect(mockNotify).toHaveBeenCalledWith(
			expect.objectContaining({
				notification: expect.objectContaining({
					type: "TO_CHECK_IN_OPENED",
					meta: expect.objectContaining({
						tournamentName: "Tournament 30 Minutes Away",
					}),
				}),
			}),
		);
	});

	test("does NOT send notification for tournament starting more than 1 hour from now", async () => {
		const now = new Date();
		const oneAndHalfHoursFromNow = add(now, { hours: 1, minutes: 30 });

		await createTestTournament({
			name: "Tournament 1.5 Hours Away",
			startTime: oneAndHalfHoursFromNow,
		});

		await NotifyCheckInStartRoutine.run();

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

	test("sends notifications for multiple tournaments in the time window", async () => {
		const now = new Date();
		const thirtyMinutesFromNow = add(now, { minutes: 30 });
		const fortyFiveMinutesFromNow = add(now, { minutes: 45 });

		await createTestTournament({
			name: "Tournament A",
			startTime: thirtyMinutesFromNow,
			discordInviteCode: "test-discord-1",
		});

		await createTestTournament({
			name: "Tournament B",
			startTime: fortyFiveMinutesFromNow,
			authorId: 2,
			discordInviteCode: "test-discord-2",
		});

		await NotifyCheckInStartRoutine.run();

		expect(mockNotify).toHaveBeenCalledTimes(2);

		const tournamentNames = mockNotify.mock.calls.map(
			(call) => call[0].notification.meta.tournamentName,
		);
		expect(tournamentNames).toContain("Tournament A");
		expect(tournamentNames).toContain("Tournament B");
	});
});
