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

import { ContentRepository } from "../../../src/database/repositories/content.js";
import type { Database } from "../../../src/database/types.js";
import { SchemaRegistry } from "../../../src/schema/registry.js";
import { FTSManager } from "../../../src/search/fts-manager.js";
import { searchWithDb } from "../../../src/search/query.js";
import { applySeed } from "../../../src/seed/apply.js";
import type { SeedFile } from "../../../src/seed/types.js";
import { validateSeed } from "../../../src/seed/validate.js";
import { createPostFixture } from "../../utils/fixtures.js";
import { setupTestDatabaseWithCollections, teardownTestDatabase } from "../../utils/test-db.js";

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

	beforeEach(async () => {
		db = await setupTestDatabaseWithCollections();
		repo = new ContentRepository(db);
	});

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

	// ─── 1. Migration — i18n columns exist ──────────────────────────

	describe("Migration — i18n columns", () => {
		it("should have locale and translation_group columns on content tables", async () => {
			const result = await sql<{ name: string }>`
				PRAGMA table_info(ec_post)
			`.execute(db);

			const columnNames = result.rows.map((r) => r.name);
			expect(columnNames).toContain("locale");
			expect(columnNames).toContain("translation_group");
		});

		it("should default locale to 'en'", async () => {
			const result = await sql<{ name: string; dflt_value: string | null }>`
				PRAGMA table_info(ec_post)
			`.execute(db);

			const localeCol = result.rows.find((r) => r.name === "locale");
			expect(localeCol).toBeDefined();
			expect(localeCol!.dflt_value).toBe("'en'");
		});

		it("should have translatable column on _emdash_fields", async () => {
			const result = await sql<{ name: string }>`
				PRAGMA table_info(_emdash_fields)
			`.execute(db);

			const columnNames = result.rows.map((r) => r.name);
			expect(columnNames).toContain("translatable");
		});

		it("should have compound unique constraint on slug+locale", async () => {
			// Insert same slug, different locale — should succeed
			await sql`
				INSERT INTO ec_post (id, slug, locale, translation_group, status, version, created_at, updated_at)
				VALUES ('id1', 'hello', 'en', 'id1', 'draft', 1, datetime('now'), datetime('now'))
			`.execute(db);

			await sql`
				INSERT INTO ec_post (id, slug, locale, translation_group, status, version, created_at, updated_at)
				VALUES ('id2', 'hello', 'fr', 'id1', 'draft', 1, datetime('now'), datetime('now'))
			`.execute(db);

			// Same slug, same locale — should fail
			await expect(
				sql`
					INSERT INTO ec_post (id, slug, locale, translation_group, status, version, created_at, updated_at)
					VALUES ('id3', 'hello', 'en', 'id3', 'draft', 1, datetime('now'), datetime('now'))
				`.execute(db),
			).rejects.toThrow();
		});

		it("should have locale and translation_group indexes", async () => {
			const result = await sql<{ name: string }>`
				PRAGMA index_list(ec_post)
			`.execute(db);

			const indexNames = result.rows.map((r) => r.name);
			expect(indexNames).toContain("idx_ec_post_locale");
			expect(indexNames).toContain("idx_ec_post_translation_group");
		});
	});

	// ─── 2. ContentRepository — locale-aware CRUD ───────────────────

	describe("ContentRepository — locale-aware CRUD", () => {
		it("create() without locale defaults to 'en'", async () => {
			const post = await repo.create(createPostFixture());
			expect(post.locale).toBe("en");
		});

		it("create() with explicit locale stores it", async () => {
			const post = await repo.create(createPostFixture({ locale: "fr", slug: "bonjour" }));
			expect(post.locale).toBe("fr");
		});

		it("create() with translationOf links via translation_group", async () => {
			const enPost = await repo.create(createPostFixture({ slug: "hello-world", locale: "en" }));

			const frPost = await repo.create(
				createPostFixture({
					slug: "bonjour-monde",
					locale: "fr",
					translationOf: enPost.id,
					data: { title: "Bonjour le Monde" },
				}),
			);

			// Both should share the same translation_group
			expect(frPost.translationGroup).toBe(enPost.translationGroup);
			// The group should be the original item's id (since it was first)
			expect(enPost.translationGroup).toBe(enPost.id);
		});

		it("create() with translationOf on a chained translation uses the root group", async () => {
			const enPost = await repo.create(createPostFixture({ slug: "hello", locale: "en" }));

			const frPost = await repo.create(
				createPostFixture({
					slug: "bonjour",
					locale: "fr",
					translationOf: enPost.id,
					data: { title: "Bonjour" },
				}),
			);

			// Create a third translation linked to the French version
			const dePost = await repo.create(
				createPostFixture({
					slug: "hallo",
					locale: "de",
					translationOf: frPost.id,
					data: { title: "Hallo" },
				}),
			);

			// All three should share the same translation_group
			expect(dePost.translationGroup).toBe(enPost.id);
			expect(frPost.translationGroup).toBe(enPost.id);
		});

		it("create() with translationOf pointing to non-existent ID throws", async () => {
			await expect(
				repo.create(
					createPostFixture({
						slug: "orphan",
						locale: "fr",
						translationOf: "NONEXISTENT_ID_12345678",
					}),
				),
			).rejects.toThrow("Translation source content not found");
		});

		it("same slug different locales are allowed", async () => {
			const en = await repo.create(createPostFixture({ slug: "about", locale: "en" }));
			const fr = await repo.create(
				createPostFixture({
					slug: "about",
					locale: "fr",
					data: { title: "À propos" },
				}),
			);

			expect(en.slug).toBe("about");
			expect(fr.slug).toBe("about");
			expect(en.id).not.toBe(fr.id);
		});

		it("same slug same locale is rejected", async () => {
			await repo.create(createPostFixture({ slug: "unique-slug", locale: "en" }));

			await expect(
				repo.create(
					createPostFixture({
						slug: "unique-slug",
						locale: "en",
						data: { title: "Duplicate" },
					}),
				),
			).rejects.toThrow();
		});

		// ── findBySlug ────────────────────────────────────────────────

		it("findBySlug() without locale returns any match", async () => {
			await repo.create(createPostFixture({ slug: "shared-slug", locale: "en" }));
			await repo.create(
				createPostFixture({
					slug: "shared-slug",
					locale: "fr",
					data: { title: "Version FR" },
				}),
			);

			const found = await repo.findBySlug("post", "shared-slug");
			expect(found).not.toBeNull();
			expect(found!.slug).toBe("shared-slug");
		});

		it("findBySlug() with locale filters to that locale", async () => {
			await repo.create(createPostFixture({ slug: "about", locale: "en" }));
			await repo.create(
				createPostFixture({
					slug: "about",
					locale: "fr",
					data: { title: "À propos" },
				}),
			);

			const en = await repo.findBySlug("post", "about", "en");
			expect(en).not.toBeNull();
			expect(en!.locale).toBe("en");

			const fr = await repo.findBySlug("post", "about", "fr");
			expect(fr).not.toBeNull();
			expect(fr!.locale).toBe("fr");

			const de = await repo.findBySlug("post", "about", "de");
			expect(de).toBeNull();
		});

		// ── findByIdOrSlug ────────────────────────────────────────────

		it("findByIdOrSlug() — ID lookup ignores locale param", async () => {
			const post = await repo.create(createPostFixture({ slug: "test-post", locale: "en" }));

			// ID lookup should find it regardless of locale param
			const found = await repo.findByIdOrSlug("post", post.id, "fr");
			expect(found).not.toBeNull();
			expect(found!.id).toBe(post.id);
			expect(found!.locale).toBe("en");
		});

		it("findByIdOrSlug() — slug lookup respects locale", async () => {
			const enPost = await repo.create(createPostFixture({ slug: "test", locale: "en" }));
			const frPost = await repo.create(
				createPostFixture({
					slug: "test",
					locale: "fr",
					data: { title: "Test FR" },
				}),
			);

			const foundEn = await repo.findByIdOrSlug("post", "test", "en");
			expect(foundEn).not.toBeNull();
			expect(foundEn!.id).toBe(enPost.id);

			const foundFr = await repo.findByIdOrSlug("post", "test", "fr");
			expect(foundFr).not.toBeNull();
			expect(foundFr!.id).toBe(frPost.id);
		});

		// ── findMany ──────────────────────────────────────────────────

		it("findMany() without locale returns all locales", async () => {
			await repo.create(createPostFixture({ slug: "en-post", locale: "en" }));
			await repo.create(
				createPostFixture({
					slug: "fr-post",
					locale: "fr",
					data: { title: "Post FR" },
				}),
			);
			await repo.create(
				createPostFixture({
					slug: "de-post",
					locale: "de",
					data: { title: "Post DE" },
				}),
			);

			const result = await repo.findMany("post");
			expect(result.items).toHaveLength(3);
		});

		it("findMany() with locale filters to that locale", async () => {
			await repo.create(createPostFixture({ slug: "en-post", locale: "en" }));
			await repo.create(
				createPostFixture({
					slug: "fr-post",
					locale: "fr",
					data: { title: "Post FR" },
				}),
			);
			await repo.create(
				createPostFixture({
					slug: "de-post",
					locale: "de",
					data: { title: "Post DE" },
				}),
			);

			const frResult = await repo.findMany("post", {
				where: { locale: "fr" },
			});
			expect(frResult.items).toHaveLength(1);
			expect(frResult.items[0]!.locale).toBe("fr");
		});

		// ── count ─────────────────────────────────────────────────────

		it("count() without locale counts all", async () => {
			await repo.create(createPostFixture({ slug: "post-en", locale: "en" }));
			await repo.create(
				createPostFixture({
					slug: "post-fr",
					locale: "fr",
					data: { title: "FR" },
				}),
			);

			const total = await repo.count("post");
			expect(total).toBe(2);
		});

		it("count() with locale counts only that locale", async () => {
			await repo.create(createPostFixture({ slug: "post-en", locale: "en" }));
			await repo.create(
				createPostFixture({
					slug: "post-fr",
					locale: "fr",
					data: { title: "FR" },
				}),
			);

			const enCount = await repo.count("post", { locale: "en" });
			expect(enCount).toBe(1);

			const deCount = await repo.count("post", { locale: "de" });
			expect(deCount).toBe(0);
		});

		// ── findTranslations ──────────────────────────────────────────

		it("findTranslations() returns all locales for a translation group", async () => {
			const enPost = await repo.create(createPostFixture({ slug: "hello", locale: "en" }));

			await repo.create(
				createPostFixture({
					slug: "bonjour",
					locale: "fr",
					translationOf: enPost.id,
					data: { title: "Bonjour" },
				}),
			);

			await repo.create(
				createPostFixture({
					slug: "hallo",
					locale: "de",
					translationOf: enPost.id,
					data: { title: "Hallo" },
				}),
			);

			const translations = await repo.findTranslations("post", enPost.translationGroup!);

			expect(translations).toHaveLength(3);

			const locales = translations
				.map((t) => t.locale)
				.toSorted((a, b) => (a ?? "").localeCompare(b ?? ""));
			expect(locales).toEqual(["de", "en", "fr"]);
		});

		it("findTranslations() returns only non-deleted items", async () => {
			const enPost = await repo.create(createPostFixture({ slug: "hello", locale: "en" }));

			const frPost = await repo.create(
				createPostFixture({
					slug: "bonjour",
					locale: "fr",
					translationOf: enPost.id,
					data: { title: "Bonjour" },
				}),
			);

			// Soft-delete the French translation
			await repo.delete("post", frPost.id);

			const translations = await repo.findTranslations("post", enPost.translationGroup!);

			expect(translations).toHaveLength(1);
			expect(translations[0]!.locale).toBe("en");
		});
	});

	// ─── 3. FTS — locale-aware search ───────────────────────────────

	describe("FTS — locale-aware search", () => {
		let registry: SchemaRegistry;
		let ftsManager: FTSManager;

		beforeEach(async () => {
			registry = new SchemaRegistry(db);
			ftsManager = new FTSManager(db);

			// Mark title as searchable and enable FTS
			await registry.updateField("post", "title", { searchable: true });
			await ftsManager.enableSearch("post");
		});

		it("search with locale filter returns only that locale's results", async () => {
			// Create published posts in different locales
			const enPost = await repo.create(
				createPostFixture({
					slug: "hello-world",
					locale: "en",
					status: "published",
					data: { title: "Hello World" },
				}),
			);

			const frPost = await repo.create(
				createPostFixture({
					slug: "bonjour-monde",
					locale: "fr",
					status: "published",
					data: { title: "Bonjour le Monde" },
				}),
			);

			// Search for "world" — English only
			const enResults = await searchWithDb(db, "Hello", {
				collections: ["post"],
				locale: "en",
				status: "published",
			});

			expect(enResults.items.length).toBeGreaterThanOrEqual(1);
			expect(enResults.items.every((r) => r.locale === "en")).toBe(true);
			expect(enResults.items.some((r) => r.id === enPost.id)).toBe(true);

			// Search for "Bonjour" — French only
			const frResults = await searchWithDb(db, "Bonjour", {
				collections: ["post"],
				locale: "fr",
				status: "published",
			});

			expect(frResults.items.length).toBeGreaterThanOrEqual(1);
			expect(frResults.items.every((r) => r.locale === "fr")).toBe(true);
			expect(frResults.items.some((r) => r.id === frPost.id)).toBe(true);
		});

		it("search without locale returns results from all locales", async () => {
			await repo.create(
				createPostFixture({
					slug: "universal-en",
					locale: "en",
					status: "published",
					data: { title: "Universal Content" },
				}),
			);

			await repo.create(
				createPostFixture({
					slug: "universal-fr",
					locale: "fr",
					status: "published",
					data: { title: "Universal Contenu" },
				}),
			);

			const results = await searchWithDb(db, "Universal", {
				collections: ["post"],
				status: "published",
			});

			expect(results.items).toHaveLength(2);
			const locales = results.items.map((r) => r.locale).toSorted();
			expect(locales).toEqual(["en", "fr"]);
		});

		it("FTS index includes locale column", async () => {
			// Verify the FTS table has the locale column by checking structure
			const exists = await ftsManager.ftsTableExists("post");
			expect(exists).toBe(true);

			// Create a post and verify it appears in FTS results with locale
			await repo.create(
				createPostFixture({
					slug: "fts-test",
					locale: "ja",
					status: "published",
					data: { title: "FTS Locale Test" },
				}),
			);

			const results = await searchWithDb(db, "FTS Locale", {
				collections: ["post"],
				locale: "ja",
				status: "published",
			});

			expect(results.items).toHaveLength(1);
			expect(results.items[0]!.locale).toBe("ja");
		});

		it("rebuilt index preserves locale-aware search", async () => {
			// Create content before rebuild
			await repo.create(
				createPostFixture({
					slug: "pre-rebuild-en",
					locale: "en",
					status: "published",
					data: { title: "Rebuild Test English" },
				}),
			);

			await repo.create(
				createPostFixture({
					slug: "pre-rebuild-fr",
					locale: "fr",
					status: "published",
					data: { title: "Rebuild Test French" },
				}),
			);

			// Rebuild the index
			await ftsManager.rebuildIndex("post", ["title"]);

			// Verify locale-aware search still works
			const enResults = await searchWithDb(db, "Rebuild", {
				collections: ["post"],
				locale: "en",
				status: "published",
			});

			expect(enResults.items).toHaveLength(1);
			expect(enResults.items[0]!.locale).toBe("en");
		});
	});

	// ─── 4. Seed — locale-aware content ─────────────────────────────

	describe("Seed — locale-aware content", () => {
		it("applySeed() creates content with locale and translationOf", async () => {
			const seed: SeedFile = {
				version: "1",
				content: {
					post: [
						{
							id: "welcome",
							slug: "welcome",
							locale: "en",
							status: "published",
							data: { title: "Welcome" },
						},
						{
							id: "welcome-fr",
							slug: "bienvenue",
							locale: "fr",
							translationOf: "welcome",
							status: "draft",
							data: { title: "Bienvenue" },
						},
						{
							id: "welcome-de",
							slug: "willkommen",
							locale: "de",
							translationOf: "welcome",
							status: "published",
							data: { title: "Willkommen" },
						},
					],
				},
			};

			const result = await applySeed(db, seed, { includeContent: true });

			expect(result.content.created).toBe(3);
			expect(result.content.skipped).toBe(0);

			// Verify the entries exist with correct locales
			const seedRepo = new ContentRepository(db);
			const enPost = await seedRepo.findBySlug("post", "welcome", "en");
			const frPost = await seedRepo.findBySlug("post", "bienvenue", "fr");
			const dePost = await seedRepo.findBySlug("post", "willkommen", "de");

			expect(enPost).not.toBeNull();
			expect(frPost).not.toBeNull();
			expect(dePost).not.toBeNull();

			expect(enPost!.locale).toBe("en");
			expect(frPost!.locale).toBe("fr");
			expect(dePost!.locale).toBe("de");

			// All should share the same translation_group
			expect(frPost!.translationGroup).toBe(enPost!.translationGroup);
			expect(dePost!.translationGroup).toBe(enPost!.translationGroup);
		});

		it("applySeed() without locale falls back to default", async () => {
			const seed: SeedFile = {
				version: "1",
				content: {
					post: [
						{
							id: "plain",
							slug: "plain-post",
							data: { title: "No Locale" },
						},
					],
				},
			};

			const result = await applySeed(db, seed, { includeContent: true });
			expect(result.content.created).toBe(1);

			const plainRepo = new ContentRepository(db);
			const post = await plainRepo.findBySlug("post", "plain-post");
			expect(post).not.toBeNull();
			expect(post!.locale).toBe("en"); // default
			expect(post!.translationGroup).toBe(post!.id); // self-reference
		});

		it("applySeed() skips existing entries with locale-aware lookup", async () => {
			// Pre-create an entry
			const skipRepo = new ContentRepository(db);
			await skipRepo.create(createPostFixture({ slug: "existing", locale: "fr" }));

			const seed: SeedFile = {
				version: "1",
				content: {
					post: [
						{
							id: "existing",
							slug: "existing",
							locale: "fr",
							data: { title: "Should Skip" },
						},
					],
				},
			};

			const result = await applySeed(db, seed, { includeContent: true });
			expect(result.content.skipped).toBe(1);
			expect(result.content.created).toBe(0);
		});

		it("applySeed() rejects missing translationOf via validation", async () => {
			const seed: SeedFile = {
				version: "1",
				content: {
					post: [
						{
							id: "orphan-fr",
							slug: "orphelin",
							locale: "fr",
							translationOf: "nonexistent",
							data: { title: "Orphan" },
						},
					],
				},
			};

			// Validation catches the bad reference before applySeed runs
			await expect(applySeed(db, seed, { includeContent: true })).rejects.toThrow(
				'references "nonexistent" which is not in this collection',
			);
		});
	});

	// ─── 5. Seed validation — i18n fields ───────────────────────────

	describe("Seed validation — i18n fields", () => {
		it("validates translationOf requires locale", () => {
			const seed = {
				version: "1",
				content: {
					posts: [
						{ id: "en", slug: "hello", data: { title: "Hello" } },
						{
							id: "fr",
							slug: "bonjour",
							translationOf: "en",
							data: { title: "Bonjour" },
						},
					],
				},
			};

			const result = validateSeed(seed);
			expect(result.valid).toBe(false);
			expect(result.errors.some((e) => e.includes("locale is required when translationOf"))).toBe(
				true,
			);
		});

		it("validates translationOf references exist", () => {
			const seed = {
				version: "1",
				content: {
					posts: [
						{
							id: "fr",
							slug: "bonjour",
							locale: "fr",
							translationOf: "nonexistent",
							data: { title: "Bonjour" },
						},
					],
				},
			};

			const result = validateSeed(seed);
			expect(result.valid).toBe(false);
			expect(
				result.errors.some((e) => e.includes('references "nonexistent" which is not in')),
			).toBe(true);
		});

		it("valid seed with i18n fields passes validation", () => {
			const seed = {
				version: "1",
				content: {
					posts: [
						{ id: "en", slug: "hello", locale: "en", data: { title: "Hello" } },
						{
							id: "fr",
							slug: "bonjour",
							locale: "fr",
							translationOf: "en",
							data: { title: "Bonjour" },
						},
					],
				},
			};

			const result = validateSeed(seed);
			expect(result.valid).toBe(true);
			expect(result.errors).toHaveLength(0);
		});
	});

	// ─── 6. Non-i18n regression ─────────────────────────────────────

	describe("Non-i18n regression", () => {
		it("content created without locale has locale 'en'", async () => {
			const post = await repo.create({
				type: "post",
				slug: "no-locale",
				data: { title: "No Locale Specified" },
			});

			expect(post.locale).toBe("en");
		});

		it("findMany without locale param returns all results", async () => {
			await repo.create(createPostFixture({ slug: "post-1" }));
			await repo.create(createPostFixture({ slug: "post-2" }));

			const result = await repo.findMany("post");
			expect(result.items).toHaveLength(2);
		});

		it("findBySlug works without locale param", async () => {
			const created = await repo.create(createPostFixture({ slug: "find-me" }));
			const found = await repo.findBySlug("post", "find-me");

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

		it("findByIdOrSlug works without locale param", async () => {
			const created = await repo.create(createPostFixture({ slug: "lookup-test" }));

			// By slug
			const bySlug = await repo.findByIdOrSlug("post", "lookup-test");
			expect(bySlug).not.toBeNull();
			expect(bySlug!.id).toBe(created.id);

			// By ID
			const byId = await repo.findByIdOrSlug("post", created.id);
			expect(byId).not.toBeNull();
			expect(byId!.id).toBe(created.id);
		});

		it("slug uniqueness is still enforced within the same locale", async () => {
			await repo.create(createPostFixture({ slug: "dupe-test" }));

			// Same slug, same default locale — should fail
			await expect(repo.create(createPostFixture({ slug: "dupe-test" }))).rejects.toThrow();
		});

		it("count works without locale param", async () => {
			await repo.create(createPostFixture({ slug: "count-1" }));
			await repo.create(createPostFixture({ slug: "count-2" }));

			const count = await repo.count("post");
			expect(count).toBe(2);
		});

		it("translation_group is auto-set to item id when no translationOf", async () => {
			const post = await repo.create(createPostFixture({ slug: "standalone" }));

			expect(post.translationGroup).toBe(post.id);
		});

		it("existing CRUD operations are unaffected by i18n columns", async () => {
			// Create
			const post = await repo.create(createPostFixture({ slug: "crud-test", status: "draft" }));
			expect(post.status).toBe("draft");

			// Update
			const updated = await repo.update("post", post.id, {
				data: { title: "Updated Title" },
			});
			expect(updated.data.title).toBe("Updated Title");
			expect(updated.locale).toBe("en"); // locale unchanged

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

			// Should not be found
			const notFound = await repo.findById("post", post.id);
			expect(notFound).toBeNull();

			// Restore
			const restored = await repo.restore("post", post.id);
			expect(restored).toBe(true);

			const found = await repo.findById("post", post.id);
			expect(found).not.toBeNull();
			expect(found!.locale).toBe("en");
		});
	});
});
