/**
 * User-Enumeration Hardening Suite
 *
 * The auth endpoints must not reveal whether an email address is registered.
 * Signup answers the same 200 `verification_sent` for every outcome, login
 * folds Google-only accounts into plain `invalid_credentials`, and the
 * revealing error codes (`email_taken`, `email_taken_google`, `use_google`) are
 * never emitted by any auth route.
 *
 * Pure decision logic plus static source assertions — no database required.
 */

import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { describe, test, expect } from "./runner";
import {
  DISCONTINUED_ENUMERATION_CODES,
  loginDecision,
  signupDecision,
  signupResponseBody,
  type SignupAccountState,
} from "../../src/lib/auth/neutral";
import { isAuthErrorCode } from "../../src/lib/auth/errors";

/**
 * Root of the workspace. The runner is always invoked from the workspace root
 * (`npx tsx tests/e2e/runner.ts …`), so the current working directory is the
 * anchor — this also keeps the static source assertions working when the suite
 * runs from a compiled copy elsewhere.
 */
const WORKSPACE_ROOT = process.cwd();

function readAuthRouteSource(route: string): string {
  return readFileSync(resolve(WORKSPACE_ROOT, "src", "app", "api", "auth", route), "utf8");
}

describe("UserEnumeration", () => {
  describe("signup answers are identical for every account state", () => {
    const ALL_STATES: SignupAccountState[] = ["new", "unverified", "verified", "google_only"];

    test("every account state maps to the same verification_sent status", () => {
      for (const state of ALL_STATES) {
        expect(signupDecision(state).status).toBe("verification_sent");
      }
    });

    test("only new and never-confirmed accounts get a real confirmation mail", () => {
      expect(signupDecision("new").sendMail).toBe(true);
      expect(signupDecision("unverified").sendMail).toBe(true);
      expect(signupDecision("verified").sendMail).toBe(false);
      expect(signupDecision("google_only").sendMail).toBe(false);
    });

    test("the wire body is identical for new, existing and Google-only accounts", () => {
      const bodies = ALL_STATES.map((state) => signupResponseBody(signupDecision(state), undefined));
      for (const body of bodies) {
        expect(body).toEqual({ status: "verification_sent" });
      }
      // Every pair is byte-for-byte the same shape.
      expect(bodies[0]).toEqual(bodies[1]);
      expect(bodies[1]).toEqual(bodies[2]);
      expect(bodies[2]).toEqual(bodies[3]);
    });

    test("devLink appears only in the dev fallback and only when a mail was produced", () => {
      // Production / no dev fallback: never present, for any state.
      for (const state of ALL_STATES) {
        expect(signupResponseBody(signupDecision(state), undefined)).toEqual({
          status: "verification_sent",
        });
      }
      // Dev fallback with a produced mail: unverified accounts get the link…
      expect(signupResponseBody(signupDecision("unverified"), "http://localhost/dev-link")).toEqual(
        { status: "verification_sent", devLink: "http://localhost/dev-link" }
      );
      expect(signupResponseBody(signupDecision("new"), "http://localhost/dev-link")).toEqual(
        { status: "verification_sent", devLink: "http://localhost/dev-link" }
      );
      // …but verified / Google-only accounts never get a mail, so no devLink either.
      expect(signupResponseBody(signupDecision("verified"), "http://localhost/dev-link")).toEqual(
        { status: "verification_sent" }
      );
      expect(signupResponseBody(signupDecision("google_only"), "http://localhost/dev-link")).toEqual(
        { status: "verification_sent" }
      );
    });
  });

  describe("login folds Google-only accounts into invalid_credentials", () => {
    test("a Google-only account is indistinguishable from a missing one", () => {
      for (const passwordMatches of [true, false]) {
        expect(loginDecision("google_only", passwordMatches)).toEqual({ kind: "invalid_credentials" });
        expect(loginDecision("none", passwordMatches)).toEqual({ kind: "invalid_credentials" });
      }
    });

    test("a wrong password stays invalid_credentials for local accounts", () => {
      expect(loginDecision("local", false)).toEqual({ kind: "invalid_credentials" });
      expect(loginDecision("unverified_local", false)).toEqual({ kind: "invalid_credentials" });
    });

    test("email_not_verified is reserved for correct-password unverified accounts", () => {
      expect(loginDecision("unverified_local", true)).toEqual({ kind: "email_not_verified" });
      // Never reachable for a missing or Google-only account, even with a "match":
      // an attacker without the password can never get this code.
      expect(loginDecision("google_only", true)).toEqual({ kind: "invalid_credentials" });
      expect(loginDecision("none", true)).toEqual({ kind: "invalid_credentials" });
    });

    test("only a verified local account with the right password signs in", () => {
      expect(loginDecision("local", true)).toEqual({ kind: "sign_in" });
      expect(loginDecision("local", false)).toEqual({ kind: "invalid_credentials" });
    });
  });

  describe("revealing codes are never emitted by auth routes", () => {
    test("the discontinued codes still exist in the vocabulary for compatibility", () => {
      for (const code of DISCONTINUED_ENUMERATION_CODES) {
        expect(isAuthErrorCode(code)).toBe(true);
      }
    });

    test("no auth route source contains any discontinued enumeration code", () => {
      const routes = [
        "signup/route.ts",
        "login/route.ts",
        "forgot-password/route.ts",
        "reset-password/route.ts",
        "resend-verification/route.ts",
      ];

      for (const route of routes) {
        const source = readAuthRouteSource(route);
        for (const code of DISCONTINUED_ENUMERATION_CODES) {
          expect(source.includes(code)).toBe(false);
        }
      }
    });

    test("the login 403 carries no extra fields that would set it apart", () => {
      // The `email_not_verified` failure body must be a plain `{ error }` —
      // no `email` field — so its shape matches every other failure.
      // (Guard: the login route must not pass an `email` extra into authError.)
      const loginSource = readAuthRouteSource("login/route.ts");
      expect(loginSource.includes('email_not_verified", 403, { email')).toBe(false);
    });
  });
});
