/**
 * Tests for seed --on-conflict modes: skip, update, error
 *
 * Verifies that applySeed() correctly handles conflicts when records
 * already exist in the database.
 */

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

import type { Database } from "../../src/database/types.js";
import { applySeed } from "../../src/seed/apply.js";
import type { SeedFile } from "../../src/seed/types.js";
import { setupTestDatabase, teardownTestDatabase } from "../utils/test-db.js";

/**
 * Minimal seed file with one collection, one byline, one redirect, and one section
 */
function createTestSeed(overrides?: Partial<SeedFile>): SeedFile {
	return {
		version: "1",
		collections: [
			{
				slug: "posts",
				label: "Posts",
				labelSingular: "Post",
				fields: [
					{ slug: "title", label: "Title", type: "string" },
					{ slug: "body", label: "Body", type: "text" },
				],
			},
		],
		bylines: [
			{
				id: "byline-1",
				slug: "jane-doe",
				displayName: "Jane Doe",
				bio: "Original bio",
			},
		],
		redirects: [
			{
				source: "/old-page",
				destination: "/new-page",
				type: 301,
			},
		],
		sections: [
			{
				slug: "hero",
				title: "Hero Section",
				description: "Original description",
				content: [{ _type: "block", _key: "1" }],
			},
		],
		content: {
			posts: [
				{
					id: "post-1",
					slug: "hello-world",
					status: "published",
					data: { title: "Hello World", body: "Original body" },
				},
			],
		},
		...overrides,
	};
}

/**
 * Seed file with updated values for all entities
 */
function createUpdatedSeed(): SeedFile {
	return {
		version: "1",
		collections: [
			{
				slug: "posts",
				label: "Blog Posts",
				labelSingular: "Blog Post",
				fields: [
					{ slug: "title", label: "Post Title", type: "string" },
					{ slug: "body", label: "Post Body", type: "text" },
				],
			},
		],
		bylines: [
			{
				id: "byline-1",
				slug: "jane-doe",
				displayName: "Jane Smith",
				bio: "Updated bio",
			},
		],
		redirects: [
			{
				source: "/old-page",
				destination: "/newer-page",
				type: 302,
			},
		],
		sections: [
			{
				slug: "hero",
				title: "Updated Hero",
				description: "Updated description",
				content: [{ _type: "block", _key: "2" }],
			},
		],
		content: {
			posts: [
				{
					id: "post-1",
					slug: "hello-world",
					status: "published",
					data: { title: "Hello World Updated", body: "Updated body" },
				},
			],
		},
	};
}

describe("applySeed onConflict modes", () => {
	let db: Kysely<Database>;

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

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

	describe("onConflict: skip (default)", () => {
		it("skips existing collections", async () => {
			const seed = createTestSeed();
			// First apply
			await applySeed(db, seed, { includeContent: true });
			// Second apply with default (skip)
			const result = await applySeed(db, seed, { includeContent: true });

			expect(result.collections.created).toBe(0);
			expect(result.collections.skipped).toBe(1);
			expect(result.collections.updated).toBe(0);
		});

		it("skips existing bylines", async () => {
			const seed = createTestSeed();
			await applySeed(db, seed, { includeContent: true });
			const result = await applySeed(db, seed, { includeContent: true });

			expect(result.bylines.created).toBe(0);
			expect(result.bylines.skipped).toBe(1);
			expect(result.bylines.updated).toBe(0);
		});

		it("skips existing redirects", async () => {
			const seed = createTestSeed();
			await applySeed(db, seed);
			const result = await applySeed(db, seed);

			expect(result.redirects.created).toBe(0);
			expect(result.redirects.skipped).toBe(1);
			expect(result.redirects.updated).toBe(0);
		});

		it("skips existing sections", async () => {
			const seed = createTestSeed();
			await applySeed(db, seed);
			const result = await applySeed(db, seed);

			expect(result.sections.created).toBe(0);
			expect(result.sections.skipped).toBe(1);
			expect(result.sections.updated).toBe(0);
		});

		it("skips existing content", async () => {
			const seed = createTestSeed();
			await applySeed(db, seed, { includeContent: true });
			const result = await applySeed(db, seed, { includeContent: true });

			expect(result.content.created).toBe(0);
			expect(result.content.skipped).toBe(1);
			expect(result.content.updated).toBe(0);
		});

		it("defaults to skip when onConflict is not specified", async () => {
			const seed = createTestSeed();
			await applySeed(db, seed, { includeContent: true });
			// No onConflict specified -- should default to skip
			const result = await applySeed(db, seed, { includeContent: true });

			expect(result.collections.skipped).toBe(1);
			expect(result.collections.created).toBe(0);
			expect(result.collections.updated).toBe(0);
		});
	});

	describe("onConflict: update", () => {
		it("updates existing collections and fields", async () => {
			const seed = createTestSeed();
			await applySeed(db, seed, { includeContent: true });

			const updatedSeed = createUpdatedSeed();
			const result = await applySeed(db, updatedSeed, {
				includeContent: true,
				onConflict: "update",
			});

			expect(result.collections.updated).toBe(1);
			expect(result.collections.created).toBe(0);
			expect(result.fields.updated).toBe(2);

			// Verify the collection was actually updated
			const row = await db
				.selectFrom("_emdash_collections")
				.selectAll()
				.where("slug", "=", "posts")
				.executeTakeFirst();
			expect(row?.label).toBe("Blog Posts");
			expect(row?.label_singular).toBe("Blog Post");
		});

		it("updates existing bylines", async () => {
			const seed = createTestSeed();
			await applySeed(db, seed, { includeContent: true });

			const updatedSeed = createUpdatedSeed();
			const result = await applySeed(db, updatedSeed, {
				includeContent: true,
				onConflict: "update",
			});

			expect(result.bylines.updated).toBe(1);
			expect(result.bylines.created).toBe(0);

			// Verify the byline was actually updated
			const row = await db
				.selectFrom("_emdash_bylines")
				.selectAll()
				.where("slug", "=", "jane-doe")
				.executeTakeFirst();
			expect(row?.display_name).toBe("Jane Smith");
			expect(row?.bio).toBe("Updated bio");
		});

		it("updates existing redirects", async () => {
			const seed = createTestSeed();
			await applySeed(db, seed);

			const updatedSeed = createUpdatedSeed();
			const result = await applySeed(db, updatedSeed, {
				onConflict: "update",
			});

			expect(result.redirects.updated).toBe(1);
			expect(result.redirects.created).toBe(0);

			// Verify the redirect was actually updated
			const row = await db
				.selectFrom("_emdash_redirects")
				.selectAll()
				.where("source", "=", "/old-page")
				.executeTakeFirst();
			expect(row?.destination).toBe("/newer-page");
			expect(row?.type).toBe(302);
		});

		it("updates existing sections", async () => {
			const seed = createTestSeed();
			await applySeed(db, seed);

			const updatedSeed = createUpdatedSeed();
			const result = await applySeed(db, updatedSeed, {
				onConflict: "update",
			});

			expect(result.sections.updated).toBe(1);
			expect(result.sections.created).toBe(0);

			// Verify the section was actually updated
			const row = await db
				.selectFrom("_emdash_sections")
				.selectAll()
				.where("slug", "=", "hero")
				.executeTakeFirst();
			expect(row?.title).toBe("Updated Hero");
			expect(row?.description).toBe("Updated description");
		});

		it("updates existing content", async () => {
			const seed = createTestSeed();
			await applySeed(db, seed, { includeContent: true });

			const updatedSeed = createUpdatedSeed();
			const result = await applySeed(db, updatedSeed, {
				includeContent: true,
				onConflict: "update",
			});

			expect(result.content.updated).toBe(1);
			expect(result.content.created).toBe(0);

			// Verify the content was actually updated
			const row = await db
				.selectFrom("ec_posts" as any)
				.selectAll()
				.where("slug", "=", "hello-world")
				.executeTakeFirstOrThrow();
			expect((row as Record<string, unknown>).title).toBe("Hello World Updated");
			expect((row as Record<string, unknown>).body).toBe("Updated body");
		});
	});

	describe("onConflict: error", () => {
		it("throws on existing collection", async () => {
			const seed = createTestSeed();
			await applySeed(db, seed, { includeContent: true });

			await expect(
				applySeed(db, seed, {
					includeContent: true,
					onConflict: "error",
				}),
			).rejects.toThrow('Conflict: collection "posts" already exists');
		});

		it("throws on existing byline", async () => {
			// Seed without collections to get past collections step
			const seed = createTestSeed({ collections: [] });
			await applySeed(db, seed);

			await expect(applySeed(db, seed, { onConflict: "error" })).rejects.toThrow(
				'Conflict: byline "jane-doe" already exists',
			);
		});

		it("throws on existing redirect", async () => {
			const seed = createTestSeed({
				collections: [],
				bylines: [],
				sections: [],
			});
			await applySeed(db, seed);

			await expect(applySeed(db, seed, { onConflict: "error" })).rejects.toThrow(
				'Conflict: redirect "/old-page" already exists',
			);
		});

		it("throws on existing section", async () => {
			const seed = createTestSeed({
				collections: [],
				bylines: [],
				redirects: [],
			});
			await applySeed(db, seed);

			await expect(applySeed(db, seed, { onConflict: "error" })).rejects.toThrow(
				'Conflict: section "hero" already exists',
			);
		});

		it("throws on existing content", async () => {
			// First apply creates collections and content
			const seed = createTestSeed({
				bylines: [],
				redirects: [],
				sections: [],
			});
			await applySeed(db, seed, { includeContent: true });

			// Second apply with only content (collections already exist, skip them)
			const contentOnlySeed = createTestSeed({
				collections: [],
				bylines: [],
				redirects: [],
				sections: [],
			});
			await expect(
				applySeed(db, contentOnlySeed, {
					includeContent: true,
					onConflict: "error",
				}),
			).rejects.toThrow('Conflict: content "hello-world" in "posts" already exists');
		});
	});

	describe("mixed scenarios", () => {
		it("creates new records alongside existing ones in update mode", async () => {
			const seed = createTestSeed();
			await applySeed(db, seed, { includeContent: true });

			// Add a new content entry to the seed
			const extendedSeed = createUpdatedSeed();
			const posts = extendedSeed.content!["posts"];
			if (!posts) throw new Error("posts missing from seed");
			posts.push({
				id: "post-2",
				slug: "second-post",
				status: "published",
				data: { title: "Second Post", body: "New content" },
			});

			const result = await applySeed(db, extendedSeed, {
				includeContent: true,
				onConflict: "update",
			});

			expect(result.content.updated).toBe(1);
			expect(result.content.created).toBe(1);
		});

		it("clears taxonomy assignments on content update when seed removes them", async () => {
			// Seed with a taxonomy and content that has taxonomy assignments
			const seed: SeedFile = {
				version: "1",
				collections: [
					{
						slug: "posts",
						label: "Posts",
						fields: [{ slug: "title", label: "Title", type: "string" }],
					},
				],
				taxonomies: [
					{
						name: "categories",
						label: "Categories",
						hierarchical: false,
						collections: ["posts"],
						terms: [
							{ slug: "news", label: "News" },
							{ slug: "tech", label: "Tech" },
						],
					},
				],
				content: {
					posts: [
						{
							id: "post-1",
							slug: "hello-world",
							status: "published",
							data: { title: "Hello" },
							taxonomies: { categories: ["news", "tech"] },
						},
					],
				},
			};
			await applySeed(db, seed, { includeContent: true });

			// Verify both terms are attached
			const beforeRows = await db
				.selectFrom("content_taxonomies")
				.selectAll()
				.where("collection", "=", "posts")
				.execute();
			expect(beforeRows).toHaveLength(2);

			// Re-apply with only one taxonomy term
			const updatedSeed: SeedFile = {
				version: "1",
				collections: [
					{
						slug: "posts",
						label: "Posts",
						fields: [{ slug: "title", label: "Title", type: "string" }],
					},
				],
				taxonomies: [
					{
						name: "categories",
						label: "Categories",
						hierarchical: false,
						collections: ["posts"],
						terms: [
							{ slug: "news", label: "News" },
							{ slug: "tech", label: "Tech" },
						],
					},
				],
				content: {
					posts: [
						{
							id: "post-1",
							slug: "hello-world",
							status: "published",
							data: { title: "Hello Updated" },
							taxonomies: { categories: ["tech"] },
						},
					],
				},
			};

			await applySeed(db, updatedSeed, {
				includeContent: true,
				onConflict: "update",
			});

			// Should only have "tech" now, not both
			const afterRows = await db
				.selectFrom("content_taxonomies")
				.selectAll()
				.where("collection", "=", "posts")
				.execute();
			expect(afterRows).toHaveLength(1);
		});

		it("clears byline assignments on content update when seed removes them", async () => {
			const seed: SeedFile = {
				version: "1",
				collections: [
					{
						slug: "posts",
						label: "Posts",
						fields: [{ slug: "title", label: "Title", type: "string" }],
					},
				],
				bylines: [{ id: "byline-1", slug: "jane-doe", displayName: "Jane Doe" }],
				content: {
					posts: [
						{
							id: "post-1",
							slug: "hello-world",
							status: "published",
							data: { title: "Hello" },
							bylines: [{ byline: "byline-1" }],
						},
					],
				},
			};
			await applySeed(db, seed, { includeContent: true });

			// Verify byline is attached
			const beforeRows = await db
				.selectFrom("_emdash_content_bylines")
				.selectAll()
				.where("collection_slug", "=", "posts")
				.execute();
			expect(beforeRows).toHaveLength(1);

			// Re-apply without bylines on the content entry
			const updatedSeed: SeedFile = {
				version: "1",
				collections: [
					{
						slug: "posts",
						label: "Posts",
						fields: [{ slug: "title", label: "Title", type: "string" }],
					},
				],
				bylines: [{ id: "byline-1", slug: "jane-doe", displayName: "Jane Doe" }],
				content: {
					posts: [
						{
							id: "post-1",
							slug: "hello-world",
							status: "published",
							data: { title: "Hello Updated" },
							// No bylines -- should clear existing
						},
					],
				},
			};

			await applySeed(db, updatedSeed, {
				includeContent: true,
				onConflict: "update",
			});

			// Should have no bylines now
			const afterRows = await db
				.selectFrom("_emdash_content_bylines")
				.selectAll()
				.where("collection_slug", "=", "posts")
				.execute();
			expect(afterRows).toHaveLength(0);
		});
	});
});
