import { describe, it, expect, beforeEach, afterEach } from "bun:test";
import { testDb as db } from '../src/lib/server/db/test';
import { user, session, dailyCompletions } from '../src/lib/server/db/schema';
import * as auth from '../src/lib/server/auth.test';
import { eq, inArray } from 'drizzle-orm';
import crypto from 'node:crypto';

// Test helper functions
function generateTestUUID() {
	return crypto.randomUUID();
}

async function createTestUser(anonymousId: string, email: string, password: string = 'testpass123') {
	const passwordHash = await auth.hashPassword(password);
	const testUser = await auth.createUser(anonymousId, email, passwordHash, 'Test', 'User');
	return testUser;
}

async function createTestCompletion(anonymousId: string, date: string, guessCount: number, completedAt: Date) {
	const completion = {
		id: generateTestUUID(),
		anonymousId,
		date,
		guessCount,
		completedAt
	};
	await db.insert(dailyCompletions).values(completion);
	return completion;
}

async function clearTestData() {
	// Clear test data in reverse dependency order
	await db.delete(session);
	await db.delete(dailyCompletions);
	await db.delete(user);
}

describe('Signin Stats Migration', () => {
	beforeEach(async () => {
		await clearTestData();
	});

	afterEach(async () => {
		await clearTestData();
	});

	it('should migrate stats from local anonymous ID to user ID on signin', async () => {
		// Setup: Create user with device 1 anonymous ID
		const device1AnonymousId = generateTestUUID();
		const device2AnonymousId = generateTestUUID();
		const email = 'test@example.com';
		
		const testUser = await createTestUser(device1AnonymousId, email);
		
		// Add some completions for device 1 (user's original device)
		await createTestCompletion(device1AnonymousId, '2024-01-01', 3, new Date('2024-01-01T08:00:00Z'));
		await createTestCompletion(device1AnonymousId, '2024-01-02', 5, new Date('2024-01-02T09:00:00Z'));
		
		// Add some completions for device 2 (before signin)
		await createTestCompletion(device2AnonymousId, '2024-01-03', 2, new Date('2024-01-03T10:00:00Z'));
		await createTestCompletion(device2AnonymousId, '2024-01-04', 4, new Date('2024-01-04T11:00:00Z'));
		
		// Verify initial state
		const initialDevice1Stats = await db
			.select()
			.from(dailyCompletions)
			.where(eq(dailyCompletions.anonymousId, device1AnonymousId));
		const initialDevice2Stats = await db
			.select()
			.from(dailyCompletions)
			.where(eq(dailyCompletions.anonymousId, device2AnonymousId));
			
		expect(initialDevice1Stats).toHaveLength(2);
		expect(initialDevice2Stats).toHaveLength(2);
		
		// Simulate signin action - this is what happens in signin/+page.server.ts
		const user = await auth.getUserByEmail(email);
		expect(user).toBeTruthy();
		
		// Migrate stats (simulating the signin logic)
		if (device2AnonymousId && device2AnonymousId !== user!.id) {
			// Update all daily completions from device2 anonymous ID to user's ID
			await db
				.update(dailyCompletions)
				.set({ anonymousId: user!.id })
				.where(eq(dailyCompletions.anonymousId, device2AnonymousId));
		}
		
		// Verify migration worked
		const finalUserStats = await db
			.select()
			.from(dailyCompletions)
			.where(eq(dailyCompletions.anonymousId, user!.id));
		const remainingDevice2Stats = await db
			.select()
			.from(dailyCompletions)
			.where(eq(dailyCompletions.anonymousId, device2AnonymousId));
			
		expect(finalUserStats).toHaveLength(4); // All 4 completions now under user ID
		expect(remainingDevice2Stats).toHaveLength(0); // No more completions under device2 ID
		
		// Verify the actual data is correct
		const dates = finalUserStats.map(c => c.date).sort();
		expect(dates).toEqual(['2024-01-01', '2024-01-02', '2024-01-03', '2024-01-04']);
	});

	it('should deduplicate entries for same date keeping earliest completion', async () => {
		// Setup: User played same day on both devices
		const device1AnonymousId = generateTestUUID();
		const device2AnonymousId = generateTestUUID();
		const email = 'test@example.com';
		
		const testUser = await createTestUser(device1AnonymousId, email);
		
		// Both devices played on same date - device1 played earlier and better
		const date = '2024-01-01';
		const earlierTime = new Date('2024-01-01T08:00:00Z');
		const laterTime = new Date('2024-01-01T14:00:00Z');
		
		await createTestCompletion(device1AnonymousId, date, 3, earlierTime); // Better score, earlier
		await createTestCompletion(device2AnonymousId, date, 5, laterTime);   // Worse score, later
		
		// Also add unique dates to ensure they're preserved
		await createTestCompletion(device1AnonymousId, '2024-01-02', 4, new Date('2024-01-02T09:00:00Z'));
		await createTestCompletion(device2AnonymousId, '2024-01-03', 2, new Date('2024-01-03T10:00:00Z'));
		
		// Migrate stats
		const user = await auth.getUserByEmail(email);
		await db
			.update(dailyCompletions)
			.set({ anonymousId: user!.id })
			.where(eq(dailyCompletions.anonymousId, device2AnonymousId));
		
		// Implement deduplication logic (from signin server action)
		const allUserCompletions = await db
			.select()
			.from(dailyCompletions)
			.where(eq(dailyCompletions.anonymousId, user!.id));

		// Group by date to find duplicates
		const dateGroups = new Map<string, typeof allUserCompletions>();
		for (const completion of allUserCompletions) {
			const date = completion.date;
			if (!dateGroups.has(date)) {
				dateGroups.set(date, []);
			}
			dateGroups.get(date)!.push(completion);
		}

		// Process dates with duplicates
		const duplicateIds: string[] = [];
		for (const [date, completions] of dateGroups) {
			if (completions.length > 1) {
				// Sort by completedAt timestamp (earliest first)
				completions.sort((a, b) => a.completedAt.getTime() - b.completedAt.getTime());
				
				// Keep the first (earliest), mark the rest for deletion
				const toDelete = completions.slice(1);
				duplicateIds.push(...toDelete.map(c => c.id));
			}
		}

		// Delete duplicate entries
		if (duplicateIds.length > 0) {
			await db
				.delete(dailyCompletions)
				.where(inArray(dailyCompletions.id, duplicateIds));
		}
		
		// Verify deduplication worked correctly
		const finalStats = await db
			.select()
			.from(dailyCompletions)
			.where(eq(dailyCompletions.anonymousId, user!.id));
			
		expect(finalStats).toHaveLength(3); // One duplicate removed
		
		// Verify the correct entry was kept for the duplicate date
		const duplicateDateEntry = finalStats.find(c => c.date === date);
		expect(duplicateDateEntry).toBeTruthy();
		expect(duplicateDateEntry!.guessCount).toBe(3); // Better score kept
		expect(duplicateDateEntry!.completedAt.getTime()).toBe(earlierTime.getTime()); // Earlier time kept
		
		// Verify unique dates are preserved
		const allDates = finalStats.map(c => c.date).sort();
		expect(allDates).toEqual(['2024-01-01', '2024-01-02', '2024-01-03']);
	});

	it('should handle no migration when anonymous ID matches user ID', async () => {
		// Setup: User signing in from same device they signed up on
		const anonymousId = generateTestUUID();
		const email = 'test@example.com';
		
		const testUser = await createTestUser(anonymousId, email);
		
		// Add some completions
		await createTestCompletion(anonymousId, '2024-01-01', 3, new Date('2024-01-01T08:00:00Z'));
		await createTestCompletion(anonymousId, '2024-01-02', 5, new Date('2024-01-02T09:00:00Z'));
		
		// Verify initial state
		const initialStats = await db
			.select()
			.from(dailyCompletions)
			.where(eq(dailyCompletions.anonymousId, anonymousId));
		expect(initialStats).toHaveLength(2);
		
		// Simulate signin with same anonymous ID (no migration needed)
		const user = await auth.getUserByEmail(email);
		
		// Migration logic should skip when IDs match
		const shouldMigrate = anonymousId && anonymousId !== user!.id;
		expect(shouldMigrate).toBe(false);
		
		// Verify no changes
		const finalStats = await db
			.select()
			.from(dailyCompletions)
			.where(eq(dailyCompletions.anonymousId, anonymousId));
		expect(finalStats).toHaveLength(2);
		expect(finalStats[0].anonymousId).toBe(anonymousId);
	});

	it('should handle multiple duplicates for same date correctly', async () => {
		// Edge case: User played same date on 3+ devices
		const device1AnonymousId = generateTestUUID();
		const device2AnonymousId = generateTestUUID();
		const device3AnonymousId = generateTestUUID();
		const email = 'test@example.com';
		
		const testUser = await createTestUser(device1AnonymousId, email);
		
		const date = '2024-01-01';
		// Three completions on same date at different times
		await createTestCompletion(device1AnonymousId, date, 4, new Date('2024-01-01T08:00:00Z')); // Earliest
		await createTestCompletion(device2AnonymousId, date, 2, new Date('2024-01-01T14:00:00Z')); // Middle
		await createTestCompletion(device3AnonymousId, date, 6, new Date('2024-01-01T20:00:00Z')); // Latest
		
		// Migrate all to user ID
		const user = await auth.getUserByEmail(email);
		await db
			.update(dailyCompletions)
			.set({ anonymousId: user!.id })
			.where(eq(dailyCompletions.anonymousId, device2AnonymousId));
		await db
			.update(dailyCompletions)
			.set({ anonymousId: user!.id })
			.where(eq(dailyCompletions.anonymousId, device3AnonymousId));
		
		// Implement deduplication
		const allUserCompletions = await db
			.select()
			.from(dailyCompletions)
			.where(eq(dailyCompletions.anonymousId, user!.id));

		const dateGroups = new Map<string, typeof allUserCompletions>();
		for (const completion of allUserCompletions) {
			if (!dateGroups.has(completion.date)) {
				dateGroups.set(completion.date, []);
			}
			dateGroups.get(completion.date)!.push(completion);
		}

		const duplicateIds: string[] = [];
		for (const [_, completions] of dateGroups) {
			if (completions.length > 1) {
				completions.sort((a, b) => a.completedAt.getTime() - b.completedAt.getTime());
				const toDelete = completions.slice(1);
				duplicateIds.push(...toDelete.map(c => c.id));
			}
		}

		// Delete duplicates
		for (const id of duplicateIds) {
			await db.delete(dailyCompletions).where(eq(dailyCompletions.id, id));
		}
		
		// Verify only earliest kept
		const finalStats = await db
			.select()
			.from(dailyCompletions)
			.where(eq(dailyCompletions.anonymousId, user!.id));
			
		expect(finalStats).toHaveLength(1); // 2 duplicates removed
		expect(finalStats[0].guessCount).toBe(4); // First device's score
		expect(finalStats[0].completedAt.getTime()).toBe(new Date('2024-01-01T08:00:00Z').getTime());
	});
});