/**
 * Integration tests for API token handlers.
 *
 * Tests token CRUD and resolution against a real in-memory SQLite database.
 */

import type { Kysely } from "kysely";
import { describe, it, expect, beforeEach, afterEach } from "vitest";

import {
	handleApiTokenCreate,
	handleApiTokenList,
	handleApiTokenRevoke,
	resolveApiToken,
	resolveOAuthToken,
} from "../../../src/api/handlers/api-tokens.js";
import { generatePrefixedToken, TOKEN_PREFIXES } from "../../../src/auth/api-tokens.js";
import type { Database } from "../../../src/database/types.js";
import { setupTestDatabase } from "../../utils/test-db.js";

// Regex patterns for token validation
const PAT_PREFIX_REGEX = /^ec_pat_/;

let db: Kysely<Database>;

beforeEach(async () => {
	db = await setupTestDatabase();

	// Create a test user
	await db
		.insertInto("users")
		.values({
			id: "user_1",
			email: "admin@test.com",
			name: "Admin",
			role: 50, // ADMIN
			email_verified: 1,
		})
		.execute();
});

afterEach(async () => {
	await db.destroy();
});

describe("handleApiTokenCreate", () => {
	it("creates a token and returns the raw value", async () => {
		const result = await handleApiTokenCreate(db, "user_1", {
			name: "Test Token",
			scopes: ["content:read", "content:write"],
		});

		expect(result.success).toBe(true);
		expect(result.data).toBeDefined();
		expect(result.data!.token).toMatch(PAT_PREFIX_REGEX);
		expect(result.data!.info.name).toBe("Test Token");
		expect(result.data!.info.scopes).toEqual(["content:read", "content:write"]);
		expect(result.data!.info.userId).toBe("user_1");
		expect(result.data!.info.prefix).toMatch(PAT_PREFIX_REGEX);
	});

	it("creates tokens with different hashes", async () => {
		const result1 = await handleApiTokenCreate(db, "user_1", {
			name: "Token 1",
			scopes: ["content:read"],
		});
		const result2 = await handleApiTokenCreate(db, "user_1", {
			name: "Token 2",
			scopes: ["content:read"],
		});

		expect(result1.data!.token).not.toBe(result2.data!.token);
	});

	it("stores expiry date when provided", async () => {
		const expiresAt = new Date(Date.now() + 86400000).toISOString();
		const result = await handleApiTokenCreate(db, "user_1", {
			name: "Expiring Token",
			scopes: ["content:read"],
			expiresAt,
		});

		expect(result.data!.info.expiresAt).toBe(expiresAt);
	});
});

describe("handleApiTokenList", () => {
	it("lists tokens for a user", async () => {
		await handleApiTokenCreate(db, "user_1", {
			name: "Token A",
			scopes: ["content:read"],
		});
		await handleApiTokenCreate(db, "user_1", {
			name: "Token B",
			scopes: ["admin"],
		});

		const result = await handleApiTokenList(db, "user_1");

		expect(result.success).toBe(true);
		expect(result.data!.items).toHaveLength(2);
		const names = result.data!.items.map((t) => t.name).toSorted();
		expect(names).toEqual(["Token A", "Token B"]);
	});

	it("does not return tokens for other users", async () => {
		await db
			.insertInto("users")
			.values({
				id: "user_2",
				email: "other@test.com",
				name: "Other",
				role: 50,
				email_verified: 1,
			})
			.execute();

		await handleApiTokenCreate(db, "user_1", {
			name: "User 1 Token",
			scopes: ["content:read"],
		});
		await handleApiTokenCreate(db, "user_2", {
			name: "User 2 Token",
			scopes: ["content:read"],
		});

		const result = await handleApiTokenList(db, "user_1");
		expect(result.data!.items).toHaveLength(1);
		expect(result.data!.items[0].name).toBe("User 1 Token");
	});

	it("never returns the token hash", async () => {
		await handleApiTokenCreate(db, "user_1", {
			name: "Test",
			scopes: ["content:read"],
		});

		const result = await handleApiTokenList(db, "user_1");
		const item = result.data!.items[0];

		// Ensure no hash or raw token is exposed
		expect(item).not.toHaveProperty("token_hash");
		expect(item).not.toHaveProperty("tokenHash");
		expect(item).not.toHaveProperty("token");
	});
});

describe("handleApiTokenRevoke", () => {
	it("revokes a token", async () => {
		const createResult = await handleApiTokenCreate(db, "user_1", {
			name: "To Revoke",
			scopes: ["content:read"],
		});
		const tokenId = createResult.data!.info.id;

		const result = await handleApiTokenRevoke(db, tokenId, "user_1");
		expect(result.success).toBe(true);

		// Should be gone from the list
		const list = await handleApiTokenList(db, "user_1");
		expect(list.data!.items).toHaveLength(0);
	});

	it("returns error for non-existent token", async () => {
		const result = await handleApiTokenRevoke(db, "nonexistent", "user_1");
		expect(result.success).toBe(false);
		expect(result.error!.code).toBe("NOT_FOUND");
	});

	it("cannot revoke another user's token", async () => {
		await db
			.insertInto("users")
			.values({
				id: "user_2",
				email: "other@test.com",
				name: "Other",
				role: 50,
				email_verified: 1,
			})
			.execute();

		const createResult = await handleApiTokenCreate(db, "user_1", {
			name: "User 1 Token",
			scopes: ["content:read"],
		});
		const tokenId = createResult.data!.info.id;

		// User 2 tries to revoke user 1's token
		const result = await handleApiTokenRevoke(db, tokenId, "user_2");
		expect(result.success).toBe(false);
		expect(result.error!.code).toBe("NOT_FOUND");

		// Token should still exist
		const list = await handleApiTokenList(db, "user_1");
		expect(list.data!.items).toHaveLength(1);
	});
});

describe("resolveApiToken", () => {
	it("resolves a valid token to user and scopes", async () => {
		const createResult = await handleApiTokenCreate(db, "user_1", {
			name: "Test",
			scopes: ["content:read", "media:write"],
		});
		const rawToken = createResult.data!.token;

		const resolved = await resolveApiToken(db, rawToken);
		expect(resolved).not.toBeNull();
		expect(resolved!.userId).toBe("user_1");
		expect(resolved!.scopes).toEqual(["content:read", "media:write"]);
	});

	it("returns null for invalid token", async () => {
		const resolved = await resolveApiToken(db, "ec_pat_invalidtoken123");
		expect(resolved).toBeNull();
	});

	it("returns null for expired token", async () => {
		const pastDate = new Date(Date.now() - 86400000).toISOString(); // Yesterday
		const createResult = await handleApiTokenCreate(db, "user_1", {
			name: "Expired",
			scopes: ["content:read"],
			expiresAt: pastDate,
		});
		const rawToken = createResult.data!.token;

		const resolved = await resolveApiToken(db, rawToken);
		expect(resolved).toBeNull();
	});

	it("resolves non-expired token", async () => {
		const futureDate = new Date(Date.now() + 86400000).toISOString(); // Tomorrow
		const createResult = await handleApiTokenCreate(db, "user_1", {
			name: "Future",
			scopes: ["admin"],
			expiresAt: futureDate,
		});
		const rawToken = createResult.data!.token;

		const resolved = await resolveApiToken(db, rawToken);
		expect(resolved).not.toBeNull();
		expect(resolved!.scopes).toEqual(["admin"]);
	});
});

describe("resolveOAuthToken", () => {
	it("resolves a valid OAuth access token", async () => {
		// Insert directly since we don't have a Device Flow handler yet
		const { raw, hash } = generatePrefixedToken(TOKEN_PREFIXES.OAUTH_ACCESS);
		const futureDate = new Date(Date.now() + 3600000).toISOString();

		await db
			.insertInto("_emdash_oauth_tokens")
			.values({
				token_hash: hash,
				token_type: "access",
				user_id: "user_1",
				scopes: JSON.stringify(["content:read"]),
				client_type: "cli",
				expires_at: futureDate,
			})
			.execute();

		const resolved = await resolveOAuthToken(db, raw);
		expect(resolved).not.toBeNull();
		expect(resolved!.userId).toBe("user_1");
		expect(resolved!.scopes).toEqual(["content:read"]);
	});

	it("returns null for expired OAuth token", async () => {
		const { raw, hash } = generatePrefixedToken(TOKEN_PREFIXES.OAUTH_ACCESS);
		const pastDate = new Date(Date.now() - 3600000).toISOString();

		await db
			.insertInto("_emdash_oauth_tokens")
			.values({
				token_hash: hash,
				token_type: "access",
				user_id: "user_1",
				scopes: JSON.stringify(["content:read"]),
				client_type: "cli",
				expires_at: pastDate,
			})
			.execute();

		const resolved = await resolveOAuthToken(db, raw);
		expect(resolved).toBeNull();
	});

	it("does not resolve refresh tokens", async () => {
		const { raw, hash } = generatePrefixedToken(TOKEN_PREFIXES.OAUTH_REFRESH);
		const futureDate = new Date(Date.now() + 3600000).toISOString();

		await db
			.insertInto("_emdash_oauth_tokens")
			.values({
				token_hash: hash,
				token_type: "refresh",
				user_id: "user_1",
				scopes: JSON.stringify(["content:read"]),
				client_type: "cli",
				expires_at: futureDate,
			})
			.execute();

		const resolved = await resolveOAuthToken(db, raw);
		expect(resolved).toBeNull();
	});
});
