import type { Locator } from '@playwright/test';
import { test, expect, UI_SETTLE_TIMEOUT, type Page } from '../../fixtures.js';
import {
  unique,
  matchingRows,
  openCreateDialog,
  saveDialog,
  searchAndFind,
  openRowEditor,
  deleteViaDialog,
  expectGoneFromSearch,
} from '../../helpers/settings.js';
import { createUniversityViaUI } from '../../helpers/universities.js';
import { convexRun } from '../../helpers/convex.js';

/**
 * Seed an active university through the universities settings UI so the
 * course dialog's university autocomplete has a deterministic match —
 * "no universities on staging" is a seeding task, not a skip. Every
 * customer with the courses feature also has universities enabled, so
 * the page is always reachable.
 */
async function seedUniversityViaUI(page: Page, name: string): Promise<void> {
  await page.goto('/admin/settings/universities');
  await createUniversityViaUI(page, name);
}

/**
 * Select the seeded university in the course dialog's autocomplete by
 * typing its unique name. The suggestion MUST appear — the university
 * was just created, so a missing suggestion is a bug, not a skip.
 */
async function selectUniversity(page: Page, dialog: Locator, uniName: string): Promise<void> {
  const uniInput = dialog.locator('p-autocomplete input').first();
  // The autocomplete uses [forceSelection]: when the option click fails
  // to register as a selection (overlay re-render under parallel-worker
  // load), the NEXT blur silently wipes the typed text and the required
  // university control stays empty — the save button then never enables.
  // So: select, blur, and require the value to SURVIVE the blur; retry
  // the whole sequence until it does.
  await expect(async () => {
    await uniInput.click();
    await uniInput.fill(uniName);
    const suggestion = page.locator('.p-autocomplete-overlay .p-autocomplete-option').first();
    await expect(
      suggestion,
      `university autocomplete must list the seeded university "${uniName}"`
    ).toBeVisible({ timeout: 5_000 });
    await suggestion.click();
    // Blur so forceSelection passes judgment on the selection.
    await dialog.locator('input').first().click();
    await expect(uniInput).toHaveValue(uniName, { timeout: 2_000 });
  }).toPass({ timeout: UI_SETTLE_TIMEOUT });
}

/**
 * Fill the course dialog's credits input. PrimeNG InputNumber needs
 * click + typed input + blur to commit the value through change detection.
 */
async function fillCredits(dialog: Locator, credits: string): Promise<void> {
  const creditsInput = dialog.locator('p-inputnumber input').first();
  await creditsInput.click();
  await creditsInput.fill('');
  await creditsInput.pressSequentially(credits);
  // Click away to trigger onBlur, which commits the value
  await dialog.locator('input').first().click();
}

/**
 * Create a course via the UI dialog, referencing the (previously
 * seeded) university `uniName`. Hard-asserts every step incl. the
 * closing save dialog — a rejected mutation fails the test.
 */
async function createCourseViaUI(
  page: Page,
  name: string,
  courseId: string,
  uniName: string,
  credits = '1'
): Promise<void> {
  const dialog = await openCreateDialog(page);

  // Fill required text fields: name and courseId
  const inputs = dialog.locator('input[type="text"], input:not([type])');
  await inputs.first().fill(name);
  await inputs.nth(1).fill(courseId);

  await selectUniversity(page, dialog, uniName);
  await fillCredits(dialog, credits);

  await saveDialog(dialog);
}

test.describe('Settings: Courses', () => {
  test.beforeEach(async ({ features, customerConfig }) => {
    test.skip(!features.courses, 'Courses feature not enabled');
    // The course dialog seeds a university, whose form needs the country
    // reference data. Ensure the real countries exist (staging accumulates
    // test countries and may have lost the real set) instead of assuming
    // them — seed the precondition, never skip over it.
    await convexRun('e2e:ensureCountries', { customer: customerConfig.name });
  });

  test('courses page loads and shows list', async ({ adminPage }) => {
    await adminPage.goto('/admin/settings/courses');
    await expect(adminPage).toHaveURL(/\/settings\/courses/);
    await expect(adminPage.locator('masterev-infinite-data-table').first()).toBeVisible({ timeout: 10_000 });
  });

  test('create a new course and verify via search', async ({ adminPage }) => {
    const id = unique();
    const courseName = `E2E Create ${id}`;
    const courseId = `E2E-C-${id}`;
    const uniName = `E2E CourseUni ${id}`;

    await seedUniversityViaUI(adminPage, uniName);
    await adminPage.goto('/admin/settings/courses');

    await createCourseViaUI(adminPage, courseName, courseId, uniName);

    // Verify via search; a row that never appears means the create failed.
    await searchAndFind(adminPage, courseName);
  });

  test('create and edit a course', async ({ adminPage }) => {
    const id = unique();
    const courseName = `E2E Edit ${id}`;
    const editedName = `E2E Edited ${id}`;
    const courseId = `E2E-E-${id}`;
    const uniName = `E2E CourseUni ${id}`;

    // Step 1: Create
    await seedUniversityViaUI(adminPage, uniName);
    await adminPage.goto('/admin/settings/courses');
    await createCourseViaUI(adminPage, courseName, courseId, uniName);

    // Step 2: Open the created course (row-scoped click + hydration assert)
    const dialog = await openRowEditor(adminPage, courseName);

    // Step 3: Modify the name and save; the dialog must close.
    await dialog.locator('input').first().fill(editedName);
    await saveDialog(dialog);

    // Step 4: Verify the updated name via search
    await searchAndFind(adminPage, editedName);
  });

  test('create and delete a course', async ({ adminPage }) => {
    const id = unique();
    const courseName = `E2E Delete ${id}`;
    const courseId = `E2E-D-${id}`;
    const uniName = `E2E CourseUni ${id}`;

    // Step 1: Create
    await seedUniversityViaUI(adminPage, uniName);
    await adminPage.goto('/admin/settings/courses');
    await createCourseViaUI(adminPage, courseName, courseId, uniName);

    // Step 2: Open the created course (an existing course always shows a
    // delete button — deleteViaDialog hard-asserts it)
    const dialog = await openRowEditor(adminPage, courseName);

    // Step 3+4: Delete and confirm; the dialog must close.
    await deleteViaDialog(adminPage, dialog);

    // Step 5: Reload and verify the course is gone from search results
    await expectGoneFromSearch(adminPage, courseName);
  });

  test('create course with subjectGroup, subSubjectGroup and comment', async ({ adminPage }) => {
    const id = unique();
    const courseName = `E2E Fields ${id}`;
    const courseId = `E2E-F-${id}`;
    const uniName = `E2E CourseUni ${id}`;
    const subjectGroup = `SG-${id.slice(0, 6)}`;
    const subSubjectGroup = `SSG-${id.slice(0, 6)}`;
    const comment = `Test comment for ${id}`;

    await seedUniversityViaUI(adminPage, uniName);
    await adminPage.goto('/admin/settings/courses');

    // Step 1: Create course with all optional fields
    const dialog = await openCreateDialog(adminPage);

    // Required fields
    const inputs = dialog.locator('input[type="text"], input:not([type])');
    await inputs.first().fill(courseName);
    await inputs.nth(1).fill(courseId);
    await selectUniversity(adminPage, dialog, uniName);
    await fillCredits(dialog, '3');

    // Optional fields: subjectGroup and subSubjectGroup
    await dialog.locator('input[formcontrolname="subjectGroup"]').first().fill(subjectGroup);
    await dialog.locator('input[formcontrolname="subSubjectGroup"]').first().fill(subSubjectGroup);

    // Comment textarea
    const commentTextarea = dialog.locator('textarea').first();
    await expect(commentTextarea).toBeVisible({ timeout: 3_000 });
    await commentTextarea.fill(comment);

    await saveDialog(dialog);

    // Step 2: Re-open and verify all optional fields persisted
    // (openRowEditor searches, scopes the edit click and asserts hydration)
    const editDialog = await openRowEditor(adminPage, courseName);

    const savedSubjectGroup = editDialog.locator('input[formcontrolname="subjectGroup"]').first();
    await expect(savedSubjectGroup).toHaveValue(subjectGroup, { timeout: 3_000 });

    const savedSubSubjectGroup = editDialog.locator('input[formcontrolname="subSubjectGroup"]').first();
    await expect(savedSubSubjectGroup).toHaveValue(subSubjectGroup, { timeout: 3_000 });

    const savedComment = editDialog.locator('textarea').first();
    await expect(savedComment).toHaveValue(comment, { timeout: 3_000 });

    await editDialog.locator('[data-testid="cancel-button"]').first().click();
    await expect(editDialog).not.toBeVisible({ timeout: 5_000 });

    // Step 3: Verify the comment indicator is visible in the courses list
    // (the search filter from openRowEditor is still applied)
    const courseRow = matchingRows(adminPage, courseName).first();
    await expect(courseRow).toBeVisible({ timeout: 5_000 });
    const commentIcon = courseRow.locator('.pi-comment').first();
    await expect(commentIcon).toBeVisible({ timeout: 5_000 });
    await commentIcon.hover();
    const tooltip = adminPage.locator('.p-tooltip-text');
    await expect(tooltip).toBeVisible({ timeout: 5_000 });
    await expect(tooltip).toHaveText(comment);
  });

  test('toggle isActive and verify persistence', async ({ adminPage }) => {
    // The courses RLS read rule lets ADMINS see inactive courses
    // (rls.rules.ts, `isAdmin || course.isActive`), so a deactivated
    // course stays findable and re-activatable in the settings list —
    // this is exactly what this test locks.
    const id = unique();
    const courseName = `E2E Active ${id}`;
    const courseId = `E2E-A-${id}`;
    const uniName = `E2E CourseUni ${id}`;

    // Step 1: Create course (isActive defaults to true)
    await seedUniversityViaUI(adminPage, uniName);
    await adminPage.goto('/admin/settings/courses');
    await createCourseViaUI(adminPage, courseName, courseId, uniName);

    // Step 2: Open the course and toggle isActive off
    const dialog = await openRowEditor(adminPage, courseName);

    // PrimeNG ToggleSwitch: host element has the click listener
    const toggleHost = dialog.locator('p-toggleswitch').first();
    await expect(toggleHost).toBeVisible({ timeout: 3_000 });
    const toggle = dialog.getByRole('switch').first();
    const wasChecked = await toggle.isChecked();

    // Click the host element to toggle the switch
    await toggleHost.click();

    // Verify the toggle state changed using web-first assertion
    if (wasChecked) {
      await expect(toggle).not.toBeChecked({ timeout: 3_000 });
    } else {
      await expect(toggle).toBeChecked({ timeout: 3_000 });
    }

    await saveDialog(dialog);

    // Step 3: Reload and re-open to verify the isActive state was
    // toggled. The admin list (courses.listPaginated) does NOT filter by
    // isActive, so the deactivated course must still be findable —
    // openRowEditor hard-fails otherwise.
    await adminPage.goto('/admin/settings/courses');
    const editDialog = await openRowEditor(adminPage, courseName);

    const savedToggle = editDialog.getByRole('switch').first();
    await expect(savedToggle).toBeVisible({ timeout: 3_000 });
    if (wasChecked) {
      await expect(savedToggle).not.toBeChecked();
    } else {
      await expect(savedToggle).toBeChecked();
    }

    await editDialog.locator('[data-testid="cancel-button"]').first().click();
    await expect(editDialog).not.toBeVisible({ timeout: 5_000 });
  });

  test('history button opens entity history panel', async ({ adminPage }) => {
    const id = unique();
    const courseName = `E2E History ${id}`;
    const courseId = `E2E-H-${id}`;
    const uniName = `E2E CourseUni ${id}`;

    // Step 1: Create course
    await seedUniversityViaUI(adminPage, uniName);
    await adminPage.goto('/admin/settings/courses');
    await createCourseViaUI(adminPage, courseName, courseId, uniName);

    // Step 2: Open the course edit dialog
    const dialog = await openRowEditor(adminPage, courseName);

    // Step 3: Click history button
    const historyButton = dialog.locator('[data-testid="history-button"]').first();
    await expect(historyButton).toBeVisible({ timeout: 3_000 });
    await historyButton.click();

    // Step 4: Verify history drawer opens (PrimeNG Drawer renders as .p-drawer)
    const drawer = adminPage.locator('.p-drawer').first();
    await expect(drawer).toBeVisible({ timeout: 10_000 });

    // Close drawer first (Escape), then close dialog
    await adminPage.keyboard.press('Escape');
    await expect(drawer).not.toBeVisible({ timeout: 5_000 });

    await dialog.locator('[data-testid="cancel-button"]').first().click();
    await expect(dialog).not.toBeVisible({ timeout: 10_000 });
  });

  test('create course with credits and verify persistence', async ({ adminPage }) => {
    const id = unique();
    const courseName = `E2E Credits ${id}`;
    const courseId = `E2E-CR-${id}`;
    const uniName = `E2E CourseUni ${id}`;
    // Use an integer to avoid locale-dependent decimal separator issues
    // (PrimeNG InputNumber formats via Intl.NumberFormat, e.g. "3.5" vs "3,5").
    // The component has minFractionDigits=1, so "5" is displayed as "5.0" or "5,0".
    const creditsNumeric = 5;

    // Step 1: Create course with specific credits
    await seedUniversityViaUI(adminPage, uniName);
    await adminPage.goto('/admin/settings/courses');
    await createCourseViaUI(adminPage, courseName, courseId, uniName, String(creditsNumeric));

    // Step 2: Re-open the course to verify credits was saved.
    // openRowEditor scopes the edit click to the matching row and asserts
    // the dialog hydrated with the freshly-created course name, so a
    // wrong-dialog open fails fast instead of timing out on the credits check.
    const editDialog = await openRowEditor(adminPage, courseName);

    const savedCreditsInput = editDialog.locator('p-inputnumber input').first();
    await expect(savedCreditsInput).toBeVisible({ timeout: 3_000 });
    // Verify the numeric value using a retrying assertion. PrimeNG InputNumber
    // formats via Intl.NumberFormat (locale-dependent): "5.0" (en) or "5,0" (de).
    // Match the integer part at the start, allowing for locale-specific decimal formatting.
    await expect(savedCreditsInput).toHaveValue(new RegExp(`^${creditsNumeric}[.,]?`), { timeout: 5_000 });

    // Close the dialog
    await editDialog.locator('[data-testid="cancel-button"]').first().click();
    await expect(editDialog).not.toBeVisible({ timeout: 5_000 });
  });
});
