import { describe, it, expect } from "vitest";

import {
	generateToken,
	hashToken,
	generateTokenWithHash,
	generateSessionId,
	generateAuthSecret,
	secureCompare,
	computeS256Challenge,
	encrypt,
	decrypt,
} from "./tokens.js";

const BASE64URL_REGEX = /^[A-Za-z0-9_-]+$/;
const NO_PADDING_REGEX = /^[A-Za-z0-9_-]+$/;

describe("tokens", () => {
	describe("generateToken", () => {
		it("generates a base64url-encoded token", () => {
			const token = generateToken();
			expect(token).toMatch(BASE64URL_REGEX);
			// 32 bytes = 43 base64url characters (without padding)
			expect(token.length).toBe(43);
		});

		it("generates unique tokens", () => {
			// eslint-disable-next-line e18e/prefer-array-fill -- We need unique tokens, not the same token repeated
			const tokens = new Set(Array.from({ length: 100 }, () => generateToken()));
			expect(tokens.size).toBe(100);
		});
	});

	describe("hashToken", () => {
		it("produces consistent hashes", () => {
			const token = generateToken();
			const hash1 = hashToken(token);
			const hash2 = hashToken(token);
			expect(hash1).toBe(hash2);
		});

		it("produces different hashes for different tokens", () => {
			const token1 = generateToken();
			const token2 = generateToken();
			expect(hashToken(token1)).not.toBe(hashToken(token2));
		});
	});

	describe("generateTokenWithHash", () => {
		it("returns both token and hash", () => {
			const { token, hash } = generateTokenWithHash();
			expect(token).toBeDefined();
			expect(hash).toBeDefined();
			expect(hashToken(token)).toBe(hash);
		});
	});

	describe("generateSessionId", () => {
		it("generates a shorter session ID", () => {
			const sessionId = generateSessionId();
			expect(sessionId).toMatch(BASE64URL_REGEX);
			// 20 bytes = 27 base64url characters
			expect(sessionId.length).toBe(27);
		});
	});

	describe("generateAuthSecret", () => {
		it("generates a 32-byte secret", () => {
			const secret = generateAuthSecret();
			expect(secret).toMatch(BASE64URL_REGEX);
			expect(secret.length).toBe(43);
		});
	});

	describe("secureCompare", () => {
		it("returns true for equal strings", () => {
			expect(secureCompare("hello", "hello")).toBe(true);
		});

		it("returns false for different strings", () => {
			expect(secureCompare("hello", "world")).toBe(false);
		});

		it("returns false for different length strings", () => {
			expect(secureCompare("hello", "hello!")).toBe(false);
		});
	});

	describe("computeS256Challenge", () => {
		it("produces correct S256 challenge for a known verifier", () => {
			// RFC 7636 Appendix B test vector:
			// verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
			// expected challenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
			const challenge = computeS256Challenge("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk");
			expect(challenge).toBe("E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM");
		});

		it("produces base64url output without padding", () => {
			const challenge = computeS256Challenge("test-verifier-string");
			expect(challenge).toMatch(NO_PADDING_REGEX);
			expect(challenge).not.toContain("=");
		});

		it("is deterministic", () => {
			const a = computeS256Challenge("same-input");
			const b = computeS256Challenge("same-input");
			expect(a).toBe(b);
		});

		it("produces different output for different input", () => {
			const a = computeS256Challenge("verifier-one");
			const b = computeS256Challenge("verifier-two");
			expect(a).not.toBe(b);
		});
	});

	describe("encrypt/decrypt", () => {
		const secret = generateAuthSecret();

		it("encrypts and decrypts a string", async () => {
			const plaintext = "my-secret-value";
			const encrypted = await encrypt(plaintext, secret);
			const decrypted = await decrypt(encrypted, secret);
			expect(decrypted).toBe(plaintext);
		});

		it("produces different ciphertext each time (due to random IV)", async () => {
			const plaintext = "my-secret-value";
			const encrypted1 = await encrypt(plaintext, secret);
			const encrypted2 = await encrypt(plaintext, secret);
			expect(encrypted1).not.toBe(encrypted2);
		});

		it("fails to decrypt with wrong secret", async () => {
			const plaintext = "my-secret-value";
			const encrypted = await encrypt(plaintext, secret);
			const wrongSecret = generateAuthSecret();
			await expect(decrypt(encrypted, wrongSecret)).rejects.toThrow();
		});
	});
});
