import { describe, test, expect, beforeEach, mock } from 'bun:test';
import { testDb as db } from '$lib/server/db/test';
import { dailyVerses, dailyCompletions } from '$lib/server/db/schema';
import { eq } from 'drizzle-orm';
import crypto from 'node:crypto';

describe('Timezone-aware daily verse system', () => {
	beforeEach(async () => {
		// Clean up test data before each test
		await db.delete(dailyVerses);
		await db.delete(dailyCompletions);
	});

	describe('Daily verse retrieval', () => {
		test('users in different timezones can see different verses at the same UTC moment', async () => {
			// Simulate: It's 2024-01-15 23:00 UTC
			// - Tokyo (UTC+9): 2024-01-16 08:00
			// - New York (UTC-5): 2024-01-15 18:00

			const tokyoDate = '2024-01-16';
			const newYorkDate = '2024-01-15';

			// Create verses for both dates
			const tokyoVerse = {
				id: crypto.randomUUID(),
				date: tokyoDate,
				bookId: 'GEN',
				verseText: 'Tokyo verse',
				reference: 'Genesis 1:1',
			};

			const newYorkVerse = {
				id: crypto.randomUUID(),
				date: newYorkDate,
				bookId: 'EXO',
				verseText: 'New York verse',
				reference: 'Exodus 1:1',
			};

			await db.insert(dailyVerses).values([tokyoVerse, newYorkVerse]);

			// Verify Tokyo user gets Jan 16 verse
			const tokyoResult = await db
				.select()
				.from(dailyVerses)
				.where(eq(dailyVerses.date, tokyoDate))
				.limit(1);

			expect(tokyoResult).toHaveLength(1);
			expect(tokyoResult[0].bookId).toBe('GEN');
			expect(tokyoResult[0].verseText).toBe('Tokyo verse');

			// Verify New York user gets Jan 15 verse
			const newYorkResult = await db
				.select()
				.from(dailyVerses)
				.where(eq(dailyVerses.date, newYorkDate))
				.limit(1);

			expect(newYorkResult).toHaveLength(1);
			expect(newYorkResult[0].bookId).toBe('EXO');
			expect(newYorkResult[0].verseText).toBe('New York verse');
		});

		test('verse dates are stored in YYYY-MM-DD format', async () => {
			const verse = {
				id: crypto.randomUUID(),
				date: '2024-01-15',
				bookId: 'GEN',
				verseText: 'Test verse',
				reference: 'Genesis 1:1',
			};

			await db.insert(dailyVerses).values(verse);

			const result = await db
				.select()
				.from(dailyVerses)
				.where(eq(dailyVerses.id, verse.id))
				.limit(1);

			expect(result[0].date).toMatch(/^\d{4}-\d{2}-\d{2}$/);
		});
	});

	describe('Completion tracking', () => {
		test('completions are stored with user local date', async () => {
			const userId = 'test-user-1';
			const localDate = '2024-01-16'; // User's local date

			const completion = {
				id: crypto.randomUUID(),
				anonymousId: userId,
				date: localDate,
				guessCount: 3,
				completedAt: new Date(),
			};

			await db.insert(dailyCompletions).values(completion);

			const result = await db
				.select()
				.from(dailyCompletions)
				.where(eq(dailyCompletions.anonymousId, userId))
				.limit(1);

			expect(result).toHaveLength(1);
			expect(result[0].date).toBe(localDate);
		});

		test('users in different timezones can complete different date verses simultaneously', async () => {
			const tokyoUser = 'tokyo-user';
			const newYorkUser = 'newyork-user';

			const completions = [
				{
					id: crypto.randomUUID(),
					anonymousId: tokyoUser,
					date: '2024-01-16', // Tokyo: Jan 16
					guessCount: 2,
					completedAt: new Date('2024-01-15T23:00:00Z'), // 23:00 UTC
				},
				{
					id: crypto.randomUUID(),
					anonymousId: newYorkUser,
					date: '2024-01-15', // New York: Jan 15
					guessCount: 4,
					completedAt: new Date('2024-01-15T23:00:00Z'), // 23:00 UTC
				},
			];

			await db.insert(dailyCompletions).values(completions);

			const tokyoResult = await db
				.select()
				.from(dailyCompletions)
				.where(eq(dailyCompletions.anonymousId, tokyoUser))
				.limit(1);

			const newYorkResult = await db
				.select()
				.from(dailyCompletions)
				.where(eq(dailyCompletions.anonymousId, newYorkUser))
				.limit(1);

			expect(tokyoResult[0].date).toBe('2024-01-16');
			expect(newYorkResult[0].date).toBe('2024-01-15');
		});
	});

	describe('Streak calculation', () => {
		test('consecutive days count as a streak', async () => {
			const userId = 'streak-user';

			const completions = [
				{
					id: crypto.randomUUID(),
					anonymousId: userId,
					date: '2024-01-13',
					guessCount: 2,
					completedAt: new Date('2024-01-13T12:00:00Z'),
				},
				{
					id: crypto.randomUUID(),
					anonymousId: userId,
					date: '2024-01-14',
					guessCount: 3,
					completedAt: new Date('2024-01-14T12:00:00Z'),
				},
				{
					id: crypto.randomUUID(),
					anonymousId: userId,
					date: '2024-01-15',
					guessCount: 1,
					completedAt: new Date('2024-01-15T12:00:00Z'),
				},
			];

			await db.insert(dailyCompletions).values(completions);

			const allCompletions = await db
				.select()
				.from(dailyCompletions)
				.where(eq(dailyCompletions.anonymousId, userId));

			const sortedDates = allCompletions.map((c) => c.date).sort();

			// Verify consecutive dates
			expect(sortedDates).toEqual(['2024-01-13', '2024-01-14', '2024-01-15']);

			// Calculate streak
			let streak = 1;
			for (let i = 1; i < sortedDates.length; i++) {
				const currentDate = new Date(sortedDates[i]);
				const prevDate = new Date(sortedDates[i - 1]);
				const daysDiff = Math.floor(
					(currentDate.getTime() - prevDate.getTime()) / (1000 * 60 * 60 * 24)
				);

				if (daysDiff === 1) {
					streak++;
				} else {
					break;
				}
			}

			expect(streak).toBe(3);
		});

		test('current streak is active if last completion was today', async () => {
			const userId = 'current-streak-user';
			const userToday = '2024-01-16';

			const completions = [
				{
					id: crypto.randomUUID(),
					anonymousId: userId,
					date: '2024-01-14',
					guessCount: 2,
					completedAt: new Date('2024-01-14T12:00:00Z'),
				},
				{
					id: crypto.randomUUID(),
					anonymousId: userId,
					date: '2024-01-15',
					guessCount: 3,
					completedAt: new Date('2024-01-15T12:00:00Z'),
				},
				{
					id: crypto.randomUUID(),
					anonymousId: userId,
					date: userToday,
					guessCount: 1,
					completedAt: new Date('2024-01-16T12:00:00Z'),
				},
			];

			await db.insert(dailyCompletions).values(completions);

			const allCompletions = await db
				.select()
				.from(dailyCompletions)
				.where(eq(dailyCompletions.anonymousId, userId));

			const sortedDates = allCompletions.map((c) => c.date).sort();
			const lastPlayedDate = sortedDates[sortedDates.length - 1];

			const yesterdayDate = new Date(userToday);
			yesterdayDate.setDate(yesterdayDate.getDate() - 1);
			const yesterday = yesterdayDate.toISOString().split('T')[0];

			const isStreakActive = lastPlayedDate === userToday || lastPlayedDate === yesterday;

			expect(isStreakActive).toBe(true);
			expect(lastPlayedDate).toBe(userToday);
		});

		test('current streak is active if last completion was yesterday', async () => {
			const userId = 'yesterday-streak-user';
			const userToday = '2024-01-16';

			const yesterdayDate = new Date(userToday);
			yesterdayDate.setDate(yesterdayDate.getDate() - 1);
			const yesterday = yesterdayDate.toISOString().split('T')[0];

			const completions = [
				{
					id: crypto.randomUUID(),
					anonymousId: userId,
					date: '2024-01-13',
					guessCount: 2,
					completedAt: new Date('2024-01-13T12:00:00Z'),
				},
				{
					id: crypto.randomUUID(),
					anonymousId: userId,
					date: '2024-01-14',
					guessCount: 3,
					completedAt: new Date('2024-01-14T12:00:00Z'),
				},
				{
					id: crypto.randomUUID(),
					anonymousId: userId,
					date: yesterday,
					guessCount: 1,
					completedAt: new Date(yesterday + 'T12:00:00Z'),
				},
			];

			await db.insert(dailyCompletions).values(completions);

			const allCompletions = await db
				.select()
				.from(dailyCompletions)
				.where(eq(dailyCompletions.anonymousId, userId));

			const sortedDates = allCompletions.map((c) => c.date).sort();
			const lastPlayedDate = sortedDates[sortedDates.length - 1];

			const isStreakActive = lastPlayedDate === userToday || lastPlayedDate === yesterday;

			expect(isStreakActive).toBe(true);
			expect(lastPlayedDate).toBe(yesterday);
		});

		test('current streak is not active if last completion was 2+ days ago', async () => {
			const userId = 'broken-streak-user';
			const userToday = '2024-01-16';

			const completions = [
				{
					id: crypto.randomUUID(),
					anonymousId: userId,
					date: '2024-01-13',
					guessCount: 2,
					completedAt: new Date('2024-01-13T12:00:00Z'),
				},
				{
					id: crypto.randomUUID(),
					anonymousId: userId,
					date: '2024-01-14',
					guessCount: 3,
					completedAt: new Date('2024-01-14T12:00:00Z'),
				},
			];

			await db.insert(dailyCompletions).values(completions);

			const allCompletions = await db
				.select()
				.from(dailyCompletions)
				.where(eq(dailyCompletions.anonymousId, userId));

			const sortedDates = allCompletions.map((c) => c.date).sort();
			const lastPlayedDate = sortedDates[sortedDates.length - 1];

			const yesterdayDate = new Date(userToday);
			yesterdayDate.setDate(yesterdayDate.getDate() - 1);
			const yesterday = yesterdayDate.toISOString().split('T')[0];

			const isStreakActive = lastPlayedDate === userToday || lastPlayedDate === yesterday;

			expect(isStreakActive).toBe(false);
			expect(lastPlayedDate).toBe('2024-01-14'); // 2 days ago
		});

		test('gap in dates breaks the streak', async () => {
			const userId = 'gap-user';

			const completions = [
				{
					id: crypto.randomUUID(),
					anonymousId: userId,
					date: '2024-01-10',
					guessCount: 2,
					completedAt: new Date('2024-01-10T12:00:00Z'),
				},
				{
					id: crypto.randomUUID(),
					anonymousId: userId,
					date: '2024-01-11',
					guessCount: 3,
					completedAt: new Date('2024-01-11T12:00:00Z'),
				},
				// Gap here (no 01-12)
				{
					id: crypto.randomUUID(),
					anonymousId: userId,
					date: '2024-01-13',
					guessCount: 1,
					completedAt: new Date('2024-01-13T12:00:00Z'),
				},
				{
					id: crypto.randomUUID(),
					anonymousId: userId,
					date: '2024-01-14',
					guessCount: 2,
					completedAt: new Date('2024-01-14T12:00:00Z'),
				},
			];

			await db.insert(dailyCompletions).values(completions);

			const allCompletions = await db
				.select()
				.from(dailyCompletions)
				.where(eq(dailyCompletions.anonymousId, userId));

			const sortedDates = allCompletions.map((c) => c.date).sort();

			// Calculate best streak (should be 2, not 4)
			let bestStreak = 1;
			let tempStreak = 1;

			for (let i = 1; i < sortedDates.length; i++) {
				const currentDate = new Date(sortedDates[i]);
				const prevDate = new Date(sortedDates[i - 1]);
				const daysDiff = Math.floor(
					(currentDate.getTime() - prevDate.getTime()) / (1000 * 60 * 60 * 24)
				);

				if (daysDiff === 1) {
					tempStreak++;
				} else {
					bestStreak = Math.max(bestStreak, tempStreak);
					tempStreak = 1;
				}
			}
			bestStreak = Math.max(bestStreak, tempStreak);

			expect(bestStreak).toBe(2); // Longest streak is Jan 13-14 or Jan 10-11
		});
	});

	describe('Date validation', () => {
		test('date must be in YYYY-MM-DD format', () => {
			const validDates = ['2024-01-15', '2023-12-31', '2024-02-29'];

			validDates.forEach((date) => {
				expect(date).toMatch(/^\d{4}-\d{2}-\d{2}$/);
			});
		});

		test('invalid date formats are rejected', () => {
			const invalidDates = [
				'2024/01/15', // Wrong separator
				'01-15-2024', // Wrong order
				'2024-1-15', // Missing leading zero
				'2024-01-15T12:00:00Z', // Includes time
			];

			invalidDates.forEach((date) => {
				if (date.includes('T')) {
					expect(date).not.toMatch(/^\d{4}-\d{2}-\d{2}$/);
				} else {
					expect(date).not.toMatch(/^\d{4}-\d{2}-\d{2}$/);
				}
			});
		});
	});

	describe('Edge cases', () => {
		test('crossing year boundary maintains streak', async () => {
			const userId = 'year-boundary-user';

			const completions = [
				{
					id: crypto.randomUUID(),
					anonymousId: userId,
					date: '2023-12-30',
					guessCount: 2,
					completedAt: new Date('2023-12-30T12:00:00Z'),
				},
				{
					id: crypto.randomUUID(),
					anonymousId: userId,
					date: '2023-12-31',
					guessCount: 3,
					completedAt: new Date('2023-12-31T12:00:00Z'),
				},
				{
					id: crypto.randomUUID(),
					anonymousId: userId,
					date: '2024-01-01',
					guessCount: 1,
					completedAt: new Date('2024-01-01T12:00:00Z'),
				},
			];

			await db.insert(dailyCompletions).values(completions);

			const allCompletions = await db
				.select()
				.from(dailyCompletions)
				.where(eq(dailyCompletions.anonymousId, userId));

			const sortedDates = allCompletions.map((c) => c.date).sort();

			let streak = 1;
			for (let i = 1; i < sortedDates.length; i++) {
				const currentDate = new Date(sortedDates[i]);
				const prevDate = new Date(sortedDates[i - 1]);
				const daysDiff = Math.floor(
					(currentDate.getTime() - prevDate.getTime()) / (1000 * 60 * 60 * 24)
				);

				if (daysDiff === 1) {
					streak++;
				}
			}

			expect(streak).toBe(3);
		});

		// Note: Duplicate prevention is handled by the API endpoint, not at the DB level in these tests
		// See /api/submit-completion for the unique constraint enforcement
	});
});

// ---------------------------------------------------------------------------
// Streak walk-back logic — mirrors /api/streak/+server.ts
// UTC is NEVER used for date comparison; all walk-back is pure string arithmetic.
// ---------------------------------------------------------------------------

function prevDay(dateStr: string): string {
	const d = new Date(dateStr + 'T00:00:00Z');
	d.setUTCDate(d.getUTCDate() - 1);
	return d.toISOString().slice(0, 10);
}

function calcStreak(completedDates: Set<string>, localDate: string): number {
	let streak = 0;
	let cursor = localDate;
	while (completedDates.has(cursor)) {
		streak++;
		cursor = prevDay(cursor);
	}
	return streak < 2 ? 0 : streak;
}

describe('Streak walk-back — local time always used, UTC never', () => {
	test('prevDay uses UTC arithmetic, never server local time', () => {
		// Regardless of server timezone, prevDay("2024-03-10") must always be "2024-03-09"
		// (including across DST boundaries where local midnight ≠ UTC midnight)
		expect(prevDay('2024-03-10')).toBe('2024-03-09'); // DST spring-forward in US
		expect(prevDay('2024-11-03')).toBe('2024-11-02'); // DST fall-back in US
		expect(prevDay('2024-01-01')).toBe('2023-12-31'); // year boundary
		expect(prevDay('2024-03-01')).toBe('2024-02-29'); // leap year
		expect(prevDay('2023-03-01')).toBe('2023-02-28'); // non-leap year
	});

	test('UTC+9 user: plays at 01:00 local (still previous UTC date) — streak uses local date', () => {
		// Scenario: It is 2024-01-16 01:00 in Tokyo (UTC+9) = 2024-01-15 16:00 UTC.
		// The verse served is for 2024-01-15 (UTC), but the user's LOCAL date is 2024-01-16.
		// Completions are stored as the user's local date (as returned by dailyVerse.date
		// which is set from the client's new Date().toLocaleDateString("en-CA")).
		// The streak walk-back must use local dates, not UTC dates.

		// Four consecutive local dates for a Tokyo user
		const completedLocalDates = new Set(['2024-01-13', '2024-01-14', '2024-01-15', '2024-01-16']);

		// If we (incorrectly) walked back from the UTC date "2024-01-15" instead of
		// the local date "2024-01-16", we would miss the most recent completion.
		const wrongStreakIfUTC = calcStreak(completedLocalDates, '2024-01-15');
		const correctStreakWithLocalDate = calcStreak(completedLocalDates, '2024-01-16');

		// UTC walk-back misses the local "2024-01-16" entry → only finds 3 consecutive days
		// (but actually it finds 2024-01-15, 2024-01-14, 2024-01-13 = 3, returned as 3)
		// The point is it does NOT include 2024-01-16 which is "today" for the user.
		expect(wrongStreakIfUTC).toBe(3); // stale — missing today's local entry

		// Local walk-back correctly finds all four entries
		expect(correctStreakWithLocalDate).toBe(4);
	});

	test('UTC-8 user: plays at 23:00 local (next UTC date) — streak uses local date', () => {
		// Scenario: It is 2024-01-15 23:00 in Los Angeles (UTC-8) = 2024-01-16 07:00 UTC.
		// The verse served is for 2024-01-16 (UTC), but the user's LOCAL date is still 2024-01-15.
		// Completion is stored as local date "2024-01-15".
		// Walk-back from "2024-01-15" (local) must find it; walk-back from "2024-01-16" (UTC)
		// would NOT find it (the entry is stored as "2024-01-15").

		const completedLocalDates = new Set(['2024-01-12', '2024-01-13', '2024-01-14', '2024-01-15']);

		// If we (incorrectly) walked back from the UTC date "2024-01-16" instead of
		// the local date "2024-01-15", "2024-01-16" is not in the set → streak = 0.
		const wrongStreakIfUTC = calcStreak(completedLocalDates, '2024-01-16');
		const correctStreakWithLocalDate = calcStreak(completedLocalDates, '2024-01-15');

		expect(wrongStreakIfUTC).toBe(0); // broken — UTC date not in DB
		expect(correctStreakWithLocalDate).toBe(4);
	});

	test('UTC+13 user (Samoa): local date is two days ahead of UTC-11', () => {
		// Extreme case: UTC+13 user on 2024-01-16 local = 2024-01-15 UTC.
		const completedLocalDates = new Set(['2024-01-14', '2024-01-15', '2024-01-16']);

		expect(calcStreak(completedLocalDates, '2024-01-14')).toBe(0); // only 1 day (< 2)
		expect(calcStreak(completedLocalDates, '2024-01-16')).toBe(3); // correct local streak
		expect(calcStreak(completedLocalDates, '2024-01-15')).toBe(0); // UTC date misses Jan 16
	});

	test('streak is 0 when today is missing even if yesterday exists', () => {
		// User missed today — streak must reset regardless of timezone
		const completedLocalDates = new Set(['2024-01-12', '2024-01-13', '2024-01-14']);
		// "Today" is 2024-01-16 — they missed the 15th and 16th
		expect(calcStreak(completedLocalDates, '2024-01-16')).toBe(0);
	});

	test('streak suppressed below 2 — single day returns 0', () => {
		const completedLocalDates = new Set(['2024-01-15']);
		expect(calcStreak(completedLocalDates, '2024-01-15')).toBe(0);
	});

	test('DB entries with local date store correctly alongside UTC completion timestamp', async () => {
		// Verify DB round-trip: storing completions with local dates (not UTC)
		// ensures streak calculation with local dates always works.
		const userId = 'tz-test-user-' + crypto.randomUUID();

		// UTC+9 scenario: played at 01:00 local on Jan 16 (= UTC Jan 15)
		// Local date is Jan 16, stored as "2024-01-16" in the DB.
		const completions = [
			{
				id: crypto.randomUUID(),
				anonymousId: userId,
				date: '2024-01-14', // local date
				guessCount: 2,
				completedAt: new Date('2024-01-13T15:00:00Z'), // UTC
			},
			{
				id: crypto.randomUUID(),
				anonymousId: userId,
				date: '2024-01-15', // local date
				guessCount: 3,
				completedAt: new Date('2024-01-14T15:00:00Z'), // UTC
			},
			{
				id: crypto.randomUUID(),
				anonymousId: userId,
				date: '2024-01-16', // local date — UTC equivalent is 2024-01-15
				guessCount: 1,
				completedAt: new Date('2024-01-15T16:00:00Z'), // 01:00 Tokyo time
			},
		];

		await db.insert(dailyCompletions).values(completions);

		const rows = await db
			.select({ date: dailyCompletions.date })
			.from(dailyCompletions)
			.where(eq(dailyCompletions.anonymousId, userId));

		const storedDates = new Set(rows.map((r) => r.date));

		// Walk-back from LOCAL date "2024-01-16" finds all three entries
		expect(calcStreak(storedDates, '2024-01-16')).toBe(3);

		// Walk-back from UTC date "2024-01-15" misses the "2024-01-16" local entry
		// (demonstrates the bug that was fixed: UTC walk-back gives wrong result)
		expect(calcStreak(storedDates, '2024-01-15')).toBe(0);

		await db.delete(dailyCompletions).where(eq(dailyCompletions.anonymousId, userId));
	});
});
