import { test, expect, UI_SETTLE_TIMEOUT, type Page } from '../../fixtures.js';
import type { Locator } from '@playwright/test';
import { DIALOG, unique, searchAndFind } from '../../helpers/settings.js';
import { convexRun } from '../../helpers/convex.js';
import { newApplicationId } from '../../helpers/students.js';

// The students settings page uses a plain p-table, not the default
// masterev-infinite-data-table, so the shared search helper needs its
// row selector. Expanded application rows are also <tr>s, but filtering
// by the seeded student's unique email only matches the account row.
const STUDENT_ROW_SELECTOR = 'p-table tbody tr';


/**
 * Seed a student account with one application. The table lists every
 * student account of the customer, so "no student rows on staging" is
 * never a legitimate skip — tests seed their own row and search for it
 * by the unique email (the backend search matches email substrings).
 */
async function seedStudentRow(
  customerName: string,
  firstProcess: string | undefined
): Promise<{ email: string; applicationId: string }> {
  if (!firstProcess) {
    throw new Error(
      `Customer ${customerName} has students enabled but no firstProcess configured — cannot seed a student application.`
    );
  }
  const id = unique();
  const email = `e2e-settings-student-${id}@e2e.local`;
  const studentId = await convexRun<string>('e2e:createStudent', {
    customer: customerName,
    email,
    firstName: 'E2E',
    lastName: `Settings ${id}`,
  });
  await convexRun('e2e:ensureOpenApplicationPeriod', {
    customer: customerName,
    process: firstProcess,
    short: 'E2E',
  });
  const applicationId = newApplicationId();
  await convexRun('e2e:createStudentApplication', {
    customer: customerName,
    studentId,
    process: firstProcess,
    applicationId,
    periodShort: 'E2E',
  });
  return { email, applicationId };
}

/** Seed a student, open the page, and return its (searched, settled) row. */
async function seedAndFindStudentRow(
  page: Page,
  customerName: string,
  firstProcess: string | undefined
): Promise<{ row: Locator; email: string; applicationId: string }> {
  const { email, applicationId } = await seedStudentRow(customerName, firstProcess);
  await page.goto('/admin/settings/students');
  await expect(page.locator('p-table, .p-datatable').first()).toBeVisible({ timeout: 10_000 });
  const rows = await searchAndFind(page, email, { rowSelector: STUDENT_ROW_SELECTOR });
  return { row: rows.first(), email, applicationId };
}

/** Open the edit dialog for a freshly seeded student and return it. */
async function openSeededStudentEditor(
  page: Page,
  customerName: string,
  firstProcess: string | undefined
): Promise<Locator> {
  const { row } = await seedAndFindStudentRow(page, customerName, firstProcess);
  await row.locator('[data-testid="edit-button"]').first().click();
  const dialog = page.locator(DIALOG).first();
  await expect(dialog).toBeVisible({ timeout: 5_000 });
  return dialog;
}

test.describe('Settings: Students', () => {
  test.beforeEach(({ features }) => {
    test.skip(!features.students, 'Students feature not enabled');
  });

  test('page loads with table and filters', async ({ adminPage }) => {
    await adminPage.goto('/admin/settings/students');
    await expect(adminPage).toHaveURL(/\/settings\/students/);

    // Verify p-table is visible (students page uses p-table, not masterev-infinite-data-table)
    await expect(adminPage.locator('p-table, .p-datatable').first()).toBeVisible({ timeout: 10_000 });

    // Verify process filter dropdown
    await expect(adminPage.locator('p-select').first()).toBeVisible({ timeout: 5_000 });

    // Verify search input
    await expect(adminPage.locator('masterev-search-input').first()).toBeVisible({ timeout: 5_000 });
  });

  test('process filter dropdown works', async ({ adminPage }) => {
    await adminPage.goto('/admin/settings/students');
    await expect(adminPage.locator('p-table, .p-datatable').first()).toBeVisible({ timeout: 10_000 });

    // Click the first p-select (process dropdown)
    const processSelect = adminPage.locator('p-select').first();
    await expect(processSelect).toBeVisible({ timeout: 5_000 });
    await processSelect.click();

    // Verify overlay opens with options
    const option = adminPage.locator('.p-select-overlay .p-select-option').first();
    await expect(option).toBeVisible({ timeout: 5_000 });

    // Capture the option text before selecting
    const optionText = (await option.textContent())!.trim();

    // Click first option to select it
    await option.click();

    // Verify overlay closes
    await expect(adminPage.locator('.p-select-overlay')).not.toBeVisible({ timeout: 5_000 });

    // Verify the selected option text is displayed in the dropdown trigger
    await expect(processSelect).toHaveText(optionText);
  });

  test('application periods button opens dialog', async ({ adminPage }) => {
    await adminPage.goto('/admin/settings/students');
    await expect(adminPage.locator('p-table, .p-datatable').first()).toBeVisible({ timeout: 10_000 });

    // The "Application Periods" / "Bewerbungszeiträume" button always renders
    // with the default "All processes" filter (only a selected Convex process
    // hides it) — a missing button is a rendering regression, not a skip.
    const periodsButton = adminPage.getByRole('button', { name: /application.*period|bewerbungszeitr/i }).first();
    await expect(periodsButton).toBeVisible({ timeout: 5_000 });
    await periodsButton.click();

    // Verify dialog opens
    const dialog = adminPage.locator(DIALOG).first();
    await expect(dialog).toBeVisible({ timeout: 5_000 });

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

  test('student table has rows with content', async ({ adminPage, customerName, customerConfig }) => {
    // Seed a student so the table is guaranteed to have a row, then verify it.
    const { row } = await seedAndFindStudentRow(adminPage, customerName, customerConfig.firstProcess);
    await expect(row).toHaveText(/.+/);
  });

  test('expand student row shows applications', async ({ adminPage, customerName, customerConfig }) => {
    // Seed a student WITH an application so the expanded row has content.
    const { row, applicationId } = await seedAndFindStudentRow(
      adminPage,
      customerName,
      customerConfig.firstProcess
    );

    // Click the expand chevron on the seeded row
    await row.locator('.pi-chevron-right').first().click();

    // Verify it changes to expanded state and shows the seeded application.
    await expect(row.locator('.pi-chevron-down').first()).toBeVisible({ timeout: 5_000 });
    // The expanded body renders account.applications, a Convex reactive join
    // that can lag the seed under staging load — give it the full budget.
    await expect(
      adminPage.locator(STUDENT_ROW_SELECTOR).filter({ hasText: applicationId }).first()
    ).toBeVisible({ timeout: UI_SETTLE_TIMEOUT });
  });

  test('edit student dialog opens with tabs', async ({ adminPage, customerName, customerConfig }) => {
    const dialog = await openSeededStudentEditor(adminPage, customerName, customerConfig.firstProcess);

    // Verify tabs are present (Personal Data, Applications, Mails)
    const tabs = dialog.locator('masterev-tabs, [role="tablist"], .p-tabview-nav').first();
    await expect(tabs).toBeVisible({ timeout: 5_000 });

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

  test('search filters students', async ({ adminPage }) => {
    await adminPage.goto('/admin/settings/students');
    const table = adminPage.locator('p-table, .p-datatable').first();
    await expect(table).toBeVisible({ timeout: 10_000 });

    const searchInput = adminPage.locator('masterev-search-input input').first();
    await expect(searchInput).toBeVisible({ timeout: 5_000 });
    await searchInput.fill('xyznonexistent');
    await expect(searchInput).toHaveValue('xyznonexistent');
    await searchInput.clear();
  });

  // The admin UI deep-links here with ?searchQuery=<applicant number> when a
  // number turned out to be on several student accounts and the admin has to
  // pick the real one. The term must reach the query, not just the input box.
  test('searchQuery param prefills the search and filters the table', async ({ adminPage }) => {
    await adminPage.goto('/admin/settings/students?searchQuery=1-99999999');
    const table = adminPage.locator('p-table, .p-datatable').first();
    await expect(table).toBeVisible({ timeout: 10_000 });

    const searchInput = adminPage.locator('masterev-search-input input').first();
    await expect(searchInput).toHaveValue('1-99999999');

    // No account carries this number, so the filtered table stays empty instead
    // of listing every student.
    await expect(table.locator('tbody tr td[colspan="6"]')).toBeVisible({ timeout: 20_000 });
    await expect(table.locator('tbody [data-testid="edit-button"]')).toHaveCount(0);
  });

  test('typing in the search writes the term back into the URL', async ({ adminPage }) => {
    await adminPage.goto('/admin/settings/students');
    await expect(adminPage.locator('p-table, .p-datatable').first()).toBeVisible({ timeout: 10_000 });

    const searchInput = adminPage.locator('masterev-search-input input').first();
    await searchInput.fill('1-88888888');
    await expect(adminPage).toHaveURL(/searchQuery=1-88888888/, { timeout: 10_000 });

    await searchInput.clear();
    await expect(adminPage).not.toHaveURL(/searchQuery=/, { timeout: 10_000 });
  });

  test('period filter dropdown works', async ({ adminPage, customerName, customerConfig }) => {
    // The period options come from the applicationPeriods table; seed an open
    // period so the dropdown is guaranteed to have at least one option
    // (an empty overlay would otherwise be indistinguishable from a bug).
    if (!customerConfig.firstProcess) {
      throw new Error(
        `Customer ${customerName} has students enabled but no firstProcess configured — cannot seed an application period.`
      );
    }
    await convexRun('e2e:ensureOpenApplicationPeriod', {
      customer: customerName,
      process: customerConfig.firstProcess,
      short: 'E2E',
    });

    await adminPage.goto('/admin/settings/students');
    await expect(adminPage.locator('p-table, .p-datatable').first()).toBeVisible({ timeout: 10_000 });

    // The second p-select is the period filter (always rendered)
    const periodSelect = adminPage.locator('p-select').nth(1);
    await expect(periodSelect).toBeVisible({ timeout: 5_000 });
    await periodSelect.click();

    const option = adminPage.locator('.p-select-overlay .p-select-option').first();
    await expect(option).toBeVisible({ timeout: 5_000 });

    // Capture the option text before selecting
    const optionText = (await option.textContent())!.trim();

    await option.click();
    await expect(adminPage.locator('.p-select-overlay')).not.toBeVisible({ timeout: 5_000 });

    // Verify the selected period text is displayed in the dropdown trigger
    const selectedLabel = periodSelect.locator('.p-select-label');
    await expect(selectedLabel).toHaveText(optionText);
  });

  test('application periods dialog shows tabs and periods', async ({ adminPage }) => {
    await adminPage.goto('/admin/settings/students');
    await expect(adminPage.locator('p-table, .p-datatable').first()).toBeVisible({ timeout: 10_000 });

    // Always rendered with the default "All processes" filter (see above).
    const periodsButton = adminPage.getByRole('button', { name: /application.*period|bewerbungszeitr/i }).first();
    await expect(periodsButton).toBeVisible({ timeout: 5_000 });

    await periodsButton.click();
    const dialog = adminPage.locator(DIALOG).first();
    await expect(dialog).toBeVisible({ timeout: 5_000 });

    // Verify process tabs exist
    const tabs = dialog.locator('[role="tab"]');
    await expect(tabs.first()).toBeVisible({ timeout: 5_000 });

    // Verify accordion panels exist (Past, Current, Next)
    const accordionPanels = dialog.locator('p-accordion-panel');
    const panelCount = await accordionPanels.count();
    expect(panelCount).toBeGreaterThanOrEqual(3);

    // Verify create button exists
    await expect(dialog.locator('[data-testid="create-button"]').first()).toBeVisible({ timeout: 3_000 });

    await adminPage.keyboard.press('Escape');
    await expect(dialog).not.toBeVisible({ timeout: 5_000 });
  });

  // Group edit dialog tests — each seeds its own student row and opens the
  // editor on exactly that row, so ambient staging data is never required.
  test.describe('student edit dialog', () => {
    test('personal data tab shows name fields and action buttons', async ({
      adminPage,
      customerName,
      customerConfig,
    }) => {
      const dialog = await openSeededStudentEditor(adminPage, customerName, customerConfig.firstProcess);

      // Personal data tab is active by default — verify specific form fields
      await expect(dialog.locator('input[formcontrolname="firstName"]')).toBeVisible({ timeout: 5_000 });
      await expect(dialog.locator('input[formcontrolname="lastName"]')).toBeVisible();
      await expect(dialog.locator('input[formcontrolname="email"]')).toBeVisible();

      // Verify cancel and save buttons
      await expect(dialog.locator('[data-testid="cancel-button"]').first()).toBeVisible();
      await expect(dialog.locator('[data-testid="save-button"]').first()).toBeVisible();

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

    test('applications tab shows content', async ({ adminPage, customerName, customerConfig }) => {
      const dialog = await openSeededStudentEditor(adminPage, customerName, customerConfig.firstProcess);

      // Click Applications tab (second tab)
      const tabs = dialog.locator('[role="tab"]');
      await tabs.nth(1).click();

      // Verify content loaded (either application cards or "no applications" message)
      const tabPanel = dialog.locator('masterev-tabpanel').nth(1);
      await expect(tabPanel).toBeVisible({ timeout: 3_000 });

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

    test('mails tab shows mail list', async ({ adminPage, customerName, customerConfig }) => {
      const dialog = await openSeededStudentEditor(adminPage, customerName, customerConfig.firstProcess);

      // Click Mails tab (third tab)
      const tabs = dialog.locator('[role="tab"]');
      await tabs.nth(2).click();

      // Verify mail list component loaded
      const mailList = dialog.locator('masterev-mail-sent-list').first();
      await expect(mailList).toBeVisible({ timeout: 5_000 });

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

    test('generate login link produces valid URL', async ({ adminPage, customerName, customerConfig }) => {
      const dialog = await openSeededStudentEditor(adminPage, customerName, customerConfig.firstProcess);

      // The generate-login-link button always renders on the personal data tab.
      const linkButton = dialog.locator('button').filter({ hasText: /login.*link|anmeldelink/i }).first();
      await expect(linkButton).toBeVisible({ timeout: 5_000 });

      await linkButton.click();

      // Wait for the generated link input (scoped by aria-label to avoid matching unrelated readonly inputs)
      const linkInput = dialog.locator('input[readonly][aria-label]').first();
      await expect(linkInput).toBeVisible({ timeout: 10_000 });
      const linkValue = await linkInput.inputValue();
      expect(linkValue).toMatch(/^https?:\/\//);

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