import { execSync } from "node:child_process";
import { expect, type Page } from "@playwright/test";

/** Dev-system credentials — these exist in the planarchy.dev seed data */
export const DEV_USERS = {
  admin: { email: "admin@planarchy.dev", password: "admin123" },
  manager: { email: "manager@planarchy.dev", password: "manager123" },
  viewer: { email: "viewer@planarchy.dev", password: "viewer123" },
} as const;

export async function signIn(page: Page, email: string, password: string) {
  await page.goto("/auth/signin");
  await page.fill('input[type="email"]', email);
  await page.fill('input[type="password"]', password);
  await page.click('button[type="submit"]');
  await expect(page).toHaveURL(/\/(dashboard|resources)/, { timeout: 15000 });
}

export async function signOut(page: Page) {
  // next-auth/react signOut() POSTs to /auth/signout with a CSRF token.
  // There is no GET-accessible signout page in this app (/auth/signout returns 404).
  // Replicate what the client-side signOut() function does:
  // 1. Fetch the CSRF token from /auth/csrf
  // 2. POST to /auth/signout with that token
  // 3. Follow the redirect to /auth/signin
  await page.goto("/dashboard"); // land on any authenticated page for cookie context
  await page.evaluate(async () => {
    const csrfRes = await fetch("/api/auth/csrf");
    const { csrfToken } = await csrfRes.json() as { csrfToken: string };
    await fetch("/api/auth/signout", {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({ csrfToken, callbackUrl: "/auth/signin", json: "true" }),
      redirect: "follow",
    });
  });
  // After the POST clears the session cookie, navigating to a protected route
  // should redirect to sign-in.
  await page.goto("/dashboard");
  await page.waitForURL(/\/auth\/signin/, { timeout: 10000 });
}

// ── Mailhog helpers ────────────────────────────────────────────────────────────

const MAILHOG_API = process.env["MAILHOG_API"] ?? "http://localhost:8025";

type MailhogMessage = {
  Content: {
    Headers: { Subject?: string[]; To?: string[]; "Content-Transfer-Encoding"?: string[] };
    Body: string;
  };
  MIME: {
    Parts?: Array<{
      Headers: { "Content-Type"?: string[]; "Content-Transfer-Encoding"?: string[] };
      Body: string;
    }>;
  } | null;
};

/** Decode a MIME part body based on its Content-Transfer-Encoding header value. */
function decodeMimeBody(body: string, encoding: string | undefined): string {
  const enc = (encoding ?? "").toLowerCase().trim();
  if (enc === "quoted-printable") {
    return body
      .replace(/=\r\n/g, "")   // soft line break (CRLF)
      .replace(/=\n/g, "")      // soft line break (LF)
      .replace(/=([0-9A-Fa-f]{2})/g, (_, hex: string) =>
        String.fromCharCode(parseInt(hex, 16)),
      );
  }
  if (enc === "base64") {
    return Buffer.from(body.replace(/\s/g, ""), "base64").toString("utf8");
  }
  return body;
}

type MailhogResponse = {
  count: number;
  items: MailhogMessage[];
};

/** Delete all messages in Mailhog (call in beforeEach to prevent cross-test contamination). */
export async function clearMailhog(): Promise<void> {
  await fetch(`${MAILHOG_API}/api/v1/messages`, { method: "DELETE" });
}

/**
 * Poll Mailhog until a message to `address` appears. Returns the message.
 * Throws after `timeoutMs` if no matching message is found.
 */
export async function getLatestEmailTo(
  address: string,
  { timeoutMs = 10_000, pollIntervalMs = 500 }: { timeoutMs?: number; pollIntervalMs?: number } = {},
): Promise<{ subject: string; body: string; html: string }> {
  const deadline = Date.now() + timeoutMs;

  while (Date.now() < deadline) {
    const res = await fetch(`${MAILHOG_API}/api/v2/messages?limit=50`);
    if (res.ok) {
      const data = (await res.json()) as MailhogResponse;
      const match = data.items.find((msg) => {
        const to = msg.Content.Headers.To ?? [];
        return to.some((t) => t.toLowerCase().includes(address.toLowerCase()));
      });

      if (match) {
        const subject = (match.Content.Headers.Subject ?? [])[0] ?? "";

        // Decode body parts based on Content-Transfer-Encoding
        const htmlPart = match.MIME?.Parts?.find((p) =>
          (p.Headers["Content-Type"]?.[0] ?? "").includes("text/html"),
        );
        const textPart = match.MIME?.Parts?.find((p) =>
          (p.Headers["Content-Type"]?.[0] ?? "").includes("text/plain"),
        );

        const rootEnc = match.Content.Headers["Content-Transfer-Encoding"]?.[0];
        const htmlEnc = htmlPart?.Headers["Content-Transfer-Encoding"]?.[0];
        const textEnc = textPart?.Headers["Content-Transfer-Encoding"]?.[0];

        const html = htmlPart
          ? decodeMimeBody(htmlPart.Body, htmlEnc)
          : decodeMimeBody(match.Content.Body, rootEnc);
        const body = textPart
          ? decodeMimeBody(textPart.Body, textEnc)
          : decodeMimeBody(match.Content.Body, rootEnc);

        return { subject, body, html };
      }
    }

    await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
  }

  throw new Error(`No email to "${address}" found within ${timeoutMs}ms`);
}

/**
 * Extract a URL from email body/html matching a path prefix.
 * e.g. extractUrlFromEmail(email, "/invite/") → "http://localhost:3100/invite/abc123"
 */
export function extractUrlFromEmail(
  email: { body: string; html: string },
  pathPrefix: string,
): string {
  const text = email.html || email.body;
  const match = text.match(new RegExp(`https?://[^\\s"'<>]*${pathPrefix.replace("/", "\\/")}[^\\s"'<>]*`));
  if (!match?.[0]) {
    throw new Error(`No URL with prefix "${pathPrefix}" found in email`);
  }
  return match[0];
}

/**
 * Reset a user's password directly via the DB (no tRPC roundtrip needed).
 * Computes an argon2id hash in Node.js, updates password_hash via docker exec psql.
 * Use in afterEach teardown — not in the test flow itself.
 */
export async function resetPasswordViaApi(
  _baseURL: string,
  email: string,
  newPassword: string,
): Promise<void> {
  const { hash } = await import("@node-rs/argon2");
  const passwordHash = await hash(newPassword);
  // argon2id hashes use base64 chars only — safe inside a SQL single-quoted string
  // Column name is camelCase (Prisma default) — must be double-quoted in SQL
  const sql = `UPDATE users SET "passwordHash" = '${passwordHash}' WHERE email = '${email}';`;
  execSync(
    `docker exec -i capakraken-postgres-1 psql -U capakraken -d capakraken`,
    { input: sql, encoding: "utf8" },
  );
}

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

/**
 * Intercept all tRPC batch responses and assert none return HTTP 401.
 * Returns a list of intercepted tRPC paths that were called.
 */
export async function assertNoTrpc401s(page: Page, action: () => Promise<void>) {
  const failures: string[] = [];

  page.on("response", (response) => {
    if (response.url().includes("/api/trpc/") && response.status() === 401) {
      const url = new URL(response.url());
      failures.push(url.pathname + url.search.slice(0, 80));
    }
  });

  await action();

  if (failures.length > 0) {
    throw new Error(
      `tRPC 401 responses detected (session registry / auth broken):\n${failures.join("\n")}`,
    );
  }
}
