import { Page, expect } from '@playwright/test';
import { logOffset, waitForOtp } from './api-log';

/**
 * A new applicant, unique to this run.
 *
 * Every spec makes its own. Sharing one account between specs means the second
 * run of the suite starts from a state the first run left behind — an
 * applicant who already declared their operations, already holds a licence —
 * and the assertions quietly stop meaning what they say.
 */
export interface Applicant {
  email: string;
  username: string;
  phoneNumber: string;
  password: string;
  /** The account name, as typed at signup. Always `${firstName} ${middleName} ${lastName}`. */
  name: string;
  firstName: string;
  middleName: string;
  lastName: string;
}

export function newApplicant(label: string): Applicant {
  const stamp = `${Date.now()}${Math.floor(Math.random() * 1000)}`;
  // The profile's Maritime tab refuses to save unless first/middle/last join to
  // exactly the account name (`ProfilePage.onSaveProfile`) — and that refusal is
  // a silent early return, no request. So the parts are the source of truth here
  // and the account name is composed from them, rather than the two being
  // written independently and hoped to agree.
  //
  // Each part is at least three characters, which `profileSchema` requires.
  const firstName = 'Dawit';
  const middleName = 'Bekele';
  const lastName = `Tesfaye${stamp.slice(-4)}`;
  return {
    email: `e2e.${label}.${stamp}@example.test`,
    username: `e2e${label}${stamp}`.slice(0, 28),
    // Ethiopian mobile format; the last digits vary so two runs never collide.
    phoneNumber: `+2519${stamp.slice(-8)}`,
    password: 'E2ePassw0rd!',
    name: `${firstName} ${middleName} ${lastName}`,
    firstName,
    middleName,
    lastName,
  };
}

/**
 * Fills and submits the signup form.
 *
 * Returns the log offset taken immediately before submitting, so the caller can
 * pick up the one-time code that submission triggers without matching a stale
 * one from an earlier test.
 */
export async function signUp(page: Page, applicant: Applicant): Promise<number> {
  await page.goto('/signup');

  // The form has shipped both as one full-name field and as first/middle/last
  // parts; `applicant.name` is what the parts join to, so either is filled.
  const fullName = page.getByLabel(/^(full )?name \(english\)$/i);
  if (await fullName.isVisible({ timeout: 5_000 }).catch(() => false)) {
    await fullName.fill(applicant.name);
  } else {
    await page.getByLabel('First name').fill(applicant.firstName);
    await page.getByLabel('Middle name').fill(applicant.middleName);
    await page.getByLabel('Last name').fill(applicant.lastName);
  }
  await page.getByLabel('Email address').fill(applicant.email);
  await page.getByLabel('Username').fill(applicant.username);
  await page.getByLabel('Phone number').fill(applicant.phoneNumber);
  await page.getByLabel('Password', { exact: true }).fill(applicant.password);
  await page.getByLabel('Confirm password').fill(applicant.password);

  // The terms checkbox gates the submit button.
  await page.getByRole('checkbox').check();

  const offset = logOffset();
  const submit = page.getByRole('button', { name: /create account|sign up/i });
  await submit.click();
  // The first request after the API boots occasionally fails in the browser
  // before it reaches the server ("Network error"); the form stays filled, so
  // resubmitting is exactly what a person would do.
  for (let attempt = 0; attempt < 3; attempt++) {
    const failed = page.getByText(/network error/i);
    const outcome = await Promise.race([
      page.waitForURL(/\/(otp-verify|onboarding|dashboard)/, { timeout: 15_000 }).then(() => 'navigated'),
      failed.waitFor({ state: 'visible', timeout: 15_000 }).then(() => 'failed'),
    ]).catch(() => 'timeout');
    if (outcome !== 'failed') break;
    await page.waitForTimeout(2_000);
    await submit.click();
  }
  return offset;
}

/**
 * Completes phone verification by typing the real code.
 *
 * No-op when signup did not land on the OTP screen — whether it does depends
 * on how the account was created, and a test that asserts the flow rather than
 * the screen should not care.
 */
export async function verifyOtpIfPrompted(
  page: Page,
  sinceOffset: number,
): Promise<boolean> {
  await page.waitForURL(/\/(otp-verify|onboarding|dashboard)/, { timeout: 30_000 });
  if (!page.url().includes('/otp-verify')) return false;

  const code = await waitForOtp(sinceOffset);

  // Mantine's PinInput is one input per character and moves focus itself, so
  // the code is typed rather than filled — filling each box individually
  // fights the component's own focus handling.
  const boxes = page.locator('input[inputmode], input[type="text"]');
  await boxes.first().click();
  await page.keyboard.type(code, { delay: 30 });

  // `onComplete` submits on the last character; the button is the fallback for
  // when it does not fire.
  const submit = page.getByRole('button', { name: /^verify$/i });
  await Promise.race([
    page.waitForURL(/\/(onboarding|dashboard)/, { timeout: 10_000 }).catch(() => null),
    submit.click({ timeout: 5_000 }).catch(() => null),
  ]);
  return true;
}

/** Signs in an existing applicant through the login screen. */
export async function logIn(page: Page, applicant: Applicant): Promise<void> {
  await page.goto('/login');
  // The API takes an email here, not a username — sending a username answers
  // "email should not be empty".
  await page.getByLabel(/email/i).fill(applicant.email);
  await page.getByLabel(/password/i).fill(applicant.password);
  await page.getByRole('button', { name: /sign in|log in|login/i }).click();
  await expect(page).toHaveURL(/\/(onboarding|dashboard|profile)/, { timeout: 30_000 });
}
