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

import { BylineRepository } from "../../../src/database/repositories/byline.js";
import { ContentRepository } from "../../../src/database/repositories/content.js";
import { UserRepository } from "../../../src/database/repositories/user.js";
import type { Database } from "../../../src/database/types.js";
import { setupTestDatabaseWithCollections, teardownTestDatabase } from "../../utils/test-db.js";

// Mock the loader's getDb to return our test database
vi.mock("../../../src/loader.js", () => ({
	getDb: vi.fn(),
}));

import {
	getByline,
	getBylineBySlug,
	getEntryBylines,
	getBylinesForEntries,
} from "../../../src/bylines/index.js";
import { getDb } from "../../../src/loader.js";

describe("Byline query functions", () => {
	let db: Kysely<Database>;
	let bylineRepo: BylineRepository;
	let contentRepo: ContentRepository;

	beforeEach(async () => {
		db = await setupTestDatabaseWithCollections();
		bylineRepo = new BylineRepository(db);
		contentRepo = new ContentRepository(db);
		vi.mocked(getDb).mockResolvedValue(db);
	});

	afterEach(async () => {
		await teardownTestDatabase(db);
		vi.restoreAllMocks();
	});

	describe("getByline", () => {
		it("returns a byline by ID", async () => {
			const created = await bylineRepo.create({
				slug: "jane-doe",
				displayName: "Jane Doe",
			});

			const result = await getByline(created.id);

			expect(result).not.toBeNull();
			expect(result?.id).toBe(created.id);
			expect(result?.displayName).toBe("Jane Doe");
			expect(result?.slug).toBe("jane-doe");
		});

		it("returns null for non-existent ID", async () => {
			const result = await getByline("non-existent");
			expect(result).toBeNull();
		});
	});

	describe("getBylineBySlug", () => {
		it("returns a byline by slug", async () => {
			await bylineRepo.create({
				slug: "john-smith",
				displayName: "John Smith",
			});

			const result = await getBylineBySlug("john-smith");

			expect(result).not.toBeNull();
			expect(result?.displayName).toBe("John Smith");
		});

		it("returns null for non-existent slug", async () => {
			const result = await getBylineBySlug("nobody");
			expect(result).toBeNull();
		});
	});

	describe("getEntryBylines", () => {
		it("returns explicit byline credits for an entry", async () => {
			const lead = await bylineRepo.create({
				slug: "lead-author",
				displayName: "Lead Author",
			});
			const editor = await bylineRepo.create({
				slug: "editor",
				displayName: "Editor",
			});

			const post = await contentRepo.create({
				type: "post",
				slug: "my-post",
				data: { title: "My Post" },
			});

			await bylineRepo.setContentBylines("post", post.id, [
				{ bylineId: lead.id },
				{ bylineId: editor.id, roleLabel: "Contributing Editor" },
			]);

			const bylines = await getEntryBylines("post", post.id);

			expect(bylines).toHaveLength(2);
			expect(bylines[0]?.byline.displayName).toBe("Lead Author");
			expect(bylines[0]?.sortOrder).toBe(0);
			expect(bylines[0]?.source).toBe("explicit");
			expect(bylines[1]?.byline.displayName).toBe("Editor");
			expect(bylines[1]?.roleLabel).toBe("Contributing Editor");
			expect(bylines[1]?.source).toBe("explicit");
		});

		it("falls back to user-linked byline when no explicit credits", async () => {
			// Create a user
			const userRepo = new UserRepository(db);
			const user = await userRepo.create({
				email: "author@example.com",
				displayName: "Author User",
				role: "editor",
			});

			// Create a byline linked to the user
			await bylineRepo.create({
				slug: "author-user",
				displayName: "Author User",
				userId: user.id,
			});

			// Create a post with this user as author, no explicit bylines
			const post = await contentRepo.create({
				type: "post",
				slug: "authored-post",
				data: { title: "Authored Post" },
				authorId: user.id,
			});

			const bylines = await getEntryBylines("post", post.id);

			expect(bylines).toHaveLength(1);
			expect(bylines[0]?.byline.displayName).toBe("Author User");
			expect(bylines[0]?.source).toBe("inferred");
			expect(bylines[0]?.roleLabel).toBeNull();
		});

		it("returns empty array when no bylines and no author fallback", async () => {
			const post = await contentRepo.create({
				type: "post",
				slug: "no-author-post",
				data: { title: "No Author" },
			});

			const bylines = await getEntryBylines("post", post.id);
			expect(bylines).toHaveLength(0);
		});
	});

	describe("getBylinesForEntries", () => {
		it("batch-fetches byline credits for multiple entries", async () => {
			const author1 = await bylineRepo.create({
				slug: "author-one",
				displayName: "Author One",
			});
			const author2 = await bylineRepo.create({
				slug: "author-two",
				displayName: "Author Two",
			});

			const post1 = await contentRepo.create({
				type: "post",
				slug: "post-1",
				data: { title: "Post 1" },
			});
			const post2 = await contentRepo.create({
				type: "post",
				slug: "post-2",
				data: { title: "Post 2" },
			});
			const post3 = await contentRepo.create({
				type: "post",
				slug: "post-3",
				data: { title: "Post 3" },
			});

			await bylineRepo.setContentBylines("post", post1.id, [{ bylineId: author1.id }]);
			await bylineRepo.setContentBylines("post", post2.id, [
				{ bylineId: author1.id },
				{ bylineId: author2.id, roleLabel: "Contributor" },
			]);
			// post3 has no bylines

			const result = await getBylinesForEntries("post", [post1.id, post2.id, post3.id]);

			expect(result.get(post1.id)).toHaveLength(1);
			expect(result.get(post1.id)?.[0]?.byline.displayName).toBe("Author One");
			expect(result.get(post1.id)?.[0]?.source).toBe("explicit");

			expect(result.get(post2.id)).toHaveLength(2);
			expect(result.get(post2.id)?.[0]?.byline.displayName).toBe("Author One");
			expect(result.get(post2.id)?.[1]?.byline.displayName).toBe("Author Two");
			expect(result.get(post2.id)?.[1]?.roleLabel).toBe("Contributor");

			expect(result.get(post3.id)).toHaveLength(0);
		});

		it("returns inferred bylines for entries without explicit credits", async () => {
			const userRepo = new UserRepository(db);
			const user = await userRepo.create({
				email: "batch-author@example.com",
				displayName: "Batch Author",
				role: "editor",
			});

			await bylineRepo.create({
				slug: "batch-author",
				displayName: "Batch Author",
				userId: user.id,
			});

			const post = await contentRepo.create({
				type: "post",
				slug: "batch-post",
				data: { title: "Batch Post" },
				authorId: user.id,
			});

			const result = await getBylinesForEntries("post", [post.id]);

			expect(result.get(post.id)).toHaveLength(1);
			expect(result.get(post.id)?.[0]?.source).toBe("inferred");
			expect(result.get(post.id)?.[0]?.byline.displayName).toBe("Batch Author");
		});

		it("returns empty map for empty input", async () => {
			const result = await getBylinesForEntries("post", []);
			expect(result.size).toBe(0);
		});
	});
});
