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

import { createDatabase } from "../../../src/database/connection.js";
import { runMigrations } from "../../../src/database/migrations/runner.js";
import { ContentRepository } from "../../../src/database/repositories/content.js";
import { EmDashValidationError } from "../../../src/database/repositories/types.js";
import type { Database } from "../../../src/database/types.js";
import { SchemaRegistry } from "../../../src/schema/registry.js";

describe("ContentRepository", () => {
	let db: Kysely<Database>;
	let repo: ContentRepository;
	let registry: SchemaRegistry;

	beforeEach(async () => {
		// Fresh in-memory database for each test
		db = createDatabase({ url: ":memory:" });
		await runMigrations(db);
		repo = new ContentRepository(db);
		registry = new SchemaRegistry(db);

		// Create collections needed for tests (this creates ec_post and ec_page tables)
		await registry.createCollection({
			slug: "post",
			label: "Posts",
			labelSingular: "Post",
		});
		await registry.createCollection({
			slug: "page",
			label: "Pages",
			labelSingular: "Page",
		});

		// Add fields to both collections
		await registry.createField("post", {
			slug: "title",
			label: "Title",
			type: "string",
		});
		await registry.createField("post", {
			slug: "content",
			label: "Content",
			type: "portableText",
		});
		await registry.createField("page", {
			slug: "title",
			label: "Title",
			type: "string",
		});
	});

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

	describe("create", () => {
		it("should create content with minimal data", async () => {
			const content = await repo.create({
				type: "post",
				data: { title: "Test Post" },
			});

			expect(content.id).toBeDefined();
			expect(content.type).toBe("post");
			expect(content.data).toEqual({ title: "Test Post" });
			expect(content.status).toBe("draft");
			expect(content.createdAt).toBeDefined();
			expect(content.updatedAt).toBeDefined();
		});

		it("should create content with all fields", async () => {
			const content = await repo.create({
				type: "post",
				slug: "test-post",
				data: { title: "Test Post", content: "Body" },
				status: "published",
				authorId: "author-1",
			});

			expect(content.id).toBeDefined();
			expect(content.type).toBe("post");
			expect(content.slug).toBe("test-post");
			expect(content.data).toEqual({ title: "Test Post", content: "Body" });
			expect(content.status).toBe("published");
			expect(content.authorId).toBe("author-1");
		});

		it("should throw validation error when type is missing", async () => {
			await expect(
				repo.create({
					type: "",
					data: { title: "Test" },
				}),
			).rejects.toThrow(EmDashValidationError);
		});

		it("should throw error for duplicate type+slug", async () => {
			await repo.create({
				type: "post",
				slug: "duplicate-slug",
				data: { title: "First" },
			});

			await expect(
				repo.create({
					type: "post",
					slug: "duplicate-slug",
					data: { title: "Second" },
				}),
			).rejects.toThrow();
		});

		it("should allow same slug for different types", async () => {
			await repo.create({
				type: "post",
				slug: "same-slug",
				data: { title: "Post" },
			});

			await expect(
				repo.create({
					type: "page",
					slug: "same-slug",
					data: { title: "Page" },
				}),
			).resolves.not.toThrow();
		});

		it("should allow null slug", async () => {
			const content = await repo.create({
				type: "post",
				slug: null,
				data: { title: "No slug" },
			});

			expect(content.slug).toBeNull();
		});

		it("should default status to draft", async () => {
			const content = await repo.create({
				type: "post",
				data: { title: "Test" },
			});

			expect(content.status).toBe("draft");
		});

		it("should generate unique ID", async () => {
			const content1 = await repo.create({
				type: "post",
				data: { title: "First" },
			});

			const content2 = await repo.create({
				type: "post",
				data: { title: "Second" },
			});

			expect(content1.id).not.toBe(content2.id);
		});

		it("should store complex nested data in JSON columns", async () => {
			// Portable Text content is stored as JSON
			const portableTextContent = [
				{
					_type: "block",
					style: "normal",
					children: [{ _type: "span", text: "Hello world" }],
				},
				{
					_type: "block",
					style: "h1",
					children: [{ _type: "span", text: "Heading", marks: ["bold"] }],
				},
			];

			const content = await repo.create({
				type: "post",
				data: {
					title: "Complex Post",
					content: portableTextContent,
				},
			});

			expect(content.data.title).toBe("Complex Post");
			expect(content.data.content).toEqual(portableTextContent);
		});
	});

	describe("findById", () => {
		it("should find content by ID", async () => {
			const created = await repo.create({
				type: "post",
				data: { title: "Test" },
			});

			const found = await repo.findById("post", created.id);

			expect(found).not.toBeNull();
			expect(found!.id).toBe(created.id);
			expect(found!.data).toEqual(created.data);
		});

		it("should return null for non-existent ID", async () => {
			const found = await repo.findById("post", "non-existent-id");
			expect(found).toBeNull();
		});

		it("should return null when type doesn't match", async () => {
			const created = await repo.create({
				type: "post",
				data: { title: "Test" },
			});

			const found = await repo.findById("page", created.id);
			expect(found).toBeNull();
		});

		it("should not find soft-deleted content", async () => {
			const created = await repo.create({
				type: "post",
				data: { title: "Test" },
			});

			await repo.delete("post", created.id);

			const found = await repo.findById("post", created.id);
			expect(found).toBeNull();
		});
	});

	describe("findBySlug", () => {
		it("should find content by slug", async () => {
			await repo.create({
				type: "post",
				slug: "test-slug",
				data: { title: "Test" },
			});

			const found = await repo.findBySlug("post", "test-slug");

			expect(found).not.toBeNull();
			expect(found!.slug).toBe("test-slug");
		});

		it("should return null for non-existent slug", async () => {
			const found = await repo.findBySlug("post", "non-existent");
			expect(found).toBeNull();
		});

		it("should return correct content when same slug exists for different types", async () => {
			await repo.create({
				type: "post",
				slug: "shared-slug",
				data: { title: "Post" },
			});

			await repo.create({
				type: "page",
				slug: "shared-slug",
				data: { title: "Page" },
			});

			const post = await repo.findBySlug("post", "shared-slug");
			const page = await repo.findBySlug("page", "shared-slug");

			expect(post!.type).toBe("post");
			expect(post!.data.title).toBe("Post");
			expect(page!.type).toBe("page");
			expect(page!.data.title).toBe("Page");
		});

		it("should not find soft-deleted content", async () => {
			const created = await repo.create({
				type: "post",
				slug: "test-slug",
				data: { title: "Test" },
			});

			await repo.delete("post", created.id);

			const found = await repo.findBySlug("post", "test-slug");
			expect(found).toBeNull();
		});
	});

	describe("findMany", () => {
		beforeEach(async () => {
			// Create test data
			for (let i = 0; i < 5; i++) {
				await repo.create({
					type: "post",
					slug: `post-${i}`,
					data: { title: `Post ${i}` },
					status: i % 2 === 0 ? "published" : "draft",
					authorId: i < 3 ? "author-1" : "author-2",
				});
			}
		});

		it("should return all content by default", async () => {
			const result = await repo.findMany("post");

			expect(result.items).toHaveLength(5);
		});

		it("should filter by status", async () => {
			const result = await repo.findMany("post", {
				where: { status: "published" },
			});

			expect(result.items).toHaveLength(3);
			expect(result.items.every((item) => item.status === "published")).toBe(true);
		});

		it("should filter by authorId", async () => {
			const result = await repo.findMany("post", {
				where: { authorId: "author-1" },
			});

			expect(result.items).toHaveLength(3);
			expect(result.items.every((item) => item.authorId === "author-1")).toBe(true);
		});

		it("should filter by both status and authorId", async () => {
			const result = await repo.findMany("post", {
				where: {
					status: "published",
					authorId: "author-1",
				},
			});

			expect(result.items).toHaveLength(2);
		});

		it("should apply limit", async () => {
			const result = await repo.findMany("post", { limit: 2 });

			expect(result.items).toHaveLength(2);
		});

		it("should support cursor pagination", async () => {
			const page1 = await repo.findMany("post", { limit: 2 });
			expect(page1.items).toHaveLength(2);
			expect(page1.nextCursor).toBeDefined();

			const page2 = await repo.findMany("post", {
				limit: 2,
				cursor: page1.nextCursor,
			});
			expect(page2.items).toHaveLength(2);

			// Items should be different
			const page1Ids = page1.items.map((i) => i.id);
			const page2Ids = page2.items.map((i) => i.id);
			expect(page1Ids).not.toEqual(page2Ids);
		});

		it("should not include nextCursor when no more items", async () => {
			const result = await repo.findMany("post", { limit: 10 });

			expect(result.items).toHaveLength(5);
			expect(result.nextCursor).toBeUndefined();
		});

		it("should order by createdAt desc by default", async () => {
			const result = await repo.findMany("post");

			// Items should be in descending order (newest first)
			for (let i = 1; i < result.items.length; i++) {
				expect(result.items[i - 1].createdAt >= result.items[i].createdAt).toBe(true);
			}
		});

		it("should support custom ordering", async () => {
			const result = await repo.findMany("post", {
				orderBy: {
					field: "createdAt",
					direction: "asc",
				},
			});

			// Items should be in ascending order (oldest first)
			for (let i = 1; i < result.items.length; i++) {
				expect(result.items[i - 1].createdAt <= result.items[i].createdAt).toBe(true);
			}
		});

		it("should default limit to 50", async () => {
			// Create more than 50 items
			for (let i = 0; i < 60; i++) {
				await repo.create({
					type: "page",
					data: { title: `Page ${i}` },
				});
			}

			const result = await repo.findMany("page");

			expect(result.items.length).toBeLessThanOrEqual(50);
		});

		it("should cap limit at 100", async () => {
			const result = await repo.findMany("post", { limit: 200 });

			// Even with limit: 200, should not return more than 100
			expect(result.items.length).toBeLessThanOrEqual(100);
		});

		it("should not include soft-deleted content", async () => {
			const toDelete = await repo.create({
				type: "post",
				data: { title: "To Delete" },
			});

			await repo.delete("post", toDelete.id);

			const result = await repo.findMany("post");

			expect(result.items.every((item) => item.id !== toDelete.id)).toBe(true);
		});

		it("should return empty array when no items match", async () => {
			const result = await repo.findMany("page");

			expect(result.items).toEqual([]);
			expect(result.nextCursor).toBeUndefined();
		});

		describe("orderBy", () => {
			// Regression guard for "table headers aren't sort controls": the
			// admin now sends orderBy={field,direction} — the repo must accept
			// the columns the UI wants to expose, not just dates.
			it("accepts status as an order field", async () => {
				const result = await repo.findMany("post", {
					orderBy: { field: "status", direction: "asc" },
				});

				// alphabetical asc places 'draft' before 'published'
				expect(result.items[0]!.status).toBe("draft");
			});

			it("accepts locale as an order field", async () => {
				await repo.findMany("post", {
					orderBy: { field: "locale", direction: "desc" },
				});
				// no throw = pass
			});

			it("rejects unknown fields to block column enumeration", async () => {
				await expect(
					repo.findMany("post", {
						orderBy: { field: "password", direction: "asc" },
					}),
				).rejects.toThrow(EmDashValidationError);
			});
		});
	});

	describe("update", () => {
		it("should update content data", async () => {
			const created = await repo.create({
				type: "post",
				data: { title: "Original" },
			});

			const updated = await repo.update("post", created.id, {
				data: { title: "Updated" },
			});

			expect(updated.data.title).toBe("Updated");
			expect(updated.id).toBe(created.id);
		});

		it("should update status", async () => {
			const created = await repo.create({
				type: "post",
				data: { title: "Test" },
				status: "draft",
			});

			const updated = await repo.update("post", created.id, {
				status: "published",
			});

			expect(updated.status).toBe("published");
		});

		it("should update slug", async () => {
			const created = await repo.create({
				type: "post",
				slug: "old-slug",
				data: { title: "Test" },
			});

			const updated = await repo.update("post", created.id, {
				slug: "new-slug",
			});

			expect(updated.slug).toBe("new-slug");
		});

		it("should update publishedAt", async () => {
			const created = await repo.create({
				type: "post",
				data: { title: "Test" },
			});

			const publishedAt = new Date().toISOString();
			const updated = await repo.update("post", created.id, {
				publishedAt,
			});

			expect(updated.publishedAt).toBe(publishedAt);
		});

		it("should support partial updates", async () => {
			const created = await repo.create({
				type: "post",
				slug: "test-slug",
				data: { title: "Test", content: "Original content" },
				status: "draft",
			});

			const updated = await repo.update("post", created.id, {
				status: "published",
			});

			// Only status should change
			expect(updated.status).toBe("published");
			expect(updated.slug).toBe("test-slug");
			expect(updated.data).toEqual({
				title: "Test",
				content: "Original content",
			});
		});

		it("should throw error for non-existent content", async () => {
			await expect(repo.update("post", "non-existent", { status: "published" })).rejects.toThrow(
				"Content not found",
			);
		});

		it("should persist removal of array items in JSON fields (multiSelect)", async () => {
			// Add a multiSelect (JSON) field to the post collection
			await registry.createField("post", {
				slug: "tags",
				label: "Tags",
				type: "multiSelect",
			});

			const created = await repo.create({
				type: "post",
				data: { title: "Test", tags: ["news", "sports", "tech"] },
			});

			expect(created.data.tags).toEqual(["news", "sports", "tech"]);

			// Remove "sports" from the array (simulates unchecking a checkbox)
			const updated = await repo.update("post", created.id, {
				data: { title: "Test", tags: ["news", "tech"] },
			});

			expect(updated.data.tags).toEqual(["news", "tech"]);

			// Verify it persists when re-reading
			const fetched = await repo.findById("post", updated.id);
			expect(fetched!.data.tags).toEqual(["news", "tech"]);
		});

		it("should persist empty array in JSON fields (multiSelect)", async () => {
			await registry.createField("post", {
				slug: "categories",
				label: "Categories",
				type: "multiSelect",
			});

			const created = await repo.create({
				type: "post",
				data: { title: "Test", categories: ["news"] },
			});

			// Uncheck all items
			const updated = await repo.update("post", created.id, {
				data: { title: "Test", categories: [] },
			});

			expect(updated.data.categories).toEqual([]);

			const fetched = await repo.findById("post", updated.id);
			expect(fetched!.data.categories).toEqual([]);
		});

		it("should not update soft-deleted content", async () => {
			const created = await repo.create({
				type: "post",
				data: { title: "Test" },
			});

			await repo.delete("post", created.id);

			await expect(repo.update("post", created.id, { status: "published" })).rejects.toThrow(
				"Content not found",
			);
		});

		it("should update updatedAt timestamp", async () => {
			const created = await repo.create({
				type: "post",
				data: { title: "Test" },
			});

			// Small delay to ensure timestamp difference
			await new Promise((resolve) => setTimeout(resolve, 10));

			const updated = await repo.update("post", created.id, {
				data: { title: "Updated" },
			});

			expect(updated.updatedAt > created.updatedAt).toBe(true);
		});
	});

	describe("delete", () => {
		it("should soft delete content", async () => {
			const created = await repo.create({
				type: "post",
				data: { title: "Test" },
			});

			const result = await repo.delete("post", created.id);

			expect(result).toBe(true);

			// Verify content is not found
			const found = await repo.findById("post", created.id);
			expect(found).toBeNull();
		});

		it("should return true for successful deletion", async () => {
			const created = await repo.create({
				type: "post",
				data: { title: "Test" },
			});

			const result = await repo.delete("post", created.id);

			expect(result).toBe(true);
		});

		it("should return false for non-existent content", async () => {
			const result = await repo.delete("post", "non-existent");

			expect(result).toBe(false);
		});

		it("should return false for already deleted content", async () => {
			const created = await repo.create({
				type: "post",
				data: { title: "Test" },
			});

			await repo.delete("post", created.id);
			const result = await repo.delete("post", created.id);

			expect(result).toBe(false);
		});

		it("should set deleted_at timestamp", async () => {
			const created = await repo.create({
				type: "post",
				data: { title: "Test" },
			});

			await repo.delete("post", created.id);

			// Directly query database to check deleted_at
			// Use raw SQL since ec_post is a dynamic table
			const { sql } = await import("kysely");
			const result = await sql<{ deleted_at: string | null }>`
				SELECT deleted_at FROM ec_post WHERE id = ${created.id}
			`.execute(db);

			expect(result.rows[0]?.deleted_at).toBeDefined();
			expect(result.rows[0]?.deleted_at).not.toBeNull();
		});
	});

	describe("count", () => {
		beforeEach(async () => {
			// Create test data
			for (let i = 0; i < 10; i++) {
				await repo.create({
					type: "post",
					data: { title: `Post ${i}` },
					status: i % 2 === 0 ? "published" : "draft",
					authorId: i < 5 ? "author-1" : "author-2",
				});
			}
		});

		it("should count all content of a type", async () => {
			const count = await repo.count("post");

			expect(count).toBe(10);
		});

		it("should count by status", async () => {
			const count = await repo.count("post", { status: "published" });

			expect(count).toBe(5);
		});

		it("should count by authorId", async () => {
			const count = await repo.count("post", { authorId: "author-1" });

			expect(count).toBe(5);
		});

		it("should count by both status and authorId", async () => {
			const count = await repo.count("post", {
				status: "published",
				authorId: "author-1",
			});

			// Posts 0, 2, 4 are published by author-1
			expect(count).toBe(3);
		});

		it("should return 0 when no items match", async () => {
			const count = await repo.count("page");

			expect(count).toBe(0);
		});

		it("should not count soft-deleted content", async () => {
			const created = await repo.create({
				type: "post",
				data: { title: "To Delete" },
			});

			await repo.delete("post", created.id);

			const count = await repo.count("post");

			expect(count).toBe(10); // Not 11
		});
	});

	describe("integration scenarios", () => {
		it("should handle full CRUD lifecycle", async () => {
			// Create
			const created = await repo.create({
				type: "post",
				slug: "test-post",
				data: { title: "Test Post", content: "Original content" },
				status: "draft",
			});

			expect(created.id).toBeDefined();
			expect(created.status).toBe("draft");

			// Read
			const found = await repo.findBySlug("post", "test-post");
			expect(found!.id).toBe(created.id);

			// Update
			const updated = await repo.update("post", created.id, {
				data: { title: "Updated Post", content: "New content" },
				status: "published",
			});

			expect(updated.data.title).toBe("Updated Post");
			expect(updated.status).toBe("published");

			// Delete
			const deleted = await repo.delete("post", created.id);
			expect(deleted).toBe(true);

			// Verify not found
			const notFound = await repo.findById("post", created.id);
			expect(notFound).toBeNull();
		});

		it("should handle concurrent operations", async () => {
			// Create multiple items concurrently
			const promises = Array.from({ length: 10 }, (_, i) =>
				repo.create({
					type: "post",
					data: { title: `Post ${i}` },
				}),
			);

			const created = await Promise.all(promises);

			expect(created).toHaveLength(10);
			expect(new Set(created.map((c) => c.id)).size).toBe(10); // All unique IDs
		});

		it("should persist complex nested data structures", async () => {
			// Use the content field (portableText type) for complex nested data
			const complexContent = [
				{
					_type: "block",
					style: "h1",
					children: [{ _type: "span", text: "Title" }],
				},
				{
					_type: "block",
					style: "normal",
					children: [
						{ _type: "span", text: "Bold", marks: ["bold"] },
						{ _type: "span", text: " and " },
						{ _type: "span", text: "italic", marks: ["italic"] },
					],
				},
			];

			const created = await repo.create({
				type: "post",
				data: {
					title: "Complex Post",
					content: complexContent,
				},
			});

			const retrieved = await repo.findById("post", created.id);

			expect(retrieved!.data.title).toBe("Complex Post");
			expect(retrieved!.data.content).toEqual(complexContent);
		});
	});
});
