import { test, expect } from "bun:test";
import {
  createContext,
  hashPassword,
  verifyPassword,
  logIn,
  logOut,
  getUser,
  sessionAuth,
  requireAuth,
} from "../src/index.ts";

function ctx(method = "GET", path = "/", accept?: string) {
  const headers: Record<string, string> = {};
  if (accept) headers.accept = accept;
  const url = new URL(`http://x${path}`);
  const req = new Request(url, { method, headers });
  return createContext(req, url);
}

test("hashPassword / verifyPassword round-trip", async () => {
  const hash = await hashPassword("correct horse battery staple");
  expect(hash).toBeTruthy();
  expect(hash).not.toBe("correct horse battery staple");
  expect(await verifyPassword("correct horse battery staple", hash)).toBe(true);
  expect(await verifyPassword("wrong", hash)).toBe(false);
});

test("verifyPassword tolerates empty/garbage hashes", async () => {
  expect(await verifyPassword("x", "")).toBe(false);
  expect(await verifyPassword("x", "not-a-real-hash")).toBe(false);
});

test("logIn stores the user; getUser reads it; logOut clears it", () => {
  const c = ctx();
  expect(getUser(c)).toBeNull();
  logIn(c, { id: 1, email: "a@b.com" });
  expect(getUser<{ id: number }>(c)?.id).toBe(1);
  expect(c.session.get<{ id: number; email: string }>("user")).toEqual({ id: 1, email: "a@b.com" });
  logOut(c);
  expect(getUser(c)).toBeNull();
  expect(c.user).toBeNull();
});

test("logIn regenerates the session id (fixation defense) but keeps data", () => {
  const c = ctx();
  c.session.set("cart", [1, 2]);
  const before = c.session.id();
  logIn(c, { id: 1, email: "a@b.com" });
  const after = c.session.id();
  expect(after).not.toBe(before); // fresh id issued on login
  expect(after.length).toBeGreaterThanOrEqual(32);
  expect(c.session.get<number[]>("cart")).toEqual([1, 2]); // data preserved
  expect(getUser<{ id: number }>(c)?.id).toBe(1);
});

test("sessionAuth hydrates ctx.user from the session", async () => {
  const c = ctx();
  c.session.set("user", { id: 7 });
  let seen: unknown = "unset";
  await sessionAuth()(c, () => {
    seen = c.user;
    return new Response("ok");
  });
  expect(seen).toEqual({ id: 7 });
});

test("requireAuth: passes through when authenticated", async () => {
  const c = ctx();
  logIn(c, { id: 1 });
  const res = await requireAuth()(c, () => new Response("secret"));
  expect(await res.text()).toBe("secret");
});

test("requireAuth: 401 JSON for API paths when anonymous", async () => {
  const c = ctx("GET", "/api/me");
  const res = await requireAuth()(c, () => new Response("secret"));
  expect(res.status).toBe(401);
  expect(await res.json()).toEqual({ ok: false, error: "Unauthorized" });
});

test("requireAuth: 302 redirect for page navigations when anonymous", async () => {
  const c = ctx("GET", "/dashboard?tab=1", "text/html");
  const res = await requireAuth()(c, () => new Response("secret"));
  expect(res.status).toBe(302);
  expect(res.headers.get("location")).toBe("/login?next=%2Fdashboard%3Ftab%3D1");
});

test("requireAuth: custom loginPath", async () => {
  const c = ctx("GET", "/dashboard", "text/html");
  const res = await requireAuth({ loginPath: "/signin" })(c, () => new Response("x"));
  expect(res.headers.get("location")).toBe("/signin?next=%2Fdashboard");
});
