import test from "node:test";
import assert from "node:assert/strict";
import request from "supertest";
import { createApp } from "../server/app.js";
import { root, password, setup, signup } from "./helpers.js";

test("strict CSP/static modules, no-store data, auth lifecycle, and ownership share one database", async () => {
	const { db, app, agent: first } = await setup();
	const second = request.agent(app);
	const anonymous = request.agent(app);

	const marketing = await anonymous.get("/");
	assert.equal(marketing.status, 200);
	assert.match(
		marketing.headers["content-security-policy"],
		/default-src 'self'/,
	);
	assert.doesNotMatch(
		marketing.headers["content-security-policy"],
		/(?:default-src|script-src)[^;]*unsafe-inline/,
	);
	assert.match(marketing.text, /Get started/);

	const login = await anonymous.get("/login");
	assert.equal(login.status, 200);
	assert.match(
		login.text,
		/<script type="module" src="\/js\/login\.js\?v=[^"]+"><\/script>/,
	);
	assert.equal((await anonymous.get("/signup")).status, 200);
	assert.equal((await anonymous.get("/forgot")).status, 200);
	assert.equal((await anonymous.get("/reset")).status, 200);
	assert.equal((await anonymous.get("/api/auth/signup-enabled")).body.enabled, true);

	// Page routes redirect a signed-out browser navigation to /login (never a bare JSON 401 —
	// that's only for /api/* callers) but the API underneath stays strictly 401/no-store.
	const adminHtml = await anonymous.get("/admin");
	assert.equal(adminHtml.status, 302);
	assert.equal(adminHtml.headers.location, "/login");
	assert.equal(adminHtml.headers["cache-control"], "no-store, private");
	const accountHtml = await anonymous.get("/account");
	assert.equal(accountHtml.status, 302);
	assert.equal(accountHtml.headers.location, "/login");
	const appHtml = await anonymous.get("/app");
	assert.equal(appHtml.status, 302);
	assert.equal(appHtml.headers.location, "/login");
	assert.equal((await anonymous.get("/api/plans")).status, 401);
	assert.equal(
		(await anonymous.get("/api/plans")).headers["cache-control"],
		"no-store, private",
	);
	// Protected HTML shells are never reachable by static filename, only through their routes —
	// including percent-encoded and repeated-slash variants that bypass an undecoded string
	// comparison but still resolve to the same file once express.static decodes them.
	assert.equal((await anonymous.get("/index.html")).status, 302);
	assert.equal((await anonymous.get("/admin.html")).status, 302);
	assert.equal((await anonymous.get("/account.html")).status, 302);
	assert.equal((await anonymous.get("/%69ndex.html")).status, 302);
	assert.equal((await anonymous.get("//index.html")).status, 302);

	assert.match(
		(await anonymous.get("/js/admin.js")).text,
		/loadNavUser/,
	);
	const mainScript = await anonymous.get("/js/main.js");
	assert.match(mainScript.text, /roastPlannerPlan\.v2/);
	assert.match(mainScript.text, /localStorage\.removeItem\(key\)/);
	const serviceWorker = (await anonymous.get("/sw.js")).text;
	assert.match(serviceWorker, /data-free authenticated shell/);
	assert.match(
		serviceWorker,
		/if \(url\.pathname !== "\/app"\) return;[\s\S]*caches\.match\("\/app"\)/,
	);
	assert.match(serviceWorker, /logout clears that namespace/);

	const one = await signup(first, "one@example.com");
	const two = await signup(second, "two@example.com");
	assert.equal(one.response.status, 201);
	assert.equal(two.response.status, 201);

	// A signed-in visitor is bounced off the public auth pages straight to /app.
	assert.equal((await first.get("/login")).status, 302);
	assert.equal((await first.get("/")).status, 302);

	const plan = await first
		.post("/api/plans")
		.set("x-csrf-token", one.csrf)
		.send({ plan: { fields: { 0.1: "Private" } } });
	assert.equal(plan.status, 201);
	assert.equal(
		(await first.get("/api/plans")).headers["cache-control"],
		"no-store, private",
	);
	assert.equal(
		(
			await second
				.put(`/api/plans/${plan.body.plan.id}`)
				.set("x-csrf-token", two.csrf)
				.send({ plan: {} })
		).status,
		404,
	);
	// A non-UUID id is a clean 404, not a 500 from the database driver.
	assert.equal(
		(
			await first
				.put("/api/plans/not-a-uuid")
				.set("x-csrf-token", one.csrf)
				.send({ plan: {} })
		).status,
		404,
	);
	// PUT validates the plan body just like POST does.
	assert.equal(
		(
			await first
				.put(`/api/plans/${plan.body.plan.id}`)
				.set("x-csrf-token", one.csrf)
				.send({ plan: null })
		).status,
		400,
	);
	assert.equal((await second.get("/api/plans")).body.plans.length, 0);
	// A signed-in non-admin visiting /admin lands back in the app, not a bare 403 JSON page.
	const nonAdminVisitsAdmin = await second.get("/admin");
	assert.equal(nonAdminVisitsAdmin.status, 302);
	assert.equal(nonAdminVisitsAdmin.headers.location, "/app");
	assert.equal(
		(
			await second
				.delete(`/api/plans/${plan.body.plan.id}`)
				.set("x-csrf-token", two.csrf)
		).status,
		404,
	);
	assert.equal(
		(
			await first
				.delete(`/api/plans/${plan.body.plan.id}`)
				.set("x-csrf-token", one.csrf)
		).status,
		200,
	);
	assert.equal((await first.get("/api/plans")).body.plans.length, 0);

	const loginAgent = request.agent(app);
	assert.equal(
		(
			await loginAgent
				.post("/api/auth/login")
				.send({ email: "one@example.com", password })
		).status,
		200,
	);
	assert.equal((await loginAgent.get("/api/auth/me")).status, 200);
	// Logout succeeds even without a valid CSRF token (it can only end the caller's own session).
	assert.equal((await loginAgent.post("/api/auth/logout")).status, 200);
	assert.equal((await loginAgent.get("/api/auth/me")).status, 401);

	const admin = await first.post("/api/auth/bootstrap").send({
		email: "snowspeeder@gmail.com",
		password,
		setupToken: "a-secure-bootstrap-token",
	});
	assert.equal(admin.status, 201);
	assert.equal((await first.get("/api/admin/users")).status, 200);
	assert.equal((await second.get("/api/admin/users")).status, 403);
	assert.equal(
		(
			await first
				.put("/api/admin/signup-enabled")
				.set("x-csrf-token", admin.body.csrfToken)
				.send({ enabled: false })
		).status,
		200,
	);
	assert.equal(
		(
			await anonymous
				.post("/api/auth/signup")
				.send({ email: "blocked@example.com", password })
		).status,
		403,
	);
	assert.equal(
		(await db.query("SELECT count(*)::int AS count FROM users")).rows[0].count,
		3,
	);
});

test("bootstrap token is optional after first setup and unavailable before setup without one", async () => {
	const { app, db } = await setup();
	const agent = request.agent(app);
	assert.equal(
		(
			await agent.post("/api/auth/bootstrap").send({
				email: "snowspeeder@gmail.com",
				password,
				setupToken: "wrong",
			})
		).status,
		403,
	);
	const noTokenApp = createApp({ db, root, env: { NODE_ENV: "test" } });
	assert.equal(
		(
			await request(noTokenApp).post("/api/auth/bootstrap").send({
				email: "snowspeeder@gmail.com",
				password,
			})
		).status,
		503,
	);
	await db.query(
		"INSERT INTO users(email,password_hash,role) VALUES($1,$2,'admin')",
		["snowspeeder@gmail.com", "not-used-in-this-test"],
	);
	assert.equal(
		(
			await request(noTokenApp).post("/api/auth/bootstrap").send({
				email: "snowspeeder@gmail.com",
				password,
			})
		).status,
		409,
	);
});

test("/setup redirects to /login once the administrator exists", async () => {
	const { app } = await setup();
	const agent = request.agent(app);
	assert.equal((await agent.get("/setup")).status, 200);
	await agent.post("/api/auth/bootstrap").send({
		email: "snowspeeder@gmail.com",
		password,
		setupToken: "a-secure-bootstrap-token",
	});
	const fresh = request.agent(app);
	const setupResponse = await fresh.get("/setup");
	assert.equal(setupResponse.status, 302);
	assert.equal(setupResponse.headers.location, "/login");
});

test("forgot/reset password issues a working link without leaking account existence", async () => {
	const { app } = await setup();
	const memberAgent = request.agent(app);
	await signup(memberAgent, "reset-me@example.com");
	const admin = request.agent(app);
	await admin.post("/api/auth/bootstrap").send({
		email: "snowspeeder@gmail.com",
		password,
		setupToken: "a-secure-bootstrap-token",
	});

	const unknown = await request(app)
		.post("/api/auth/forgot")
		.send({ email: "nobody@example.com" });
	assert.equal(unknown.status, 200);
	const known = await request(app)
		.post("/api/auth/forgot")
		.send({ email: "reset-me@example.com" });
	assert.equal(known.status, 200);
	assert.deepEqual(known.body, unknown.body);

	const pending = await admin.get("/api/admin/password-resets");
	assert.equal(pending.status, 200);
	const entry = pending.body.links.find((l) => l.email === "reset-me@example.com");
	assert.ok(entry, "expected a pending reset link for reset-me@example.com");
	const token = new URL(entry.url, "http://x").searchParams.get("token");

	assert.equal(
		(
			await request(app)
				.post("/api/auth/reset")
				.send({ token: "wrong-token", password: "another long password" })
		).status,
		400,
	);
	const resetResponse = await request(app)
		.post("/api/auth/reset")
		.send({ token, password: "another long password" });
	assert.equal(resetResponse.status, 200);
	// The old password no longer works; the new one does.
	assert.equal(
		(
			await request(app)
				.post("/api/auth/login")
				.send({ email: "reset-me@example.com", password })
		).status,
		401,
	);
	assert.equal(
		(
			await request(app)
				.post("/api/auth/login")
				.send({ email: "reset-me@example.com", password: "another long password" })
		).status,
		200,
	);
});

test("account: email change, password change, sessions, and self-delete", async () => {
	const { app } = await setup();
	const agent = request.agent(app);
	const { csrf } = await signup(agent, "acct@example.com");

	assert.equal(
		(
			await agent
				.put("/api/account/email")
				.set("x-csrf-token", csrf)
				.send({ email: "new-acct@example.com", password: "wrong password wrong" })
		).status,
		401,
	);
	assert.equal(
		(
			await agent
				.put("/api/account/email")
				.set("x-csrf-token", csrf)
				.send({ email: "new-acct@example.com", password })
		).status,
		200,
	);

	const sessionsBefore = await agent.get("/api/account/sessions");
	assert.equal(sessionsBefore.body.sessions.length, 1);
	assert.equal(sessionsBefore.body.sessions[0].current, true);

	assert.equal(
		(
			await agent
				.put("/api/account/password")
				.set("x-csrf-token", csrf)
				.send({ currentPassword: password, newPassword: "yet another long one" })
		).status,
		200,
	);
	// Logging in with the old password now fails; the new one works.
	assert.equal(
		(
			await request(app)
				.post("/api/auth/login")
				.send({ email: "new-acct@example.com", password })
		).status,
		401,
	);
	const relogin = request.agent(app);
	assert.equal(
		(
			await relogin
				.post("/api/auth/login")
				.send({ email: "new-acct@example.com", password: "yet another long one" })
		).status,
		200,
	);

	assert.equal(
		(
			await agent
				.delete("/api/account")
				.set("x-csrf-token", csrf)
				.send({ password: "yet another long one" })
		).status,
		200,
	);
	assert.equal(
		(
			await request(app)
				.post("/api/auth/login")
				.send({ email: "new-acct@example.com", password: "yet another long one" })
		).status,
		401,
	);
});

test("the bootstrap admin's email is reserved and fixed, and the bootstrap admin cannot delete themself even as a second admin exists", async () => {
	const { app } = await setup();

	// Nobody can claim the reserved email via plain signup before bootstrap ever runs.
	assert.equal(
		(
			await request(app)
				.post("/api/auth/signup")
				.send({ email: "snowspeeder@gmail.com", password })
		).status,
		409,
	);

	const bootstrapAgent = request.agent(app);
	const admin = await bootstrapAgent.post("/api/auth/bootstrap").send({
		email: "snowspeeder@gmail.com",
		password,
		setupToken: "a-secure-bootstrap-token",
	});
	assert.equal(admin.status, 201);

	// Someone else still can't claim it after bootstrap (this would also just 409 on the
	// column's UNIQUE constraint, but the reserved-email check must fire first regardless).
	const otherAgent = request.agent(app);
	const { csrf: otherCsrf } = await signup(otherAgent, "other@example.com");
	assert.equal(
		(
			await otherAgent
				.put("/api/account/email")
				.set("x-csrf-token", otherCsrf)
				.send({ email: "snowspeeder@gmail.com", password })
		).status,
		403,
	);

	// The bootstrap admin can't change away from the reserved email either — that would strip
	// every other guard's protection for this account.
	assert.equal(
		(
			await bootstrapAgent
				.put("/api/account/email")
				.set("x-csrf-token", admin.body.csrfToken)
				.send({ email: "someone-else@example.com", password })
		).status,
		403,
	);

	// Promote a second admin, then confirm the bootstrap admin still can't delete themself even
	// though the "last admin" count check alone would otherwise allow it.
	const secondAgent = request.agent(app);
	await signup(secondAgent, "second-admin@example.com");
	const secondId = (
		await bootstrapAgent.get("/api/admin/users")
	).body.users.find((u) => u.email === "second-admin@example.com").id;
	await bootstrapAgent
		.put(`/api/admin/users/${secondId}/role`)
		.set("x-csrf-token", admin.body.csrfToken)
		.send({ role: "admin" });
	assert.equal(
		(
			await bootstrapAgent
				.delete("/api/account")
				.set("x-csrf-token", admin.body.csrfToken)
				.send({ password })
		).status,
		403,
	);
	assert.equal((await bootstrapAgent.get("/api/auth/me")).status, 200);
});

test("password-reset links for admin accounts never appear in the shared admin panel", async () => {
	const { app } = await setup();
	const bootstrapAgent = request.agent(app);
	const admin = await bootstrapAgent.post("/api/auth/bootstrap").send({
		email: "snowspeeder@gmail.com",
		password,
		setupToken: "a-secure-bootstrap-token",
	});
	const secondAgent = request.agent(app);
	await signup(secondAgent, "second-admin@example.com");
	const secondId = (
		await bootstrapAgent.get("/api/admin/users")
	).body.users.find((u) => u.email === "second-admin@example.com").id;
	await bootstrapAgent
		.put(`/api/admin/users/${secondId}/role`)
		.set("x-csrf-token", admin.body.csrfToken)
		.send({ role: "admin" });

	// The second admin requests a reset for the bootstrap admin's own account...
	await request(app)
		.post("/api/auth/forgot")
		.send({ email: "snowspeeder@gmail.com" });
	// ...but that link must never surface in the panel any admin (including this one) can read —
	// otherwise a second admin could take over the account every other guard protects.
	const pending = await secondAgent.get("/api/admin/password-resets");
	assert.equal(pending.status, 200);
	assert.equal(
		pending.body.links.some((l) => l.email === "snowspeeder@gmail.com"),
		false,
	);
});

test("admin: metrics, role changes, disable, delete, and audit trail", async () => {
	const { app } = await setup();
	const adminAgent = request.agent(app);
	const admin = await adminAgent.post("/api/auth/bootstrap").send({
		email: "snowspeeder@gmail.com",
		password,
		setupToken: "a-secure-bootstrap-token",
	});
	const userAgent = request.agent(app);
	await signup(userAgent, "member@example.com");
	const userId = (
		await adminAgent.get("/api/admin/users")
	).body.users.find((u) => u.email === "member@example.com").id;

	const metrics = await adminAgent.get("/api/admin/metrics");
	assert.equal(metrics.status, 200);
	assert.equal(metrics.body.metrics.totalUsers, 2);

	// The bootstrap admin cannot be demoted, disabled, or deleted by another admin, or by itself.
	const bootstrapId = (
		await adminAgent.get("/api/admin/users")
	).body.users.find((u) => u.email === "snowspeeder@gmail.com").id;
	assert.equal(
		(
			await adminAgent
				.put(`/api/admin/users/${bootstrapId}/role`)
				.set("x-csrf-token", admin.body.csrfToken)
				.send({ role: "user" })
		).status,
		403,
	);

	assert.equal(
		(
			await adminAgent
				.put(`/api/admin/users/${userId}/role`)
				.set("x-csrf-token", admin.body.csrfToken)
				.send({ role: "admin" })
		).status,
		200,
	);
	assert.equal(
		(
			await adminAgent
				.put(`/api/admin/users/${userId}/disabled`)
				.set("x-csrf-token", admin.body.csrfToken)
				.send({ disabled: true })
		).status,
		200,
	);
	// A disabled user's existing session stops working and cannot log back in.
	assert.equal((await userAgent.get("/api/auth/me")).status, 401);
	assert.equal(
		(
			await request(app)
				.post("/api/auth/login")
				.send({ email: "member@example.com", password })
		).status,
		401,
	);

	const audit = await adminAgent.get("/api/admin/audit");
	assert.equal(audit.status, 200);
	assert.ok(audit.body.events.some((e) => e.action === "role_changed"));
	assert.ok(audit.body.events.some((e) => e.action === "user_disabled"));

	assert.equal(
		(
			await adminAgent
				.delete(`/api/admin/users/${userId}`)
				.set("x-csrf-token", admin.body.csrfToken)
		).status,
		200,
	);
	assert.equal((await adminAgent.get("/api/admin/metrics")).body.metrics.totalUsers, 1);
});

test("login is locked out per-email after repeated failures, independent of source IP", async () => {
	// Uses distinct simulated client IPs (via a trust-proxy app instance) so the assertion
	// isolates the per-email lockout from the separate per-IP rate limiter.
	const { db } = await setup();
	const app = createApp({
		db,
		root,
		env: {
			NODE_ENV: "test",
			BOOTSTRAP_SETUP_TOKEN: "a-secure-bootstrap-token",
			TRUST_PROXY: true,
		},
	});
	await signup(request.agent(app), "lockout@example.com");
	for (let i = 0; i < 10; i++) {
		await request(app)
			.post("/api/auth/login")
			.set("X-Forwarded-For", `10.0.0.${i}`)
			.send({ email: "lockout@example.com", password: "wrong password wrong" });
	}
	// Further wrong guesses are locked out...
	const stillWrong = await request(app)
		.post("/api/auth/login")
		.set("X-Forwarded-For", "10.0.0.99")
		.send({ email: "lockout@example.com", password: "still not it either" });
	assert.equal(stillWrong.status, 429);
	assert.equal(stillWrong.body.code, "too_many_attempts");
	// ...but the lock never blocks the real owner: the correct password always gets them in,
	// so the lockout can only ever throttle guessing, never be weaponized to deny the owner.
	const legitimateLogin = await request(app)
		.post("/api/auth/login")
		.set("X-Forwarded-For", "10.0.0.100")
		.send({ email: "lockout@example.com", password });
	assert.equal(legitimateLogin.status, 200);
});
