import { test, expect } from '@playwright/test';
import { execSync } from 'child_process';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const baseUrl = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:4173';
const urlMatch = baseUrl.match(/:(\d+)$/);
const PORT = urlMatch ? urlMatch[1] : '4173';
const ALPHA_URL = `http://ecole-alpha.classeo.local:${PORT}`;

const STUDENT_EMAIL = 'e2e-student-schedule@example.com';
const STUDENT_PASSWORD = 'StudentSchedule123';
const TEACHER_EMAIL = 'e2e-student-sched-teacher@example.com';
const TEACHER_PASSWORD = 'TeacherSchedule123';
const TENANT_ID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';

const projectRoot = join(__dirname, '../..');
const composeFile = join(projectRoot, 'compose.yaml');

function runSql(sql: string) {
	execSync(
		`docker compose -f "${composeFile}" exec -T php php bin/console dbal:run-sql "${sql}" 2>&1`,
		{ encoding: 'utf-8' }
	);
}

function clearCache() {
	try {
		execSync(
			`docker compose -f "${composeFile}" exec -T php php bin/console cache:pool:clear paginated_queries.cache 2>&1`,
			{ encoding: 'utf-8' }
		);
	} catch {
		// Cache pool may not exist
	}
}

function resolveDeterministicIds(): { schoolId: string; academicYearId: string } {
	const output = execSync(
		`docker compose -f "${composeFile}" exec -T php php -r '` +
			`require "/app/vendor/autoload.php"; ` +
			`$t="${TENANT_ID}"; $ns="6ba7b814-9dad-11d1-80b4-00c04fd430c8"; ` +
			`echo Ramsey\\Uuid\\Uuid::uuid5($ns,"school-$t")->toString()."\\n"; ` +
			`$m=(int)date("n"); $s=$m>=9?(int)date("Y"):(int)date("Y")-1; $e=$s+1; ` +
			`echo Ramsey\\Uuid\\Uuid::uuid5($ns,"$t:$s-$e")->toString();` +
			`' 2>&1`,
		{ encoding: 'utf-8' }
	).trim();
	const [schoolId, academicYearId] = output.split('\n');
	return { schoolId: schoolId!, academicYearId: academicYearId! };
}

/**
 * Returns ISO day of week (1=Monday ... 5=Friday) for the current day,
 * clamped to weekdays for schedule slot seeding.
 */
function currentWeekdayIso(): number {
	const jsDay = new Date().getDay(); // 0=Sun, 1=Mon...6=Sat
	if (jsDay === 0) return 5; // Sunday → use Friday
	if (jsDay === 6) return 5; // Saturday → use Friday
	return jsDay;
}

/**
 * Returns the number of day-back navigations needed to reach the seeded weekday.
 * 0 on weekdays, 1 on Saturday (→ Friday), 2 on Sunday (→ Friday).
 */
function daysBackToSeededWeekday(): number {
	const jsDay = new Date().getDay();
	if (jsDay === 6) return 1; // Saturday → go back 1 day to Friday
	if (jsDay === 0) return 2; // Sunday → go back 2 days to Friday
	return 0;
}

/**
 * Returns the French day name for the target seeded weekday.
 */
function seededDayName(): string {
	const jsDay = new Date().getDay();
	// Saturday → Friday, Sunday → Friday, else today
	const target = jsDay === 6 ? 5 : jsDay === 0 ? 5 : jsDay;
	return ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'][target]!;
}

/**
 * On weekends, navigate back to the weekday where schedule slots were seeded.
 * Must be called after the schedule page has loaded.
 *
 * Webkit needs time after page render for Svelte 5 event delegation to hydrate.
 * We retry clicking until the day title changes, with a timeout.
 */
async function navigateToSeededDay(page: import('@playwright/test').Page) {
	const back = daysBackToSeededWeekday();
	if (back === 0) return;

	const targetDay = seededDayName();
	const targetPattern = new RegExp(targetDay, 'i');
	const prevBtn = page.getByLabel('Précédent');
	await expect(prevBtn).toBeVisible({ timeout: 10000 });

	// Navigate one day at a time, waiting for each load to complete before clicking again
	const deadline = Date.now() + 20000;
	let navigated = false;
	while (Date.now() < deadline && !navigated) {
		await prevBtn.click();
		// Wait for the schedule API call to complete before checking/clicking again
		await page.waitForTimeout(1500);
		const title = await page.locator('.day-title').textContent();
		if (title && targetPattern.test(title)) {
			navigated = true;
		}
	}

	await expect(page.locator('.day-title').getByText(targetPattern)).toBeVisible({
		timeout: 5000
	});

	// Wait for any in-flight schedule loads to settle after reaching target day
	await page.waitForTimeout(2000);
}

async function loginAsStudent(page: import('@playwright/test').Page) {
	await page.goto(`${ALPHA_URL}/login`);
	await page.locator('#email').fill(STUDENT_EMAIL);
	await page.locator('#password').fill(STUDENT_PASSWORD);
	await Promise.all([
		page.waitForURL(/\/dashboard/, { timeout: 30000 }),
		page.getByRole('button', { name: /se connecter/i }).click()
	]);
}

test.describe('Student Schedule Consultation (Story 4.3)', () => {
	test.describe.configure({ mode: 'serial' });

	test.beforeAll(async () => {
		// Clear caches to prevent stale data from previous runs
		try {
			execSync(
				`docker compose -f "${composeFile}" exec -T php php bin/console cache:pool:clear cache.rate_limiter users.cache --env=dev 2>&1`,
				{ encoding: 'utf-8' }
			);
		} catch {
			// Cache pools may not exist
		}

		// Create student user
		execSync(
			`docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-user --tenant=ecole-alpha --email=${STUDENT_EMAIL} --password=${STUDENT_PASSWORD} --role=ROLE_ELEVE 2>&1`,
			{ encoding: 'utf-8' }
		);

		// Create teacher user
		execSync(
			`docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-user --tenant=ecole-alpha --email=${TEACHER_EMAIL} --password=${TEACHER_PASSWORD} --role=ROLE_PROF 2>&1`,
			{ encoding: 'utf-8' }
		);

		const { schoolId, academicYearId } = resolveDeterministicIds();

		// Ensure class exists
		try {
			runSql(
				`INSERT INTO school_classes (id, tenant_id, school_id, academic_year_id, name, level, status, created_at, updated_at) VALUES (gen_random_uuid(), '${TENANT_ID}', '${schoolId}', '${academicYearId}', 'E2E-StudentSched-6A', '6ème', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING`
			);
		} catch {
			// May already exist
		}

		// Ensure subject exists
		try {
			runSql(
				`INSERT INTO subjects (id, tenant_id, school_id, name, code, color, status, created_at, updated_at) VALUES (gen_random_uuid(), '${TENANT_ID}', '${schoolId}', 'E2E-StudentSched-Maths', 'E2ESTUMATH', '#3b82f6', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING`
			);
		} catch {
			// May already exist
		}

		try {
			runSql(
				`INSERT INTO subjects (id, tenant_id, school_id, name, code, color, status, created_at, updated_at) VALUES (gen_random_uuid(), '${TENANT_ID}', '${schoolId}', 'E2E-StudentSched-Français', 'E2ESTUFRA', '#ef4444', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING`
			);
		} catch {
			// May already exist
		}

		// Clean up schedule data for this tenant
		try {
			runSql(`DELETE FROM schedule_slots WHERE tenant_id = '${TENANT_ID}' AND class_id IN (SELECT id FROM school_classes WHERE name = 'E2E-StudentSched-6A' AND tenant_id = '${TENANT_ID}')`);
		} catch {
			// Table may not exist
		}

		// Clean up calendar entries to prevent holidays/vacations from blocking schedule resolution
		try {
			runSql(`DELETE FROM school_calendar_entries WHERE tenant_id = '${TENANT_ID}'`);
		} catch {
			// Table may not exist
		}

		// Assign student to class
		runSql(
			`INSERT INTO class_assignments (id, tenant_id, user_id, school_class_id, academic_year_id, assigned_at, created_at, updated_at) ` +
				`SELECT gen_random_uuid(), '${TENANT_ID}', u.id, c.id, '${academicYearId}', NOW(), NOW(), NOW() ` +
				`FROM users u, school_classes c ` +
				`WHERE u.email = '${STUDENT_EMAIL}' AND u.tenant_id = '${TENANT_ID}' ` +
				`AND c.name = 'E2E-StudentSched-6A' AND c.tenant_id = '${TENANT_ID}' ` +
				`ON CONFLICT DO NOTHING`
		);

		// Create schedule slots for the class on today's weekday
		const dayOfWeek = currentWeekdayIso();
		runSql(
			`INSERT INTO schedule_slots (id, tenant_id, class_id, subject_id, teacher_id, day_of_week, start_time, end_time, room, is_recurring, created_at, updated_at) ` +
				`SELECT gen_random_uuid(), '${TENANT_ID}', c.id, s.id, t.id, ${dayOfWeek}, '09:00', '10:00', 'A101', true, NOW(), NOW() ` +
				`FROM school_classes c, ` +
				`(SELECT id FROM subjects WHERE code = 'E2ESTUMATH' AND tenant_id = '${TENANT_ID}' LIMIT 1) s, ` +
				`(SELECT id FROM users WHERE email = '${TEACHER_EMAIL}' AND tenant_id = '${TENANT_ID}') t ` +
				`WHERE c.name = 'E2E-StudentSched-6A' AND c.tenant_id = '${TENANT_ID}'`
		);

		runSql(
			`INSERT INTO schedule_slots (id, tenant_id, class_id, subject_id, teacher_id, day_of_week, start_time, end_time, room, is_recurring, created_at, updated_at) ` +
				`SELECT gen_random_uuid(), '${TENANT_ID}', c.id, s.id, t.id, ${dayOfWeek}, '10:15', '11:15', 'B202', true, NOW(), NOW() ` +
				`FROM school_classes c, ` +
				`(SELECT id FROM subjects WHERE code = 'E2ESTUFRA' AND tenant_id = '${TENANT_ID}' LIMIT 1) s, ` +
				`(SELECT id FROM users WHERE email = '${TEACHER_EMAIL}' AND tenant_id = '${TENANT_ID}') t ` +
				`WHERE c.name = 'E2E-StudentSched-6A' AND c.tenant_id = '${TENANT_ID}'`
		);

		clearCache();
	});

	// ======================================================================
	// AC1: Day View
	// ======================================================================
	test.describe('AC1: Day View', () => {
		test('student can navigate to schedule page', async ({ page }) => {
			await loginAsStudent(page);
			await page.goto(`${ALPHA_URL}/dashboard/schedule`);

			await expect(
				page.getByRole('heading', { name: /mon emploi du temps/i })
			).toBeVisible({ timeout: 15000 });
		});

		test('day view is the default view', async ({ page }) => {
			await loginAsStudent(page);
			await page.goto(`${ALPHA_URL}/dashboard/schedule`);

			await expect(
				page.getByRole('heading', { name: /mon emploi du temps/i })
			).toBeVisible({ timeout: 15000 });

			// Day toggle should be active
			const dayButton = page.locator('.view-toggle button', { hasText: 'Jour' });
			await expect(dayButton).toHaveClass(/active/, { timeout: 5000 });
		});

		test('day view shows schedule slots for today', async ({ page }) => {
			await loginAsStudent(page);
			await page.goto(`${ALPHA_URL}/dashboard/schedule`);
			await expect(
				page.getByRole('heading', { name: /mon emploi du temps/i })
			).toBeVisible({ timeout: 15000 });
			await navigateToSeededDay(page);

			// Wait for slots to load
			const slots = page.locator('[data-testid="schedule-slot"]');
			await expect(slots.first()).toBeVisible({ timeout: 20000 });

			// Should see both slots
			await expect(slots).toHaveCount(2);

			// Verify slot content
			await expect(page.getByText('E2E-StudentSched-Maths')).toBeVisible();
			await expect(page.getByText('E2E-StudentSched-Français')).toBeVisible();
			await expect(page.getByText('A101')).toBeVisible();
			await expect(page.getByText('B202')).toBeVisible();
		});
	});

	// ======================================================================
	// AC2: Day Navigation
	// ======================================================================
	test.describe('AC2: Day Navigation', () => {
		test('navigating to a day with no courses shows empty message', async ({ page }) => {
			await loginAsStudent(page);
			await page.goto(`${ALPHA_URL}/dashboard/schedule`);

			await expect(
				page.getByRole('heading', { name: /mon emploi du temps/i })
			).toBeVisible({ timeout: 15000 });

			// On weekends, the current day already has no courses — verify directly
			const back = daysBackToSeededWeekday();
			if (back > 0) {
				await expect(page.getByText('Aucun cours ce jour')).toBeVisible({ timeout: 10000 });
				return;
			}

			// Wait for today's slots to fully load before navigating
			await expect(page.locator('[data-testid="schedule-slot"]').first()).toBeVisible({
				timeout: 15000
			});

			// Navigate forward enough days to reach a day with no seeded slots.
			// Slots are only seeded on today's weekday, so +1 day is guaranteed empty.
			await page.getByLabel('Suivant').click();

			// The day view should show the empty-state message
			await expect(page.getByText('Aucun cours ce jour')).toBeVisible({ timeout: 10000 });
		});

		test('can navigate to next day and back', async ({ page }) => {
			await loginAsStudent(page);
			await page.goto(`${ALPHA_URL}/dashboard/schedule`);
			await expect(
				page.getByRole('heading', { name: /mon emploi du temps/i })
			).toBeVisible({ timeout: 15000 });

			await navigateToSeededDay(page);

			// Wait for slots to load on the seeded day
			await expect(page.locator('[data-testid="schedule-slot"]').first()).toBeVisible({
				timeout: 15000
			});

			// Navigate to next day — wait for the load to settle before navigating back
			await page.getByLabel('Suivant').click();
			await page.waitForTimeout(1500);

			// Then navigate back
			await page.getByLabel('Précédent').click();

			// Slots should be visible again
			const slots = page.locator('[data-testid="schedule-slot"]');
			await expect(slots.first()).toBeVisible({ timeout: 20000 });
		});
	});

	// ======================================================================
	// AC3: Week View
	// ======================================================================
	test.describe('AC3: Week View', () => {
		test('can switch to week view and see grid', async ({ page }) => {
			await loginAsStudent(page);
			await page.goto(`${ALPHA_URL}/dashboard/schedule`);
			await navigateToSeededDay(page);

			// Wait for day view to load
			await expect(page.locator('[data-testid="schedule-slot"]').first()).toBeVisible({
				timeout: 15000
			});

			// Switch to week view
			const weekButton = page.locator('.view-toggle button', { hasText: 'Semaine' });
			await weekButton.click();

			// Week grid should show day headers (proves week view rendered)
			// Use exact match to avoid strict mode violation with mobile list labels ("Lun 2" etc.)
			await expect(page.getByText('Lun', { exact: true })).toBeVisible({ timeout: 15000 });
			await expect(page.getByText('Mar', { exact: true })).toBeVisible();
			await expect(page.getByText('Mer', { exact: true })).toBeVisible();
			await expect(page.getByText('Jeu', { exact: true })).toBeVisible();
			await expect(page.getByText('Ven', { exact: true })).toBeVisible();

			// Week slots should be visible (scope to desktop grid to avoid hidden mobile slots)
			const weekSlots = page.locator('.week-slot-desktop[data-testid="week-slot"]');
			await expect(weekSlots.first()).toBeVisible({ timeout: 15000 });
		});

		test('week view shows mobile list layout on small viewport', async ({ page }) => {
			// Resize to mobile
			await page.setViewportSize({ width: 375, height: 667 });
			await loginAsStudent(page);
			await page.goto(`${ALPHA_URL}/dashboard/schedule`);
			await navigateToSeededDay(page);

			// Wait for day view to load
			await expect(page.locator('[data-testid="schedule-slot"]').first()).toBeVisible({
				timeout: 15000
			});

			// Switch to week view
			const weekButton = page.locator('.view-toggle button', { hasText: 'Semaine' });
			await weekButton.click();

			// Mobile list should be visible, desktop grid should be hidden
			const weekList = page.locator('.week-list');
			const weekGrid = page.locator('.week-grid');
			await expect(weekList).toBeVisible({ timeout: 15000 });
			await expect(weekGrid).not.toBeVisible();

			// Should show day sections with slot count
			await expect(page.getByText(/\d+ cours/).first()).toBeVisible();

			// Week slots should be visible in mobile layout
			const weekSlots = page.locator('[data-testid="week-slot"]');
			await expect(weekSlots.first()).toBeVisible({ timeout: 15000 });
		});

		test('week view shows desktop grid on large viewport', async ({ page }) => {
			await loginAsStudent(page);
			await page.goto(`${ALPHA_URL}/dashboard/schedule`);
			await navigateToSeededDay(page);

			// Wait for day view to load
			await expect(page.locator('[data-testid="schedule-slot"]').first()).toBeVisible({
				timeout: 15000
			});

			// Switch to week view
			const weekButton = page.locator('.view-toggle button', { hasText: 'Semaine' });
			await weekButton.click();

			// Desktop grid should be visible, mobile list should be hidden
			const weekList = page.locator('.week-list');
			const weekGrid = page.locator('.week-grid');
			await expect(weekGrid).toBeVisible({ timeout: 15000 });
			await expect(weekList).not.toBeVisible();
		});

		test('can switch back to day view from week view', async ({ page }) => {
			await loginAsStudent(page);
			await page.goto(`${ALPHA_URL}/dashboard/schedule`);
			await navigateToSeededDay(page);

			// Wait for day view to load
			await expect(page.locator('[data-testid="schedule-slot"]').first()).toBeVisible({
				timeout: 15000
			});

			// Switch to week
			await page.locator('.view-toggle button', { hasText: 'Semaine' }).click();
			await expect(page.locator('.week-slot-desktop[data-testid="week-slot"]').first()).toBeVisible({
				timeout: 15000
			});

			// Switch back to day
			await page.locator('.view-toggle button', { hasText: 'Jour' }).click();

			// Day slots should be visible again (proves day view rendered)
			await expect(page.locator('[data-testid="schedule-slot"]').first()).toBeVisible({
				timeout: 15000
			});
		});
	});

	// ======================================================================
	// AC4: Slot Details
	// ======================================================================
	test.describe('AC4: Slot Details', () => {
		test('clicking a slot opens details modal', async ({ page }) => {
			await loginAsStudent(page);
			await page.goto(`${ALPHA_URL}/dashboard/schedule`);
			await navigateToSeededDay(page);

			// Wait for slots to load
			const firstSlot = page.locator('[data-testid="schedule-slot"]').first();
			await expect(firstSlot).toBeVisible({ timeout: 15000 });

			// Click the slot
			await firstSlot.click();

			// Modal should appear with course details
			const dialog = page.getByRole('dialog');
			await expect(dialog).toBeVisible({ timeout: 5000 });

			// Should show subject, teacher, room, time
			await expect(dialog.getByText('E2E-StudentSched-Maths')).toBeVisible();
			await expect(dialog.getByText('09:00 - 10:00')).toBeVisible();
			await expect(dialog.getByText('A101')).toBeVisible();
		});

		test('details modal closes with Escape key', async ({ page }) => {
			await loginAsStudent(page);
			await page.goto(`${ALPHA_URL}/dashboard/schedule`);
			await navigateToSeededDay(page);

			const firstSlot = page.locator('[data-testid="schedule-slot"]').first();
			await expect(firstSlot).toBeVisible({ timeout: 15000 });
			await firstSlot.click();

			const dialog = page.getByRole('dialog');
			await expect(dialog).toBeVisible({ timeout: 5000 });

			// Close with Escape
			await page.keyboard.press('Escape');
			await expect(dialog).not.toBeVisible({ timeout: 5000 });
		});

		test('details modal closes when clicking overlay', async ({ page }) => {
			await loginAsStudent(page);
			await page.goto(`${ALPHA_URL}/dashboard/schedule`);
			await navigateToSeededDay(page);

			const firstSlot = page.locator('[data-testid="schedule-slot"]').first();
			await expect(firstSlot).toBeVisible({ timeout: 15000 });
			await firstSlot.click();

			const dialog = page.getByRole('dialog');
			await expect(dialog).toBeVisible({ timeout: 5000 });

			// Close by clicking the overlay (outside the card)
			await page.locator('.overlay').click({ position: { x: 10, y: 10 } });
			await expect(dialog).not.toBeVisible({ timeout: 5000 });
		});
	});

	// ======================================================================
	// AC5: Offline Mode
	// ======================================================================
	test.describe('AC5: Offline Mode', () => {
		test('shows offline banner when network is lost', async ({ page, context }) => {
			await loginAsStudent(page);
			await page.goto(`${ALPHA_URL}/dashboard/schedule`);
			await navigateToSeededDay(page);

			// Wait for schedule to load (data is now cached by the browser)
			await expect(
				page.locator('[data-testid="schedule-slot"]').first()
			).toBeVisible({ timeout: 15000 });

			// Simulate going offline — triggers window 'offline' event
			await context.setOffline(true);

			// The offline banner should appear
			const offlineBanner = page.locator('.offline-banner[role="status"]');
			await expect(offlineBanner).toBeVisible({ timeout: 5000 });
			await expect(offlineBanner.getByText('Hors ligne')).toBeVisible();
			await expect(offlineBanner.getByText(/Dernière sync/)).toBeVisible();

			// Restore online
			await context.setOffline(false);

			// Banner should disappear
			await expect(offlineBanner).not.toBeVisible({ timeout: 5000 });
		});
	});

	// ======================================================================
	// Navigation link
	// ======================================================================
	test.describe('Navigation', () => {
		test('schedule link is visible in dashboard header', async ({ page }) => {
			await loginAsStudent(page);

			const navLink = page.locator('.desktop-nav a', { hasText: 'Mon EDT' });
			await expect(navLink).toBeVisible({ timeout: 10000 });
		});

		test('clicking schedule nav link navigates to schedule page', async ({ page }) => {
			await loginAsStudent(page);

			const navLink = page.locator('.desktop-nav a', { hasText: 'Mon EDT' });
			await expect(navLink).toBeVisible({ timeout: 10000 });
			await navLink.click();

			await expect(page).toHaveURL(/\/dashboard\/schedule/);
			await expect(
				page.getByRole('heading', { name: /mon emploi du temps/i })
			).toBeVisible({ timeout: 15000 });
		});
	});
});
