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 ADMIN_EMAIL = 'e2e-schedule-admin@example.com';
const ADMIN_PASSWORD = 'ScheduleTest123';
const TEACHER_EMAIL = 'e2e-schedule-teacher@example.com';
const TEACHER_PASSWORD = 'ScheduleTeacher123';
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}"; ` +
			`$dns="6ba7b810-9dad-11d1-80b4-00c04fd430c8"; ` +
			`$ay="6ba7b814-9dad-11d1-80b4-00c04fd430c8"; ` +
			`echo Ramsey\\Uuid\\Uuid::uuid5($dns,"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($ay,"$t:$s-$e")->toString();` +
			`' 2>&1`,
		{ encoding: 'utf-8' }
	).trim();
	const [schoolId, academicYearId] = output.split('\n');
	return { schoolId: schoolId!, academicYearId: academicYearId! };
}

function cleanupScheduleData() {
	try {
		runSql(`DELETE FROM schedule_slots WHERE tenant_id = '${TENANT_ID}'`);
	} catch {
		// Table may not exist yet
	}
}

function seedTeacherAssignments() {
	const { academicYearId } = resolveDeterministicIds();
	try {
		// Assign test teacher to ALL classes × ALL subjects so any dropdown combo is valid
		runSql(
			`INSERT INTO teacher_assignments (id, tenant_id, teacher_id, school_class_id, subject_id, academic_year_id, status, start_date, created_at, updated_at) ` +
				`SELECT gen_random_uuid(), '${TENANT_ID}', u.id, c.id, s.id, '${academicYearId}', 'active', NOW(), NOW(), NOW() ` +
				`FROM users u CROSS JOIN school_classes c CROSS JOIN subjects s ` +
				`WHERE u.email = '${TEACHER_EMAIL}' AND u.tenant_id = '${TENANT_ID}' ` +
				`AND c.tenant_id = '${TENANT_ID}' ` +
				`AND s.tenant_id = '${TENANT_ID}' ` +
				`ON CONFLICT DO NOTHING`
		);
	} catch {
		// Table may not exist
	}
}

function cleanupCalendarEntries() {
	try {
		runSql(`DELETE FROM school_calendar_entries WHERE tenant_id = '${TENANT_ID}'`);
	} catch {
		// Table may not exist
	}
}

function seedBlockedDate(date: string, label: string, type: string) {
	const { academicYearId } = resolveDeterministicIds();
	runSql(
		`INSERT INTO school_calendar_entries (id, tenant_id, academic_year_id, entry_type, start_date, end_date, label, created_at) ` +
			`VALUES (gen_random_uuid(), '${TENANT_ID}', '${academicYearId}', '${type}', '${date}', '${date}', '${label}', NOW()) ` +
			`ON CONFLICT DO NOTHING`
	);
}

function getWeekdayInCurrentWeek(isoDay: number): string {
	const now = new Date();
	const monday = new Date(now);
	monday.setDate(now.getDate() - ((now.getDay() + 6) % 7));
	const target = new Date(monday);
	target.setDate(monday.getDate() + (isoDay - 1));
	const y = target.getFullYear();
	const m = String(target.getMonth() + 1).padStart(2, '0');
	const d = String(target.getDate()).padStart(2, '0');
	return `${y}-${m}-${d}`;
}

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

async function waitForScheduleReady(page: import('@playwright/test').Page) {
	await expect(page.getByRole('heading', { name: /emploi du temps/i })).toBeVisible({
		timeout: 15000
	});
	// Wait for either the grid or the empty state to appear
	await expect(page.locator('.schedule-grid, .empty-state')).toBeVisible({
		timeout: 15000
	});
}

async function chooseScopeAndEdit(page: import('@playwright/test').Page) {
	// Scope modal appears - choose "this occurrence"
	const scopeModal = page.getByRole('dialog');
	await expect(scopeModal).toBeVisible({ timeout: 10000 });
	await scopeModal.getByText('Cette occurrence uniquement').click();
}

async function fillSlotForm(
	dialog: import('@playwright/test').Locator,
	options: {
		className?: string;
		dayValue?: string;
		startTime?: string;
		endTime?: string;
		room?: string;
	} = {}
) {
	const { className, dayValue = '1', startTime = '09:00', endTime = '10:00', room } = options;

	if (className) {
		await dialog.locator('#slot-class').selectOption({ label: className });
	}
	// Wait for assignments to load, then select subject first (filters teachers)
	const subjectOptions = dialog.locator('#slot-subject option:not([value=""])');
	await expect(subjectOptions.first()).toBeAttached({ timeout: 10000 });
	await dialog.locator('#slot-subject').selectOption({ index: 1 });
	// After subject selection, wait for teacher dropdown to be filtered
	const teacherOptions = dialog.locator('#slot-teacher option:not([value=""])');
	await expect(teacherOptions).toHaveCount(1, { timeout: 10000 });
	await dialog.locator('#slot-teacher').selectOption({ index: 1 });
	await dialog.locator('#slot-day').selectOption(dayValue);
	await dialog.locator('#slot-start').fill(startTime);
	await dialog.locator('#slot-end').fill(endTime);
	if (room) {
		await dialog.locator('#slot-room').fill(room);
	}
}

test.describe('Schedule Management - Modification & Conflicts & Calendar (Story 4.1)', () => {
	// Tests share database state (same tenant, users, slots) so they must run sequentially
	test.describe.configure({ mode: 'serial' });

	test.beforeAll(async () => {
		// Create admin user
		execSync(
			`docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-user --tenant=ecole-alpha --email=${ADMIN_EMAIL} --password=${ADMIN_PASSWORD} --role=ROLE_ADMIN 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();

		// Clean up stale test data (e.g. from previous runs with wrong school_id)
		try {
			runSql(`DELETE FROM schedule_slots WHERE class_id IN (SELECT id FROM school_classes WHERE name IN ('E2E-Schedule-6A','E2E-Schedule-5A') AND tenant_id = '${TENANT_ID}')`);
			runSql(`DELETE FROM teacher_assignments WHERE school_class_id IN (SELECT id FROM school_classes WHERE name IN ('E2E-Schedule-6A','E2E-Schedule-5A') AND tenant_id = '${TENANT_ID}')`);
			runSql(`DELETE FROM school_classes WHERE name IN ('E2E-Schedule-6A','E2E-Schedule-5A') AND tenant_id = '${TENANT_ID}'`);
			runSql(`DELETE FROM subjects WHERE code IN ('E2SMATH','E2SFRA') AND tenant_id = '${TENANT_ID}'`);
		} catch {
			// Tables may not exist
		}

		// Create test classes
		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-Schedule-6A', '6ème', 'active', NOW(), NOW())`
		);
		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-Schedule-5A', '5ème', 'active', NOW(), NOW())`
		);

		// Create test subjects
		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-Schedule-Maths', 'E2SMATH', '#3b82f6', 'active', NOW(), NOW())`
		);
		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-Schedule-Français', 'E2SFRA', '#ef4444', 'active', NOW(), NOW())`
		);

		cleanupScheduleData();
		cleanupCalendarEntries();
		clearCache();
	});

	test.beforeEach(async () => {
		cleanupScheduleData();
		cleanupCalendarEntries();
		try {
			runSql(`DELETE FROM teacher_assignments WHERE tenant_id = '${TENANT_ID}'`);
		} catch {
			// Table may not exist
		}
		seedTeacherAssignments();
		clearCache();
	});

	// ==========================================================================
	// AC3: Slot Modification & Deletion
	// ==========================================================================
	test.describe('AC3: Slot Modification & Deletion', () => {
		test('clicking a slot opens scope modal then edit modal', async ({ page }) => {
			await loginAsAdmin(page);
			await page.goto(`${ALPHA_URL}/admin/schedule`);

			await waitForScheduleReady(page);

			// First create a slot
			const timeCell = page.locator('.time-cell').first();
			await timeCell.click();

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

			await fillSlotForm(dialog);

			await dialog.getByRole('button', { name: /créer/i }).click();
			await expect(dialog).not.toBeVisible({ timeout: 10000 });

			// Click on the created slot — scope modal should appear
			const slotCard = page.locator('.slot-card').first();
			await expect(slotCard).toBeVisible({ timeout: 10000 });
			await slotCard.click();

			await chooseScopeAndEdit(page);

			// Edit modal should appear
			const editDialog = page.getByRole('dialog');
			await expect(editDialog).toBeVisible({ timeout: 10000 });
			await expect(
				editDialog.getByRole('heading', { name: /modifier le créneau/i })
			).toBeVisible();
		});

		test('can delete a slot via edit modal', async ({ page }) => {
			await loginAsAdmin(page);
			await page.goto(`${ALPHA_URL}/admin/schedule`);

			await waitForScheduleReady(page);

			// Create a slot
			const timeCell = page.locator('.time-cell').first();
			await timeCell.click();

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

			await fillSlotForm(dialog, { dayValue: '2', startTime: '14:00', endTime: '15:00' });

			await dialog.getByRole('button', { name: /créer/i }).click();
			await expect(dialog).not.toBeVisible({ timeout: 10000 });

			// Click on the slot — scope modal then edit modal
			const slotCard = page.locator('.slot-card').first();
			await expect(slotCard).toBeVisible({ timeout: 10000 });
			await slotCard.click();

			await chooseScopeAndEdit(page);

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

			// Click delete button — opens scope modal again for delete scope
			await dialog.getByRole('button', { name: /supprimer/i }).click();

			// Scope modal appears for delete action
			const scopeModal = page.locator('.modal-scope');
			await expect(scopeModal).toBeVisible({ timeout: 10000 });
			await scopeModal.getByText('Cette occurrence uniquement').click();

			// Confirmation modal should appear
			const deleteModal = page.getByRole('alertdialog');
			await expect(deleteModal).toBeVisible({ timeout: 10000 });

			// Confirm deletion
			await deleteModal.getByRole('button', { name: /supprimer/i }).click();

			// Modal should close and slot should disappear
			await expect(deleteModal).not.toBeVisible({ timeout: 10000 });
			await expect(page.locator('.slot-card')).not.toBeVisible({ timeout: 10000 });
		});

		test('can modify a slot and see updated data in grid', async ({ page }) => {
			await loginAsAdmin(page);
			await page.goto(`${ALPHA_URL}/admin/schedule`);

			await waitForScheduleReady(page);

			// Create initial slot with room
			const timeCell = page.locator('.time-cell').first();
			await timeCell.click();

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

			await fillSlotForm(dialog, { room: 'A101' });

			await dialog.getByRole('button', { name: /créer/i }).click();
			await expect(dialog).not.toBeVisible({ timeout: 10000 });

			// Verify initial slot with room A101
			const slotCard = page.locator('.slot-card').first();
			await expect(slotCard).toBeVisible({ timeout: 10000 });
			await expect(slotCard.getByText('A101')).toBeVisible();

			// Click to open scope modal then edit modal
			await slotCard.click();

			await chooseScopeAndEdit(page);

			dialog = page.getByRole('dialog');
			await expect(dialog).toBeVisible({ timeout: 10000 });
			await expect(
				dialog.getByRole('heading', { name: /modifier le créneau/i })
			).toBeVisible();

			// Change room to B202
			await dialog.locator('#slot-room').clear();
			await dialog.locator('#slot-room').fill('B202');

			// Submit modification
			await dialog.getByRole('button', { name: /modifier/i }).click();
			await expect(dialog).not.toBeVisible({ timeout: 10000 });

			// Verify success and updated data
			await expect(page.getByText('Créneau modifié.')).toBeVisible({ timeout: 5000 });
			const updatedSlot = page.locator('.slot-card').first();
			await expect(updatedSlot.getByText('B202')).toBeVisible();
		});
	});

	// ==========================================================================
	// AC4: Conflict Detection
	// ==========================================================================
	test.describe('AC4: Conflict Detection', () => {
		test('displays conflict warning when creating slot with same teacher at overlapping time', async ({
			page
		}) => {
			await loginAsAdmin(page);
			await page.goto(`${ALPHA_URL}/admin/schedule`);

			await waitForScheduleReady(page);

			// Step 1: Create first slot (class 6A, Wednesday 10:00-11:00)
			const timeCell = page.locator('.time-cell').first();
			await timeCell.click();

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

			await fillSlotForm(dialog, { dayValue: '3', startTime: '10:00', endTime: '11:00' });

			await dialog.getByRole('button', { name: /créer/i }).click();
			await expect(dialog).not.toBeVisible({ timeout: 10000 });
			await expect(page.locator('.slot-card')).toBeVisible({ timeout: 10000 });

			// Step 2: Create conflicting slot with DIFFERENT class but SAME teacher at same time
			const timeCell2 = page.locator('.time-cell').first();
			await timeCell2.click();

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

			await fillSlotForm(dialog, {
				className: 'E2E-Schedule-5A',
				dayValue: '3',
				startTime: '10:00',
				endTime: '11:00'
			});

			// Submit - should trigger conflict detection
			await dialog.getByRole('button', { name: /créer/i }).click();

			// Conflict warning should appear inside the dialog
			await expect(dialog.locator('.alert-warning')).toBeVisible({ timeout: 10000 });
			await expect(dialog.getByText(/conflits détectés/i)).toBeVisible();

			// Force checkbox should be available
			await expect(dialog.getByText(/forcer la création/i)).toBeVisible();

			// Dialog should still be open (not closed)
			await expect(dialog).toBeVisible();
		});

		test('can force creation despite detected conflict', async ({ page }) => {
			await loginAsAdmin(page);
			await page.goto(`${ALPHA_URL}/admin/schedule`);

			await waitForScheduleReady(page);

			// Step 1: Create first slot (class 6A, Thursday 14:00-15:00)
			const timeCell = page.locator('.time-cell').first();
			await timeCell.click();

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

			await fillSlotForm(dialog, { dayValue: '4', startTime: '14:00', endTime: '15:00' });

			await dialog.getByRole('button', { name: /créer/i }).click();
			await expect(dialog).not.toBeVisible({ timeout: 10000 });
			await expect(page.locator('.slot-card')).toBeVisible({ timeout: 10000 });

			// Step 2: Create conflicting slot with different class, same teacher, same time
			const timeCell2 = page.locator('.time-cell').first();
			await timeCell2.click();

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

			await fillSlotForm(dialog, {
				className: 'E2E-Schedule-5A',
				dayValue: '4',
				startTime: '14:00',
				endTime: '15:00'
			});

			// First submit - triggers conflict warning
			await dialog.getByRole('button', { name: /créer/i }).click();
			await expect(dialog.locator('.alert-warning')).toBeVisible({ timeout: 10000 });

			// Check force checkbox
			await dialog.locator('.force-checkbox input[type="checkbox"]').check();

			// Submit again with force enabled
			await dialog.getByRole('button', { name: /créer/i }).click();

			// Modal should close - slot created despite conflict
			await expect(dialog).not.toBeVisible({ timeout: 10000 });

			// Success message should appear
			await expect(page.getByText('Créneau créé.')).toBeVisible({ timeout: 5000 });
		});
	});

	// ==========================================================================
	// AC5: Calendar Respect (Blocked Days)
	// ==========================================================================
	test.describe('AC5: Calendar Respect', () => {
		test('time validation prevents end before start', async ({ page }) => {
			await loginAsAdmin(page);
			await page.goto(`${ALPHA_URL}/admin/schedule`);

			await waitForScheduleReady(page);

			// Open creation modal
			const timeCell = page.locator('.time-cell').first();
			await timeCell.click();

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

			// Set end time before start time
			await dialog.locator('#slot-start').fill('10:00');
			await dialog.locator('#slot-end').fill('09:00');

			// Error message should appear
			await expect(
				dialog.getByText(/l'heure de fin doit être après/i)
			).toBeVisible();

			// Submit should be disabled
			await expect(dialog.getByRole('button', { name: /créer/i })).toBeDisabled();
		});

		test('blocked day is visually marked in the grid', async ({ page }) => {
			// Seed a holiday on Wednesday of current week
			const wednesdayDate = getWeekdayInCurrentWeek(3);
			seedBlockedDate(wednesdayDate, 'Jour férié test', 'holiday');
			clearCache();

			await loginAsAdmin(page);
			await page.goto(`${ALPHA_URL}/admin/schedule`);
			await waitForScheduleReady(page);

			// Wait for the blocked date badge to appear — confirms API data is loaded
			await expect(page.getByText('Jour férié test')).toBeVisible({ timeout: 20000 });

			// The third day-column (Wednesday) should have the blocked class
			const dayColumns = page.locator('.day-column');
			await expect(dayColumns.nth(2)).toHaveClass(/day-blocked/, { timeout: 5000 });
		});

		test('cannot create a slot on a blocked day', async ({ page }) => {
			// Seed a vacation on Tuesday of current week
			const tuesdayDate = getWeekdayInCurrentWeek(2);
			seedBlockedDate(tuesdayDate, 'Vacances test', 'vacation');
			clearCache();

			await loginAsAdmin(page);
			await page.goto(`${ALPHA_URL}/admin/schedule`);
			await waitForScheduleReady(page);

			// Tuesday column should be blocked
			const dayColumns = page.locator('.day-column');
			await expect(dayColumns.nth(1)).toHaveClass(/day-blocked/, { timeout: 10000 });

			// Attempt to click a time cell in the blocked day — dialog should NOT open
			// Use dispatchEvent to bypass pointer-events: none
			await dayColumns.nth(1).locator('.time-cell').first().dispatchEvent('click');

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

		test('recurring slots do not appear on vacation days in next week (AC4)', async ({
			page
		}) => {
			// Compute next week Thursday (use local format to avoid UTC shift)
			const thisThursday = new Date(getWeekdayInCurrentWeek(4) + 'T00:00:00');
			const nextThursday = new Date(thisThursday);
			nextThursday.setDate(thisThursday.getDate() + 7);
			const y = nextThursday.getFullYear();
			const m = String(nextThursday.getMonth() + 1).padStart(2, '0');
			const d = String(nextThursday.getDate()).padStart(2, '0');
			const nextThursdayStr = `${y}-${m}-${d}`;

			// Clean calendar entries and seed a vacation on next Thursday only
			cleanupCalendarEntries();
			seedBlockedDate(nextThursdayStr, 'Vacances AC4', 'vacation');
			clearCache();

			await loginAsAdmin(page);
			await page.goto(`${ALPHA_URL}/admin/schedule`);
			await waitForScheduleReady(page);

			// Create a Thursday slot (if none exists) so we have a recurring slot on Thursday
			const dayColumns = page.locator('.day-column');
			// Check if Thursday column (index 3) already has a slot
			const thursdaySlots = dayColumns.nth(3).locator('.slot-card');
			const existingCount = await thursdaySlots.count();

			if (existingCount === 0) {
				// Create a slot on Thursday
				const thursdayCell = dayColumns.nth(3).locator('.time-cell').first();
				await thursdayCell.click();

				const dialog = page.getByRole('dialog');
				await expect(dialog).toBeVisible({ timeout: 10000 });
				await fillSlotForm(dialog, { dayValue: '4', startTime: '09:00', endTime: '10:00' });
				await dialog.getByRole('button', { name: /créer/i }).click();
				await expect(dialog).not.toBeVisible({ timeout: 10000 });
				await waitForScheduleReady(page);
			}

			// Verify slot is visible on current week's Thursday
			await expect(dayColumns.nth(3).locator('.slot-card').first()).toBeVisible({
				timeout: 10000
			});

			// Navigate to next week
			await page.getByRole('button', { name: 'Semaine suivante' }).click();
			await waitForScheduleReady(page);

			// Next week Thursday should be blocked
			await expect(dayColumns.nth(3)).toHaveClass(/day-blocked/, { timeout: 10000 });

			// No slot card should appear on the blocked Thursday
			await expect(dayColumns.nth(3).locator('.slot-card')).toHaveCount(0);
		});
	});
});
