import { beforeEach, describe, expect, it, mock } from "bun:test";
import { createHash, randomBytes } from "node:crypto";
import { Hono } from "hono";
import { oauthRoutes, wellKnownRoute } from "../../src/server/routes/oauth.ts";
import { createTestDb } from "../helpers/db.ts";

// Mock @hono/oidc-auth — must be before importing routes
const mockGetAuth = mock(() => null as any);
mock.module("@hono/oidc-auth", () => ({
	getAuth: mockGetAuth,
	oidcAuthMiddleware: () => async (_c: any, next: any) => next(),
	processOAuthCallback: async (c: any) => c.json({ ok: true }),
	revokeSession: async () => {},
}));

async function createTestApp() {
	const { db, userId } = await createTestDb();
	const app = new Hono<{ Variables: { db?: any; userId?: number } }>();
	app.use("*", async (c, next) => {
		c.set("db", db);
		c.set("userId", userId);
		await next();
	});
	app.route("/.well-known", wellKnownRoute);
	app.route("/oauth", oauthRoutes);
	return { app, db, userId };
}

function generatePkce() {
	const verifier = randomBytes(32).toString("hex");
	const challenge = createHash("sha256").update(verifier).digest("base64url");
	return { verifier, challenge };
}

describe("OAuth Routes", () => {
	let app: Hono;
	let _db: Awaited<ReturnType<typeof createTestDb>>["db"];
	let _userId: number;

	beforeEach(async () => {
		const testApp = await createTestApp();
		app = testApp.app;
		_db = testApp.db;
		_userId = testApp.userId;
		mockGetAuth.mockReset();
		// Default: user is authenticated via OIDC
		mockGetAuth.mockReturnValue({
			sub: "test-user-logto-sub",
			email: "admin@example.com",
		});
	});

	describe("GET /.well-known/oauth-authorization-server", () => {
		it("returns 200 with correct metadata fields", async () => {
			const res = await app.request("/.well-known/oauth-authorization-server");
			expect(res.status).toBe(200);

			const body = await res.json();
			expect(body.issuer).toBeDefined();
			expect(body.authorization_endpoint).toContain("/oauth/authorize");
			expect(body.token_endpoint).toContain("/oauth/token");
			expect(body.registration_endpoint).toContain("/oauth/register");
			expect(body.response_types_supported).toEqual(["code"]);
			expect(body.grant_types_supported).toEqual([
				"authorization_code",
				"refresh_token",
			]);
			expect(body.code_challenge_methods_supported).toEqual(["S256"]);
			expect(body.token_endpoint_auth_methods_supported).toEqual(["none"]);
		});
	});

	describe("POST /oauth/register", () => {
		it("returns 201 with client_id, client_name, redirect_uris", async () => {
			const res = await app.request("/oauth/register", {
				method: "POST",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({
					client_name: "Test Client",
					redirect_uris: ["http://localhost:3000/callback"],
				}),
			});

			expect(res.status).toBe(201);
			const body = await res.json();
			expect(body.client_id).toBeDefined();
			expect(body.client_name).toBe("Test Client");
			expect(body.redirect_uris).toEqual(["http://localhost:3000/callback"]);
		});

		it("returns 400 without redirect_uris", async () => {
			const res = await app.request("/oauth/register", {
				method: "POST",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({ client_name: "Test Client" }),
			});

			expect(res.status).toBe(400);
		});
	});

	describe("GET /oauth/authorize", () => {
		it("returns 200 HTML with consent form when OIDC session exists", async () => {
			const regRes = await app.request("/oauth/register", {
				method: "POST",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({
					client_name: "Test Client",
					redirect_uris: ["http://localhost:3000/callback"],
				}),
			});
			const { client_id } = await regRes.json();
			const { challenge } = generatePkce();

			const params = new URLSearchParams({
				response_type: "code",
				client_id,
				redirect_uri: "http://localhost:3000/callback",
				code_challenge: challenge,
				code_challenge_method: "S256",
				state: "abc123",
			});

			const res = await app.request(`/oauth/authorize?${params}`);
			expect(res.status).toBe(200);

			const html = await res.text();
			expect(html).toContain("GearBox");
			expect(html).toContain("Authorize");
			expect(html).toContain("Test Client");
		});

		it("redirects to /login when no OIDC session", async () => {
			mockGetAuth.mockReturnValue(null);

			const regRes = await app.request("/oauth/register", {
				method: "POST",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({
					client_name: "Test Client",
					redirect_uris: ["http://localhost:3000/callback"],
				}),
			});
			const { client_id } = await regRes.json();
			const { challenge } = generatePkce();

			const params = new URLSearchParams({
				response_type: "code",
				client_id,
				redirect_uri: "http://localhost:3000/callback",
				code_challenge: challenge,
				code_challenge_method: "S256",
				state: "abc123",
			});

			const res = await app.request(`/oauth/authorize?${params}`, {
				redirect: "manual",
			});
			expect(res.status).toBe(302);
			const location = res.headers.get("location");
			expect(location).toContain("/login");
		});

		it("returns 400 with invalid client_id", async () => {
			const { challenge } = generatePkce();
			const params = new URLSearchParams({
				response_type: "code",
				client_id: "nonexistent",
				redirect_uri: "http://localhost:3000/callback",
				code_challenge: challenge,
				code_challenge_method: "S256",
				state: "abc123",
			});

			const res = await app.request(`/oauth/authorize?${params}`);
			expect(res.status).toBe(400);
		});
	});

	describe("Full OAuth flow", () => {
		it("register -> authorize (consent) -> token exchange", async () => {
			const regRes = await app.request("/oauth/register", {
				method: "POST",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({
					client_name: "Test Client",
					redirect_uris: ["http://localhost:3000/callback"],
				}),
			});
			const { client_id } = await regRes.json();
			const { verifier, challenge } = generatePkce();

			const formBody = new URLSearchParams({
				client_id,
				redirect_uri: "http://localhost:3000/callback",
				code_challenge: challenge,
				code_challenge_method: "S256",
				state: "mystate",
			});

			const authRes = await app.request("/oauth/authorize", {
				method: "POST",
				headers: { "Content-Type": "application/x-www-form-urlencoded" },
				body: formBody.toString(),
				redirect: "manual",
			});

			expect(authRes.status).toBe(302);
			const location = authRes.headers.get("location")!;
			expect(location).toContain("code=");
			expect(location).toContain("state=mystate");

			const redirectUrl = new URL(location);
			const code = redirectUrl.searchParams.get("code")!;

			const tokenBody = new URLSearchParams({
				grant_type: "authorization_code",
				code,
				code_verifier: verifier,
				client_id,
				redirect_uri: "http://localhost:3000/callback",
			});

			const tokenRes = await app.request("/oauth/token", {
				method: "POST",
				headers: { "Content-Type": "application/x-www-form-urlencoded" },
				body: tokenBody.toString(),
			});

			expect(tokenRes.status).toBe(200);
			const tokens = await tokenRes.json();
			expect(tokens.access_token).toBeDefined();
			expect(tokens.refresh_token).toBeDefined();
			expect(tokens.token_type).toBe("Bearer");
			expect(tokens.expires_in).toBe(3600);
		});
	});

	describe("Token refresh", () => {
		it("exchanges refresh token for new tokens", async () => {
			const regRes = await app.request("/oauth/register", {
				method: "POST",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({
					client_name: "Test Client",
					redirect_uris: ["http://localhost:3000/callback"],
				}),
			});
			const { client_id } = await regRes.json();
			const { verifier, challenge } = generatePkce();

			const formBody = new URLSearchParams({
				client_id,
				redirect_uri: "http://localhost:3000/callback",
				code_challenge: challenge,
				code_challenge_method: "S256",
				state: "s",
			});

			const authRes = await app.request("/oauth/authorize", {
				method: "POST",
				headers: { "Content-Type": "application/x-www-form-urlencoded" },
				body: formBody.toString(),
				redirect: "manual",
			});

			const location = authRes.headers.get("location")!;
			const code = new URL(location).searchParams.get("code")!;

			const tokenBody = new URLSearchParams({
				grant_type: "authorization_code",
				code,
				code_verifier: verifier,
				client_id,
				redirect_uri: "http://localhost:3000/callback",
			});

			const tokenRes = await app.request("/oauth/token", {
				method: "POST",
				headers: { "Content-Type": "application/x-www-form-urlencoded" },
				body: tokenBody.toString(),
			});
			const tokens = await tokenRes.json();

			const refreshBody = new URLSearchParams({
				grant_type: "refresh_token",
				refresh_token: tokens.refresh_token,
				client_id,
			});

			const refreshRes = await app.request("/oauth/token", {
				method: "POST",
				headers: { "Content-Type": "application/x-www-form-urlencoded" },
				body: refreshBody.toString(),
			});

			expect(refreshRes.status).toBe(200);
			const newTokens = await refreshRes.json();
			expect(newTokens.access_token).toBeDefined();
			expect(newTokens.refresh_token).toBeDefined();
			expect(newTokens.token_type).toBe("Bearer");
			expect(newTokens.expires_in).toBe(3600);
			expect(newTokens.access_token).not.toBe(tokens.access_token);
		});
	});
});
