import { describe, it, expect, beforeAll, afterAll } from "vitest";
import bcrypt from "bcryptjs";
import { buildApp, extractCookie } from "./helpers";
import prisma from "../config/database";
import { config } from "../config/env";

let app: Awaited<ReturnType<typeof buildApp>>;

// Fixture prefix so cleanup never touches real data.
const N = "auth_test_";
const TEST_USERNAME = `${N}user`;
const TEST_PASSWORD = "Sup3r-Secret-Pw!";

let testUserId: number;

/** Pull every Set-Cookie header line for a given cookie name (raw, with attrs). */
function rawSetCookie(res: { headers: Record<string, any> }, name: string) {
  const cookies = res.headers["set-cookie"];
  if (!cookies) return undefined;
  const arr = Array.isArray(cookies) ? cookies : [cookies];
  return arr.find((c: string) => c.startsWith(`${name}=`));
}

beforeAll(async () => {
  app = await buildApp();

  // Clean up any leftover fixture from a previous failed run, plus its tokens.
  const leftovers = await prisma.users.findMany({
    where: { username: { startsWith: N } },
    select: { id: true },
  });
  if (leftovers.length) {
    await prisma.refresh_tokens.deleteMany({
      where: { user_id: { in: leftovers.map((u) => u.id) } },
    });
    await prisma.users.deleteMany({ where: { username: { startsWith: N } } });
  }

  const role = await prisma.roles.findFirst();
  if (!role) throw new Error("Test setup: no roles found — seed the database");

  // Hash the known password with the SAME bcrypt cost the app uses so login's
  // bcrypt.compare succeeds.
  const passwordHash = await bcrypt.hash(
    TEST_PASSWORD,
    config.security.bcryptCost,
  );

  const user = await prisma.users.create({
    data: {
      username: TEST_USERNAME,
      email: `${N}user@test.local`,
      password_hash: passwordHash,
      first_name: "Auth",
      last_name: "Test",
      is_active: true,
      totp_enabled: false,
      role_id: role.id,
    },
  });
  testUserId = user.id;
});

afterAll(async () => {
  await prisma.refresh_tokens
    .deleteMany({ where: { user_id: testUserId } })
    .catch(() => {});
  await prisma.users
    .deleteMany({ where: { username: { startsWith: N } } })
    .catch(() => {});
  await app.close();
});

describe("POST /api/admin/login", () => {
  it("returns 401 for invalid credentials", async () => {
    const res = await app.inject({
      method: "POST",
      url: "/api/admin/login",
      payload: { username: "nonexistent", password: "wrong" },
    });
    expect(res.statusCode).toBe(401);
    expect(res.json().success).toBe(false);
  });

  it("returns 400 for missing fields", async () => {
    const res = await app.inject({
      method: "POST",
      url: "/api/admin/login",
      payload: {},
    });
    expect(res.statusCode).toBe(400);
  });

  it("returns 401 for a valid user with the wrong password", async () => {
    const res = await app.inject({
      method: "POST",
      url: "/api/admin/login",
      payload: { username: TEST_USERNAME, password: "definitely-wrong" },
    });
    expect(res.statusCode).toBe(401);
    expect(res.json().success).toBe(false);
  });

  it("returns 200 with an access token and a refresh cookie on valid credentials", async () => {
    const res = await app.inject({
      method: "POST",
      url: "/api/admin/login",
      payload: { username: TEST_USERNAME, password: TEST_PASSWORD },
    });
    expect(res.statusCode).toBe(200);
    const body = res.json();
    expect(body.success).toBe(true);
    expect(typeof body.data.access_token).toBe("string");
    expect(body.data.access_token.length).toBeGreaterThan(0);
    expect(body.data.expires_in).toBe(config.jwt.accessTokenExpiry);
    expect(body.data.user.userId).toBe(testUserId);
    expect(body.data.user.username).toBe(TEST_USERNAME);

    // The refresh token must be set as an httpOnly, SameSite=Strict cookie.
    const cookie = extractCookie(res, "refresh_token");
    expect(cookie).toBeTruthy();
    const rawCookie = rawSetCookie(res, "refresh_token")!;
    expect(rawCookie).toMatch(/HttpOnly/i);
    expect(rawCookie).toMatch(/SameSite=Strict/i);
  });
});

describe("POST /api/admin/refresh", () => {
  it("returns 401 without refresh token", async () => {
    const res = await app.inject({
      method: "POST",
      url: "/api/admin/refresh",
    });
    expect(res.statusCode).toBe(401);
  });

  it("returns 401 for an invalid/unknown refresh token", async () => {
    const res = await app.inject({
      method: "POST",
      url: "/api/admin/refresh",
      cookies: { refresh_token: "not-a-real-token" },
    });
    expect(res.statusCode).toBe(401);
    expect(res.json().success).toBe(false);
  });

  it("rotates a valid refresh cookie into a new access token + new refresh cookie", async () => {
    // First log in to obtain a valid refresh cookie.
    const loginRes = await app.inject({
      method: "POST",
      url: "/api/admin/login",
      payload: { username: TEST_USERNAME, password: TEST_PASSWORD },
    });
    expect(loginRes.statusCode).toBe(200);
    const originalRefresh = extractCookie(loginRes, "refresh_token")!;
    expect(originalRefresh).toBeTruthy();

    const refreshRes = await app.inject({
      method: "POST",
      url: "/api/admin/refresh",
      cookies: { refresh_token: originalRefresh },
    });
    expect(refreshRes.statusCode).toBe(200);
    const body = refreshRes.json();
    expect(body.success).toBe(true);
    expect(typeof body.data.access_token).toBe("string");
    expect(body.data.user.userId).toBe(testUserId);

    // The refresh token must be ROTATED: a new cookie value, different from
    // the one we presented.
    const rotated = extractCookie(refreshRes, "refresh_token");
    expect(rotated).toBeTruthy();
    expect(rotated).not.toBe(originalRefresh);

    // Reuse-detection: presenting the now-consumed original token must fail.
    const reuseRes = await app.inject({
      method: "POST",
      url: "/api/admin/refresh",
      cookies: { refresh_token: originalRefresh },
    });
    expect(reuseRes.statusCode).toBe(401);
  });
});

describe("POST /api/admin/logout", () => {
  it("clears the refresh token cookie", async () => {
    // Log in so logout has a real token to delete.
    const loginRes = await app.inject({
      method: "POST",
      url: "/api/admin/login",
      payload: { username: TEST_USERNAME, password: TEST_PASSWORD },
    });
    const refresh = extractCookie(loginRes, "refresh_token")!;

    const res = await app.inject({
      method: "POST",
      url: "/api/admin/logout",
      cookies: { refresh_token: refresh },
    });
    expect(res.statusCode).toBe(200);

    // The response must actively clear the cookie — an expiry in the past
    // and/or an empty value (clearCookie sets Max-Age=0 / Expires in the past).
    const raw = rawSetCookie(res, "refresh_token");
    expect(raw).toBeTruthy();
    const cleared =
      /Max-Age=0/i.test(raw!) ||
      /Expires=Thu, 01 Jan 1970/i.test(raw!) ||
      /^refresh_token=;/.test(raw!);
    expect(cleared).toBe(true);

    // The deleted token must no longer be usable for refresh.
    const reuseRes = await app.inject({
      method: "POST",
      url: "/api/admin/refresh",
      cookies: { refresh_token: refresh },
    });
    expect(reuseRes.statusCode).toBe(401);
  });
});
