import { expect, type Page } from '../fixtures.js';
import { convexRun } from './convex.js';
import { MailpitClient } from './mailpit.js';

const mailpit = new MailpitClient();

/**
 * Fresh 10-char application number in the dashboard-validator format
 * ("1-" prefix + 8 digits, ApplicationIdValidator). Single definition —
 * the format is domain knowledge and must not drift across per-spec
 * copies.
 */
export const newApplicationId = (): string =>
  `1-${String(Math.floor(Math.random() * 1e8)).padStart(8, '0')}`;

/**
 * Create an authenticated student page via magic link flow.
 * Shared helper used across student E2E tests.
 *
 * By default, also fills the auto-opening "complete profile" dialog so subsequent
 * dashboard interactions are not blocked. Pass `completeProfile: false` for tests
 * that explicitly need to assert on the unfilled-profile state.
 */
export async function createStudentPage(
  browser: { newContext: (opts: object) => Promise<{ newPage: () => Promise<Page>; close: () => Promise<void> }> },
  customerConfig: { url: string },
  customerName: string,
  emailPrefix = 'e2e-student',
  options: { completeProfile?: boolean; email?: string } = {},
): Promise<{ page: Page; context: { close: () => Promise<void> }; email: string }> {
  const { completeProfile = true } = options;
  const testEmail =
    options.email ??
    `${emailPrefix}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}@e2e.local`;

  const requestUrl = new URL('/api/actions/auth/magic-link/request', customerConfig.url);
  requestUrl.searchParams.set('redirect_uri', customerConfig.url);
  requestUrl.searchParams.set('customer', customerName);

  const response = await fetch(requestUrl.toString(), {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email: testEmail }),
  });
  if (!response.ok) {
    throw new Error(`Magic link request failed: ${response.status} ${response.statusText}`);
  }

  const email = await mailpit.waitForEmail(testEmail, 15_000);
  const emailDetail = await mailpit.getEmailBody(email.ID);
  const magicLinkUrl = mailpit.extractMagicLinkUrl(emailDetail.HTML);

  const context = await browser.newContext({
    baseURL: customerConfig.url,
    ignoreHTTPSErrors: true,
  });
  const page = await context.newPage();
  await page.goto(magicLinkUrl);
  // Wait for verify to complete: VerifyComponent navigates to /students/application
  // on success. The /students/verify URL also matches /students/, so we must check
  // for /students/application specifically — otherwise the next page.goto() can
  // race and abort the in-flight verify fetch before the session cookie is set.
  await expect(page).toHaveURL(/\/students\/application/, { timeout: 15_000 });

  if (completeProfile) {
    await fillProfileDialog(page);
  }

  return { page, context, email: testEmail };
}

/**
 * Fill the auto-opening "Complete profile" dialog with placeholder values
 * and submit it. No-op if the dialog isn't present.
 */
async function fillProfileDialog(page: Page): Promise<void> {
  const dialog = page.locator('.p-dialog-mask .p-dialog').first();
  const isOpen = await dialog.waitFor({ state: 'visible', timeout: 5_000 }).then(() => true).catch(() => false);
  if (!isOpen) return;

  await dialog.locator('input[formcontrolname="firstName"]').fill('E2E');
  await dialog.locator('input[formcontrolname="lastName"]').fill('Student');
  const saveButton = dialog.locator('p-button').filter({ hasText: /save|speichern/i }).locator('button').first();
  await saveButton.click();
  await expect(dialog).not.toBeVisible({ timeout: 5_000 });
}

/**
 * Create an authenticated student and create a new application through the
 * dashboard dialog. Every failure path THROWS instead of returning a flag:
 * an earlier version reported `hasApplication: false` for a disabled Create
 * button (a validation regression) and `true` for a failed navigation, so a
 * broken create flow silently skipped every student-application spec (the
 * 9-char-id incident below). The open application period is seeded up front
 * via `e2e:ensureOpenApplicationPeriod`, so "no active period" is not a
 * legitimate state either.
 */
export async function createStudentWithApplication(
  browser: { newContext: (opts: object) => Promise<{ newPage: () => Promise<Page>; close: () => Promise<void> }> },
  customerConfig: { url: string; firstProcess?: string },
  customerName: string,
): Promise<{
  page: Page;
  context: { close: () => Promise<void> };
  applicationId: string;
}> {
  // Make sure the customer's student process is offered for a new
  // application: the portal only lists processes whose period has an open
  // validity window, which a fresh local dev DB lacks by default.
  if (!customerConfig.firstProcess) {
    throw new Error(
      `Customer ${customerName} has no firstProcess configured — ` +
        'student application specs cannot run without a Convex student process.'
    );
  }
  await convexRun('e2e:ensureOpenApplicationPeriod', {
    customer: customerName,
    process: customerConfig.firstProcess,
    short: 'E2E',
  });

  const { page, context } = await createStudentPage(browser, customerConfig, customerName, 'e2e-sg');

  // Navigate to dashboard
  await page.goto(`${customerConfig.url}/students/application`);
  await expect(page).toHaveURL(/\/students/, { timeout: 15_000 });

  // The period is seeded above, so the create button MUST render. A missing
  // button is a dashboard regression, not a data problem.
  const newAppButton = page.locator('p-button').filter({ hasText: /new.*application|neue.*bewerbung/i }).first();
  await expect(newAppButton, 'new-application button must render (period is seeded)').toBeVisible({
    timeout: 15_000,
  });

  await newAppButton.click();
  const dialog = page.locator('.p-dialog-mask .p-dialog').first();
  await expect(dialog).toBeVisible({ timeout: 5_000 });

  // Select the process (required: the form is invalid until a process is
  // chosen, so this must actually happen, not be best-effort).
  const radioButton = dialog.locator('p-radiobutton').first();
  await radioButton.waitFor({ state: 'visible', timeout: 5_000 });
  await radioButton.click();

  // Enter a valid application ID. The dashboard validator requires exactly
  // 10 chars: the "1-" prefix plus 8 digits (ApplicationIdValidator). Using 7
  // digits produced a 9-char id that failed validation, left the Create button
  // disabled, and silently skipped every student-application flow.
  const applicationId = `1-${Date.now().toString().slice(-8)}`;
  const appIdInput = dialog.locator('input[pinputtext], input[formcontrolname="applicationId"]').last();
  await appIdInput.fill(applicationId);

  // The Create button enables only once the reactive form validates. Poll for
  // that instead of a one-shot isEnabled() check, which raced the validation
  // and intermittently reported the button as disabled. A button that never
  // enables is a form-validation regression and must FAIL the test.
  const createButton = dialog
    .locator('p-button')
    .filter({ hasText: /create|erstellen/i })
    .locator('button')
    .first();
  await expect(createButton, 'create button must enable once the form validates').toBeEnabled({
    timeout: 10_000,
  });

  await createButton.click();

  // The dashboard navigates to <process>/subject-groups (relative), so the URL
  // carries the process, not the application id — return the generated id here.
  // A create that never navigates means the mutation was rejected: FAIL.
  await expect(page, 'create must navigate into the application').toHaveURL(
    /\/students\/application\//,
    { timeout: 15_000 }
  );

  return { page, context, applicationId };
}

/**
 * Create a student + application and assert the subject-group page
 * loaded. `createStudentWithApplication` seeds the open period and
 * throws on any create failure, so reaching a page WITHOUT the
 * navigation component is a routing/rendering regression — assert it,
 * never skip it. Shared by the subject-group/submission/add-module
 * specs (one copy, so flakiness lessons land everywhere at once).
 */
export async function openSubjectGroupPage(
  browser: Parameters<typeof createStudentWithApplication>[0],
  customerConfig: { url: string; firstProcess?: string },
  customerName: string,
): Promise<{
  page: Page;
  context: { close: () => Promise<void> };
  applicationId: string;
}> {
  const { page, context, applicationId } = await createStudentWithApplication(
    browser,
    customerConfig,
    customerName,
  );
  await expect(
    page.locator('masterev-application-navigation'),
    'subject-group page must render the application navigation',
  ).toBeVisible({ timeout: 15_000 });
  return { page, context, applicationId };
}
