import { beforeEach, describe, expect, it } from "bun:test";
import { createHash, randomBytes } from "node:crypto";
import {
	createAuthorizationCode,
	exchangeCode,
	getClient,
	refreshAccessToken,
	registerClient,
	verifyAccessToken,
} from "../../src/server/services/oauth.service.ts";
import { createTestDb } from "../helpers/db.ts";

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

describe("OAuth Service", () => {
	let db: any;
	let userId: number;

	beforeEach(async () => {
		({ db, userId } = await createTestDb());
	});

	describe("Client Registration", () => {
		it("registers a client and returns clientId (string, non-empty)", async () => {
			const result = await registerClient(db, "Test App", [
				"http://localhost:8080/callback",
			]);

			expect(result).toBeDefined();
			expect(typeof result.clientId).toBe("string");
			expect(result.clientId.length).toBeGreaterThan(0);
		});

		it("getClient returns registered client with correct clientName and redirectUris (JSON parsed)", async () => {
			const redirectUris = ["http://localhost:8080/callback"];
			const { clientId } = await registerClient(db, "Test App", redirectUris);

			const client = await getClient(db, clientId);

			expect(client).not.toBeNull();
			expect(client!.clientName).toBe("Test App");
			expect(JSON.parse(client!.redirectUris)).toEqual(redirectUris);
		});

		it("getClient returns null for unknown client", async () => {
			const client = await getClient(db, "unknown-client-id");

			expect(client).toBeNull();
		});
	});

	describe("Authorization Code + PKCE", () => {
		it("creates an authorization code (non-empty)", async () => {
			const { clientId } = await registerClient(db, "Test App", [
				"http://localhost:8080/callback",
			]);
			const { challenge } = generatePkce();

			const result = await createAuthorizationCode(
				db,
				clientId,
				userId,
				challenge,
				"S256",
				"http://localhost:8080/callback",
			);

			expect(result).toBeDefined();
			expect(typeof result.code).toBe("string");
			expect(result.code.length).toBeGreaterThan(0);
		});

		it("exchanges code for tokens with valid PKCE verifier (returns accessToken, refreshToken, expiresIn=3600)", async () => {
			const { clientId } = await registerClient(db, "Test App", [
				"http://localhost:8080/callback",
			]);
			const { verifier, challenge } = generatePkce();

			const { code } = await createAuthorizationCode(
				db,
				clientId,
				userId,
				challenge,
				"S256",
				"http://localhost:8080/callback",
			);

			const tokens = await exchangeCode(
				db,
				code,
				verifier,
				clientId,
				"http://localhost:8080/callback",
			);

			expect(tokens).not.toBeNull();
			expect(typeof tokens!.accessToken).toBe("string");
			expect(tokens!.accessToken.length).toBeGreaterThan(0);
			expect(typeof tokens!.refreshToken).toBe("string");
			expect(tokens!.refreshToken.length).toBeGreaterThan(0);
			expect(tokens!.expiresIn).toBe(3600);
		});

		it("rejects code exchange with wrong PKCE verifier (returns null)", async () => {
			const { clientId } = await registerClient(db, "Test App", [
				"http://localhost:8080/callback",
			]);
			const { challenge } = generatePkce();

			const { code } = await createAuthorizationCode(
				db,
				clientId,
				userId,
				challenge,
				"S256",
				"http://localhost:8080/callback",
			);

			const tokens = await exchangeCode(
				db,
				code,
				"wrongverifier",
				clientId,
				"http://localhost:8080/callback",
			);

			expect(tokens).toBeNull();
		});

		it("rejects code exchange with wrong redirect_uri (returns null)", async () => {
			const { clientId } = await registerClient(db, "Test App", [
				"http://localhost:8080/callback",
			]);
			const { verifier, challenge } = generatePkce();

			const { code } = await createAuthorizationCode(
				db,
				clientId,
				userId,
				challenge,
				"S256",
				"http://localhost:8080/callback",
			);

			const tokens = await exchangeCode(
				db,
				code,
				verifier,
				clientId,
				"http://localhost:9999/wrong",
				userId,
			);

			expect(tokens).toBeNull();
		});

		it("rejects replayed code - single use (second exchange returns null)", async () => {
			const { clientId } = await registerClient(db, "Test App", [
				"http://localhost:8080/callback",
			]);
			const { verifier, challenge } = generatePkce();

			const { code } = await createAuthorizationCode(
				db,
				clientId,
				userId,
				challenge,
				"S256",
				"http://localhost:8080/callback",
			);

			// First exchange succeeds
			const first = await exchangeCode(
				db,
				code,
				verifier,
				clientId,
				"http://localhost:8080/callback",
			);
			expect(first).not.toBeNull();

			// Second exchange is rejected
			const second = await exchangeCode(
				db,
				code,
				verifier,
				clientId,
				"http://localhost:8080/callback",
			);
			expect(second).toBeNull();
		});
	});

	describe("Token Verification", () => {
		it("verifies a valid access token returns { userId }", async () => {
			const { clientId } = await registerClient(db, "Test App", [
				"http://localhost:8080/callback",
			]);
			const { verifier, challenge } = generatePkce();

			const { code } = await createAuthorizationCode(
				db,
				clientId,
				userId,
				challenge,
				"S256",
				"http://localhost:8080/callback",
			);

			const tokens = await exchangeCode(
				db,
				code,
				verifier,
				clientId,
				"http://localhost:8080/callback",
			);

			const verified = await verifyAccessToken(db, tokens!.accessToken);
			expect(verified).not.toBeNull();
			expect(verified?.userId).toBe(userId);
		});

		it("rejects an unknown token (returns null)", async () => {
			const verified = await verifyAccessToken(db, "unknowntoken12345678");
			expect(verified).toBeNull();
		});
	});

	describe("Token Refresh", () => {
		it("refreshes a valid refresh token and returns new tokens (different accessToken)", async () => {
			const { clientId } = await registerClient(db, "Test App", [
				"http://localhost:8080/callback",
			]);
			const { verifier, challenge } = generatePkce();

			const { code } = await createAuthorizationCode(
				db,
				clientId,
				userId,
				challenge,
				"S256",
				"http://localhost:8080/callback",
			);

			const tokens = await exchangeCode(
				db,
				code,
				verifier,
				clientId,
				"http://localhost:8080/callback",
			);

			const newTokens = await refreshAccessToken(
				db,
				tokens!.refreshToken,
				clientId,
			);

			expect(newTokens).not.toBeNull();
			expect(newTokens!.accessToken).not.toBe(tokens!.accessToken);
			expect(typeof newTokens!.refreshToken).toBe("string");
			expect(newTokens!.expiresIn).toBe(3600);
		});

		it("rejects refresh with wrong clientId (returns null)", async () => {
			const { clientId } = await registerClient(db, "Test App", [
				"http://localhost:8080/callback",
			]);
			const { verifier, challenge } = generatePkce();

			const { code } = await createAuthorizationCode(
				db,
				clientId,
				userId,
				challenge,
				"S256",
				"http://localhost:8080/callback",
			);

			const tokens = await exchangeCode(
				db,
				code,
				verifier,
				clientId,
				"http://localhost:8080/callback",
			);

			const newTokens = await refreshAccessToken(
				db,
				tokens!.refreshToken,
				"wrong-client-id",
				userId,
			);

			expect(newTokens).toBeNull();
		});
	});
});
