/**
 * E2E tests for MFA (TOTP) flows.
 *
 * Coverage:
 *   1. MFA setup: generate secret, enter valid TOTP, confirm enabled
 *   2. MFA login: sign in with password → TOTP prompt → enter code → dashboard
 *   3. MFA login: wrong TOTP code → error message, stays on prompt
 *   4. Login without MFA: no TOTP prompt for users without MFA enabled
 *
 * Design notes:
 *   - Uses the admin user from STORAGE_STATE to set up/tear down MFA via tRPC.
 *   - TOTP codes are generated in Node context using the `otpauth` package.
 *   - Each test that enables MFA cleans up via disableMfa() in afterEach so
 *     other test suites are not affected.
 *   - Tests run against the live dev server (playwright.dev.config.ts).
 */

import { expect, test, type Page } from "@playwright/test";
import { TOTP, Secret } from "otpauth";
import { STORAGE_STATE } from "../../playwright.dev.config.js";

// ─── tRPC helpers ─────────────────────────────────────────────────────────────

type TrpcResult = { result?: { data?: unknown }; error?: { data?: { code?: string }; message?: string } };

async function trpcMutation(page: Page, procedure: string, input: unknown = null): Promise<TrpcResult> {
  return page.evaluate(
    async ({ procedure, input }) => {
      const res = await fetch(`/api/trpc/${procedure}?batch=1`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        credentials: "include",
        body: JSON.stringify({ "0": { json: input } }),
      });
      const body = (await res.json()) as TrpcResult[];
      return body[0] ?? {};
    },
    { procedure, input },
  );
}

async function trpcQuery(page: Page, procedure: string, input: unknown = null): Promise<TrpcResult> {
  return page.evaluate(
    async ({ procedure, input }) => {
      const encodedInput = encodeURIComponent(JSON.stringify({ "0": { json: input } }));
      const res = await fetch(`/api/trpc/${procedure}?batch=1&input=${encodedInput}`, {
        credentials: "include",
      });
      const body = (await res.json()) as TrpcResult[];
      return body[0] ?? {};
    },
    { procedure, input },
  );
}

// Enable MFA for the session user and return the TOTP instance for code generation.
async function enableMfaForSession(page: Page): Promise<TOTP> {
  const genRes = await trpcMutation(page, "user.generateTotpSecret");
  const data = (genRes.result?.data as { json?: { secret?: string; uri?: string } })?.json;
  if (!data?.secret) throw new Error(`generateTotpSecret failed: ${JSON.stringify(genRes)}`);

  const totp = new TOTP({
    issuer: "CapaKraken",
    algorithm: "SHA1",
    digits: 6,
    period: 30,
    secret: Secret.fromBase32(data.secret),
  });

  const token = totp.generate();
  const enableRes = await trpcMutation(page, "user.verifyAndEnableTotp", { token });
  if (enableRes.error) throw new Error(`verifyAndEnableTotp failed: ${JSON.stringify(enableRes)}`);

  return totp;
}

// Disable MFA for the session user. Fetches current user profile first to get the userId.
async function disableMfaForSession(page: Page): Promise<void> {
  const profileRes = await trpcQuery(page, "user.getCurrentUserProfile");
  const profile = (profileRes.result?.data as { json?: { id?: string } })?.json;
  if (!profile?.id) throw new Error("Could not resolve current user id for MFA disable");
  await trpcMutation(page, "user.disableTotp", { userId: profile.id });
}

// ─── test suite ───────────────────────────────────────────────────────────────

test.describe("MFA — setup flow (account/security page)", () => {
  test.use({ storageState: STORAGE_STATE.admin });

  let totp: TOTP | null = null;

  test.afterEach(async ({ page }) => {
    // Clean up: disable MFA if a test enabled it
    if (totp) {
      await disableMfaForSession(page).catch(() => {/* already disabled or admin override */});
      totp = null;
    }
  });

  test("generates a TOTP secret and returns a valid otpauth URI", async ({ page }) => {
    await page.goto("/account/security");
    await page.waitForLoadState("networkidle");

    const genRes = await trpcMutation(page, "user.generateTotpSecret");
    const data = (genRes.result?.data as { json?: { secret?: string; uri?: string } })?.json;

    expect(data?.secret).toBeTruthy();
    expect(data?.uri).toMatch(/^otpauth:\/\/totp\//);
    expect(data?.uri).toContain("CapaKraken");
  });

  test("verifyAndEnableTotp accepts a valid code and enables MFA", async ({ page }) => {
    await page.goto("/account/security");

    totp = await enableMfaForSession(page);

    // Verify status via tRPC
    const statusRes = await trpcQuery(page, "user.getMfaStatus");
    const status = (statusRes.result?.data as { json?: { totpEnabled?: boolean } })?.json;
    expect(status?.totpEnabled).toBe(true);
  });

  test("verifyAndEnableTotp rejects an invalid 6-digit code", async ({ page }) => {
    await page.goto("/account/security");

    // First generate a secret
    await trpcMutation(page, "user.generateTotpSecret");

    // Submit obviously wrong code
    const res = await trpcMutation(page, "user.verifyAndEnableTotp", { token: "000000" });
    expect(res.error?.data?.code).toBe("BAD_REQUEST");
    expect(res.error?.message).toMatch(/Invalid TOTP token/i);
  });

  test("setup UI shows QR code and secret fields after generating", async ({ page }) => {
    await page.goto("/account/security");
    await page.waitForLoadState("networkidle");

    // Click the enable/setup button if MFA is not yet enabled
    const setupBtn = page.getByRole("button", { name: /set up/i }).or(
      page.getByRole("button", { name: /enable.*mfa/i }),
    );

    if (await setupBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
      await setupBtn.click();
      // QR code image or canvas should appear
      await expect(
        page.locator('img[alt*="QR"], canvas, [data-testid="qr-code"]').first(),
      ).toBeVisible({ timeout: 5000 });
    }
  });
});

// ─── MFA login flow ───────────────────────────────────────────────────────────

test.describe("MFA — login flow", () => {
  // These tests need a fresh unauthenticated browser context
  test.use({ storageState: { cookies: [], origins: [] } });

  let totp: TOTP | null = null;

  // Enable MFA on admin before each login test using a separate authenticated context
  test.beforeEach(async ({ browser }) => {
    const ctx = await browser.newContext({ storageState: STORAGE_STATE.admin });
    const page = await ctx.newPage();
    await page.goto("/dashboard");
    totp = await enableMfaForSession(page);
    await ctx.close();
  });

  test.afterEach(async ({ browser }) => {
    if (!totp) return;
    // Re-authenticate as admin (without MFA — use admin bypass via fresh session)
    // Since we just enabled MFA on the admin account, we need to sign in with TOTP to disable
    const ctx = await browser.newContext();
    const page = await ctx.newPage();
    await page.goto("/auth/signin");
    await page.fill('input[type="email"]', "admin@planarchy.dev");
    await page.fill('input[type="password"]', "admin123");
    await page.click('button[type="submit"]');
    await page.waitForURL(/\/auth\/signin/, { timeout: 5000 }).catch(() => {});

    // Enter TOTP if prompted
    const totpInput = page.locator("#totp");
    if (await totpInput.isVisible({ timeout: 3000 }).catch(() => false)) {
      await totpInput.fill(totp!.generate());
      await page.click('button[type="submit"]');
      await page.waitForURL(/\/(dashboard|resources)/, { timeout: 10000 }).catch(() => {});
    }

    // Disable MFA
    await disableMfaForSession(page).catch(() => {});
    await ctx.close();
    totp = null;
  });

  test("login with MFA-enabled account: password step shows TOTP prompt", async ({ page }) => {
    await page.goto("/auth/signin");
    await page.fill('input[type="email"]', "admin@planarchy.dev");
    await page.fill('input[type="password"]', "admin123");
    await page.click('button[type="submit"]');

    // Should NOT redirect to dashboard — should show TOTP input
    await expect(page.locator("#totp")).toBeVisible({ timeout: 8000 });
    await expect(page.getByText(/two-factor/i)).toBeVisible();
  });

  test("login with MFA: valid TOTP code completes sign-in", async ({ page }) => {
    await page.goto("/auth/signin");
    await page.fill('input[type="email"]', "admin@planarchy.dev");
    await page.fill('input[type="password"]', "admin123");
    await page.click('button[type="submit"]');

    await expect(page.locator("#totp")).toBeVisible({ timeout: 8000 });

    const code = totp!.generate();
    await page.fill("#totp", code);
    await page.click('button[type="submit"]');

    await expect(page).toHaveURL(/\/(dashboard|resources)/, { timeout: 12000 });
  });

  test("login with MFA: wrong TOTP code shows error and stays on MFA prompt", async ({ page }) => {
    await page.goto("/auth/signin");
    await page.fill('input[type="email"]', "admin@planarchy.dev");
    await page.fill('input[type="password"]', "admin123");
    await page.click('button[type="submit"]');

    await expect(page.locator("#totp")).toBeVisible({ timeout: 8000 });

    await page.fill("#totp", "000000");
    await page.click('button[type="submit"]');

    // Should show error and remain on TOTP step
    await expect(
      page.getByText(/invalid.*code|incorrect.*token|try again/i).or(
        page.locator("[data-error]"),
      ).first(),
    ).toBeVisible({ timeout: 5000 });

    // Should NOT have navigated away
    expect(page.url()).not.toMatch(/\/(dashboard|resources)/);
  });
});

// ─── Login without MFA ────────────────────────────────────────────────────────

test.describe("MFA — users without MFA enabled", () => {
  test.use({ storageState: { cookies: [], origins: [] } });

  test("login for MFA-less user goes straight to dashboard without TOTP prompt", async ({ page }) => {
    await page.goto("/auth/signin");
    await page.fill('input[type="email"]', "manager@planarchy.dev");
    await page.fill('input[type="password"]', "manager123");
    await page.click('button[type="submit"]');

    // No TOTP step — straight redirect
    await expect(page).toHaveURL(/\/(dashboard|resources)/, { timeout: 12000 });
    await expect(page.locator("#totp")).not.toBeVisible();
  });
});
