import { expect, test } from "@playwright/test";
import {
  captureBrowserFailures,
  clickUntilVisible,
  gotoWithTransientRetry,
  isTransientNetworkError,
  loginWithPassword,
  projectEmail,
  registerAndConfirm,
  waitForApplicationEmailLink,
} from "./helpers";

test("registration, email confirmation, password, and email change work end to end", async ({
  browser,
  request,
}, testInfo) => {
  const originalEmail = projectEmail("auth-user", testInfo.project.name);
  const changedEmail = projectEmail("auth-user-changed", testInfo.project.name);
  const password = "E2E public password 2026!";
  const user = await registerAndConfirm(
    browser,
    request,
    originalEmail,
    "E2E Auth User",
  );
  const recoverableOneTimeNavigations = new Set<string>();
  const assertBrowserClean = captureBrowserFailures(user.page, {
    recoverableOneTimeNavigations,
  });

  await user.page.goto("/users/settings");
  const passwordForm = user.page.locator("#update_password");
  await passwordForm.getByLabel("New password", { exact: true }).fill(password);
  await passwordForm.getByLabel("Confirm new password", { exact: true }).fill(password);
  const passwordUpdated = user.page.getByText("Password updated successfully.");
  await passwordForm.getByRole("button", { name: "Save Password" }).click();

  try {
    await expect(passwordUpdated).toBeVisible({ timeout: 5_000 });
  } catch {
    // The password commit rotates the current session. If the following GET
    // is interrupted after the 302 response, never submit the mutation again:
    // recover through an idempotent page and verify the server-side flash.
    await gotoWithTransientRetry(user.page, "/users/settings");
    await expect(passwordUpdated).toBeVisible();
  }

  const emailForm = user.page.locator("#update_email");
  await emailForm.getByLabel("Email").fill(changedEmail);
  await clickUntilVisible(
    emailForm.getByRole("button", { name: "Change Email" }),
    user.page.getByText("A link to confirm your email change has been sent"),
  );

  const emailChangeLink = await waitForApplicationEmailLink(
    request,
    changedEmail,
    "/users/settings/confirm-email",
  );
  recoverableOneTimeNavigations.add(emailChangeLink);

  try {
    await user.page.goto(emailChangeLink);
    await expect(user.page.locator("#email-change-fragment-form")).toBeVisible();
    await user.page
      .getByRole("button", { name: "Confirm email change" })
      .click();
    await expect(user.page.getByText("Email changed successfully.")).toBeVisible();
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);

    if (!isTransientNetworkError(message)) {
      throw error;
    }

    // The server may commit and consume the single-use token before a
    // transport reset reaches the browser. Never replay that URL: verify the
    // committed account state through an idempotent page instead.
    await gotoWithTransientRetry(user.page, "/users/settings");
    await expect(user.page.locator("#update_email").getByLabel("Email")).toHaveValue(
      changedEmail,
    );
  }

  await user.context.close();

  const passwordLogin = await loginWithPassword(browser, changedEmail, password);
  const assertPasswordLoginClean = captureBrowserFailures(passwordLogin.page);
  await expect(passwordLogin.page.getByText(changedEmail, { exact: true })).toBeVisible();

  assertBrowserClean();
  assertPasswordLoginClean();
  await passwordLogin.context.close();
});
