/**
 * MCP content tools — coverage for the remaining tools and edges.
 *
 * Covers:
 *   - content_duplicate
 *   - content_permanent_delete
 *   - content_translations + locale handling on create/get
 *   - _rev optimistic concurrency (happy + race)
 *   - Soft-delete visibility (content_get / content_list filtering)
 *   - Edit-while-trashed
 *   - Idempotency (publish twice, unpublish-on-draft, schedule + publish)
 */

import { Role } from "@emdash-cms/auth";
import { sql, type Kysely } from "kysely";
import { afterEach, beforeEach, describe, expect, it } from "vitest";

import { ContentRepository } from "../../../src/database/repositories/content.js";
import type { Database } from "../../../src/database/types.js";
import {
	connectMcpHarness,
	extractJson,
	extractText,
	type McpHarness,
} from "../../utils/mcp-runtime.js";
import { setupTestDatabaseWithCollections, teardownTestDatabase } from "../../utils/test-db.js";

const ADMIN_ID = "user_admin";

// ---------------------------------------------------------------------------
// content_duplicate
// ---------------------------------------------------------------------------

describe("content_duplicate", () => {
	let db: Kysely<Database>;
	let harness: McpHarness;

	beforeEach(async () => {
		db = await setupTestDatabaseWithCollections();
		harness = await connectMcpHarness({ db, userId: ADMIN_ID, userRole: Role.ADMIN });
	});

	afterEach(async () => {
		if (harness) await harness.cleanup();
		await teardownTestDatabase(db);
	});

	it("creates a copy with new id and slug", async () => {
		const created = await harness.client.callTool({
			name: "content_create",
			arguments: { collection: "post", data: { title: "Original" }, slug: "original" },
		});
		const original = extractJson<{ item: { id: string; slug: string } }>(created).item;

		const dup = await harness.client.callTool({
			name: "content_duplicate",
			arguments: { collection: "post", id: original.id },
		});
		expect(dup.isError, extractText(dup)).toBeFalsy();
		const copy = extractJson<{ item: { id: string; slug: string; status: string } }>(dup).item;

		expect(copy.id).not.toBe(original.id);
		expect(copy.slug).not.toBe(original.slug);
		// Created as draft per tool description
		expect(copy.status).toBe("draft");
	});

	it("rejects duplicating a missing item", async () => {
		const result = await harness.client.callTool({
			name: "content_duplicate",
			arguments: { collection: "post", id: "01NEVER" },
		});
		expect(result.isError).toBe(true);
		expect(extractText(result)).toMatch(/\bNOT_FOUND\b|\bnot found\b/i);
	});

	it("rejects duplicating in non-existent collection", async () => {
		const result = await harness.client.callTool({
			name: "content_duplicate",
			arguments: { collection: "ghost", id: "01NEVER" },
		});
		expect(result.isError).toBe(true);
	});

	it("requires CONTRIBUTOR or higher", async () => {
		await harness.cleanup();
		harness = await connectMcpHarness({
			db,
			userId: "user_subscriber",
			userRole: Role.SUBSCRIBER,
		});
		const result = await harness.client.callTool({
			name: "content_duplicate",
			arguments: { collection: "post", id: "01ANY" },
		});
		expect(result.isError).toBe(true);
	});
});

// ---------------------------------------------------------------------------
// content_permanent_delete
// ---------------------------------------------------------------------------

describe("content_permanent_delete", () => {
	let db: Kysely<Database>;
	let harness: McpHarness;

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

	afterEach(async () => {
		if (harness) await harness.cleanup();
		await teardownTestDatabase(db);
	});

	async function seedTrashedItem(): Promise<string> {
		const repo = new ContentRepository(db);
		const item = await repo.create({
			type: "post",
			data: { title: "T" },
			slug: `t-${Math.random().toString(36).slice(2, 6)}`,
			status: "draft",
			authorId: ADMIN_ID,
		});
		await repo.delete("post", item.id);
		return item.id;
	}

	it("permanently deletes a trashed item (ADMIN)", async () => {
		const id = await seedTrashedItem();
		harness = await connectMcpHarness({ db, userId: ADMIN_ID, userRole: Role.ADMIN });

		const result = await harness.client.callTool({
			name: "content_permanent_delete",
			arguments: { collection: "post", id },
		});
		expect(result.isError, extractText(result)).toBeFalsy();

		// Verify it's gone — not even in trash
		const got = await harness.client.callTool({
			name: "content_get",
			arguments: { collection: "post", id },
		});
		expect(got.isError).toBe(true);
	});

	it("EDITOR cannot permanent-delete (ADMIN-only)", async () => {
		const id = await seedTrashedItem();
		harness = await connectMcpHarness({ db, userId: "user_editor", userRole: Role.EDITOR });

		const result = await harness.client.callTool({
			name: "content_permanent_delete",
			arguments: { collection: "post", id },
		});
		expect(result.isError).toBe(true);
	});

	it("returns NOT_FOUND for missing id", async () => {
		harness = await connectMcpHarness({ db, userId: ADMIN_ID, userRole: Role.ADMIN });
		const result = await harness.client.callTool({
			name: "content_permanent_delete",
			arguments: { collection: "post", id: "01NEVEREXISTED" },
		});
		expect(result.isError).toBe(true);
		expect(extractText(result)).toMatch(/\bNOT_FOUND\b|\bnot found\b/i);
		expect(extractText(result)).toContain("01NEVEREXISTED");
	});
});

// ---------------------------------------------------------------------------
// content_translations + locale handling
// ---------------------------------------------------------------------------

describe("content_translations + locale", () => {
	let db: Kysely<Database>;
	let harness: McpHarness;

	beforeEach(async () => {
		db = await setupTestDatabaseWithCollections();
		harness = await connectMcpHarness({ db, userId: ADMIN_ID, userRole: Role.ADMIN });
	});

	afterEach(async () => {
		if (harness) await harness.cleanup();
		await teardownTestDatabase(db);
	});

	it("creates a translation linked via translationOf", async () => {
		const en = await harness.client.callTool({
			name: "content_create",
			arguments: { collection: "post", data: { title: "Hello" }, locale: "en" },
		});
		const enId = extractJson<{ item: { id: string } }>(en).item.id;

		const fr = await harness.client.callTool({
			name: "content_create",
			arguments: {
				collection: "post",
				data: { title: "Bonjour" },
				locale: "fr",
				translationOf: enId,
			},
		});
		expect(fr.isError, extractText(fr)).toBeFalsy();

		const trans = await harness.client.callTool({
			name: "content_translations",
			arguments: { collection: "post", id: enId },
		});
		expect(trans.isError, extractText(trans)).toBeFalsy();
		const data = extractJson<{
			translations: Array<{ id: string; locale: string }>;
		}>(trans);
		const locales = data.translations.map((t) => t.locale).toSorted();
		expect(locales).toEqual(["en", "fr"]);
	});

	it("returns single-locale translations array for content with no other translations", async () => {
		const en = await harness.client.callTool({
			name: "content_create",
			arguments: { collection: "post", data: { title: "Standalone" }, locale: "en" },
		});
		const id = extractJson<{ item: { id: string } }>(en).item.id;

		const result = await harness.client.callTool({
			name: "content_translations",
			arguments: { collection: "post", id },
		});
		expect(result.isError, extractText(result)).toBeFalsy();
		const data = extractJson<{ translations: unknown[] }>(result);
		expect(data.translations.length).toBeGreaterThanOrEqual(1);
	});

	it("content_get with locale param resolves slug per-locale", async () => {
		await harness.client.callTool({
			name: "content_create",
			arguments: { collection: "post", data: { title: "EN" }, slug: "shared", locale: "en" },
		});
		await harness.client.callTool({
			name: "content_create",
			arguments: { collection: "post", data: { title: "FR" }, slug: "shared", locale: "fr" },
		});

		const en = await harness.client.callTool({
			name: "content_get",
			arguments: { collection: "post", id: "shared", locale: "en" },
		});
		expect(en.isError, extractText(en)).toBeFalsy();
		const fr = await harness.client.callTool({
			name: "content_get",
			arguments: { collection: "post", id: "shared", locale: "fr" },
		});
		expect(fr.isError, extractText(fr)).toBeFalsy();

		const enItem = extractJson<{
			item: { locale: string; data?: { title?: unknown }; title?: unknown };
		}>(en).item;
		const frItem = extractJson<{
			item: { locale: string; data?: { title?: unknown }; title?: unknown };
		}>(fr).item;
		const enTitle = enItem.data?.title ?? enItem.title;
		const frTitle = frItem.data?.title ?? frItem.title;
		expect(enTitle).toBe("EN");
		expect(frTitle).toBe("FR");
	});

	it("rejects translationOf pointing to a non-existent item", async () => {
		const result = await harness.client.callTool({
			name: "content_create",
			arguments: {
				collection: "post",
				data: { title: "Orphan" },
				locale: "fr",
				translationOf: "01NEVEREXISTED",
			},
		});
		expect(result.isError).toBe(true);
	});
});

// ---------------------------------------------------------------------------
// _rev optimistic concurrency
// ---------------------------------------------------------------------------

describe("_rev optimistic concurrency", () => {
	let db: Kysely<Database>;
	let harness: McpHarness;

	beforeEach(async () => {
		db = await setupTestDatabaseWithCollections();
		harness = await connectMcpHarness({ db, userId: ADMIN_ID, userRole: Role.ADMIN });
	});

	afterEach(async () => {
		if (harness) await harness.cleanup();
		await teardownTestDatabase(db);
	});

	it("content_get returns a _rev token", async () => {
		const created = await harness.client.callTool({
			name: "content_create",
			arguments: { collection: "post", data: { title: "T" } },
		});
		const id = extractJson<{ item: { id: string } }>(created).item.id;

		const got = await harness.client.callTool({
			name: "content_get",
			arguments: { collection: "post", id },
		});
		const data = extractJson<{ item: { id: string }; _rev?: string }>(got);
		expect(data._rev).toBeTruthy();
	});

	it("content_update with current _rev succeeds", async () => {
		const created = await harness.client.callTool({
			name: "content_create",
			arguments: { collection: "post", data: { title: "Original" } },
		});
		const id = extractJson<{ item: { id: string } }>(created).item.id;

		const got = await harness.client.callTool({
			name: "content_get",
			arguments: { collection: "post", id },
		});
		const rev = extractJson<{ _rev: string }>(got)._rev;

		const updated = await harness.client.callTool({
			name: "content_update",
			arguments: { collection: "post", id, data: { title: "Updated" }, _rev: rev },
		});
		expect(updated.isError, extractText(updated)).toBeFalsy();
	});

	it("content_update with stale _rev returns CONFLICT-style error", async () => {
		const created = await harness.client.callTool({
			name: "content_create",
			arguments: { collection: "post", data: { title: "Original" } },
		});
		const id = extractJson<{ item: { id: string } }>(created).item.id;

		const got = await harness.client.callTool({
			name: "content_get",
			arguments: { collection: "post", id },
		});
		const oldRev = extractJson<{ _rev: string }>(got)._rev;

		// First update: succeeds and bumps the rev
		await harness.client.callTool({
			name: "content_update",
			arguments: { collection: "post", id, data: { title: "Update 1" }, _rev: oldRev },
		});

		// Second update with stale rev: should conflict
		const result = await harness.client.callTool({
			name: "content_update",
			arguments: { collection: "post", id, data: { title: "Update 2" }, _rev: oldRev },
		});
		expect(result.isError).toBe(true);
		expect(extractText(result)).toMatch(/conflict|stale|outdated|modified|rev/i);
	});

	it("content_update without _rev still succeeds (opt-in concurrency)", async () => {
		const created = await harness.client.callTool({
			name: "content_create",
			arguments: { collection: "post", data: { title: "T" } },
		});
		const id = extractJson<{ item: { id: string } }>(created).item.id;

		const result = await harness.client.callTool({
			name: "content_update",
			arguments: { collection: "post", id, data: { title: "U" } },
		});
		expect(result.isError, extractText(result)).toBeFalsy();
	});
});

// ---------------------------------------------------------------------------
// Soft-delete visibility
// ---------------------------------------------------------------------------

describe("soft-delete visibility", () => {
	let db: Kysely<Database>;
	let harness: McpHarness;

	beforeEach(async () => {
		db = await setupTestDatabaseWithCollections();
		harness = await connectMcpHarness({ db, userId: ADMIN_ID, userRole: Role.ADMIN });
	});

	afterEach(async () => {
		if (harness) await harness.cleanup();
		await teardownTestDatabase(db);
	});

	it("content_get on a trashed item returns NOT_FOUND (not the item)", async () => {
		const created = await harness.client.callTool({
			name: "content_create",
			arguments: { collection: "post", data: { title: "T" } },
		});
		const id = extractJson<{ item: { id: string } }>(created).item.id;

		await harness.client.callTool({
			name: "content_delete",
			arguments: { collection: "post", id },
		});

		const got = await harness.client.callTool({
			name: "content_get",
			arguments: { collection: "post", id },
		});
		expect(got.isError).toBe(true);
		expect(extractText(got)).toMatch(/\bNOT_FOUND\b|\bnot found\b/i);
	});

	it("content_list does NOT include trashed items by default", async () => {
		const a = await harness.client.callTool({
			name: "content_create",
			arguments: { collection: "post", data: { title: "Live" } },
		});
		const b = await harness.client.callTool({
			name: "content_create",
			arguments: { collection: "post", data: { title: "Trashed" } },
		});
		const trashedId = extractJson<{ item: { id: string } }>(b).item.id;

		await harness.client.callTool({
			name: "content_delete",
			arguments: { collection: "post", id: trashedId },
		});

		const list = await harness.client.callTool({
			name: "content_list",
			arguments: { collection: "post" },
		});
		const ids = extractJson<{ items: Array<{ id: string }> }>(list).items.map((i) => i.id);
		expect(ids).not.toContain(trashedId);
		expect(ids).toContain(extractJson<{ item: { id: string } }>(a).item.id);
	});

	it("content_list_trashed returns only trashed items", async () => {
		await harness.client.callTool({
			name: "content_create",
			arguments: { collection: "post", data: { title: "Live" } },
		});
		const b = await harness.client.callTool({
			name: "content_create",
			arguments: { collection: "post", data: { title: "Trashed" } },
		});
		await harness.client.callTool({
			name: "content_delete",
			arguments: {
				collection: "post",
				id: extractJson<{ item: { id: string } }>(b).item.id,
			},
		});

		const trashed = await harness.client.callTool({
			name: "content_list_trashed",
			arguments: { collection: "post" },
		});
		const items = extractJson<{ items: Array<{ id: string }> }>(trashed).items;
		expect(items).toHaveLength(1);
		expect(items[0]?.id).toBe(extractJson<{ item: { id: string } }>(b).item.id);
	});
});

describe("edit-while-trashed", () => {
	let db: Kysely<Database>;
	let harness: McpHarness;

	beforeEach(async () => {
		db = await setupTestDatabaseWithCollections();
		harness = await connectMcpHarness({ db, userId: ADMIN_ID, userRole: Role.ADMIN });
	});

	afterEach(async () => {
		if (harness) await harness.cleanup();
		await teardownTestDatabase(db);
	});

	it("content_update on a trashed item is rejected (item not visible)", async () => {
		const created = await harness.client.callTool({
			name: "content_create",
			arguments: { collection: "post", data: { title: "T" } },
		});
		const id = extractJson<{ item: { id: string } }>(created).item.id;

		await harness.client.callTool({
			name: "content_delete",
			arguments: { collection: "post", id },
		});

		const updated = await harness.client.callTool({
			name: "content_update",
			arguments: { collection: "post", id, data: { title: "Edit while dead" } },
		});
		expect(updated.isError).toBe(true);
		expect(extractText(updated)).toMatch(/\bNOT_FOUND\b|\bnot found\b|trash/i);
	});

	it("content_publish on a trashed item is rejected", async () => {
		const created = await harness.client.callTool({
			name: "content_create",
			arguments: { collection: "post", data: { title: "T" } },
		});
		const id = extractJson<{ item: { id: string } }>(created).item.id;

		await harness.client.callTool({
			name: "content_delete",
			arguments: { collection: "post", id },
		});

		const result = await harness.client.callTool({
			name: "content_publish",
			arguments: { collection: "post", id },
		});
		expect(result.isError).toBe(true);
	});
});

// ---------------------------------------------------------------------------
// Idempotency
// ---------------------------------------------------------------------------

describe("idempotency", () => {
	let db: Kysely<Database>;
	let harness: McpHarness;

	beforeEach(async () => {
		db = await setupTestDatabaseWithCollections();
		harness = await connectMcpHarness({ db, userId: ADMIN_ID, userRole: Role.ADMIN });
	});

	afterEach(async () => {
		if (harness) await harness.cleanup();
		await teardownTestDatabase(db);
	});

	it("publish twice is idempotent: second call succeeds, status stays published, publishedAt is preserved", async () => {
		const created = await harness.client.callTool({
			name: "content_create",
			arguments: { collection: "post", data: { title: "T" } },
		});
		const id = extractJson<{ item: { id: string } }>(created).item.id;

		const first = await harness.client.callTool({
			name: "content_publish",
			arguments: { collection: "post", id },
		});
		expect(first.isError, extractText(first)).toBeFalsy();
		const firstItem = extractJson<{
			item: { status: string; publishedAt: string | null };
		}>(first).item;
		expect(firstItem.status).toBe("published");
		expect(firstItem.publishedAt).toBeTruthy();

		// Pin publishedAt to a known fixed value so the comparison can't be
		// satisfied by coincidence (two publishes within the same ms would
		// produce identical ISO strings even on a regression that drops the
		// COALESCE preservation).
		const KNOWN = "2020-01-01T00:00:00.000Z";
		await sql`UPDATE ec_post SET published_at = ${KNOWN} WHERE id = ${id}`.execute(db);

		const second = await harness.client.callTool({
			name: "content_publish",
			arguments: { collection: "post", id },
		});
		// Contract: publish is idempotent. Second call succeeds, status
		// remains published, and publishedAt is preserved (the repository
		// uses COALESCE so the existing timestamp survives a re-publish).
		expect(second.isError, extractText(second)).toBeFalsy();
		const secondItem = extractJson<{
			item: { status: string; publishedAt: string | null };
		}>(second).item;
		expect(secondItem.status).toBe("published");
		expect(secondItem.publishedAt).toBe(KNOWN);
	});

	it("unpublish on a draft (already unpublished) is idempotent: status stays draft", async () => {
		const created = await harness.client.callTool({
			name: "content_create",
			arguments: { collection: "post", data: { title: "T" } },
		});
		const createdItem = extractJson<{
			item: { id: string; version: number };
		}>(created).item;
		const id = createdItem.id;
		const versionBefore = createdItem.version;

		// Item is born as draft. Contract: unpublish is idempotent — succeeds
		// and the item stays draft.
		const result = await harness.client.callTool({
			name: "content_unpublish",
			arguments: { collection: "post", id },
		});
		expect(result.isError, extractText(result)).toBeFalsy();
		const item = extractJson<{
			item: { status: string; publishedAt: string | null; version: number };
		}>(result).item;
		expect(item.status).toBe("draft");
		expect(item.publishedAt).toBeNull();
		// Idempotent: nothing meaningful changed. A regression that always
		// bumps the version or creates a phantom revision would surface here.
		// (updated_at can tick because the UPDATE re-runs; version is the
		// stricter invariant.)
		expect(item.version).toBe(versionBefore);
	});

	it("schedule then publish: schedule is preserved or cleared cleanly", async () => {
		const created = await harness.client.callTool({
			name: "content_create",
			arguments: { collection: "post", data: { title: "T" } },
		});
		const id = extractJson<{ item: { id: string } }>(created).item.id;

		const future = new Date(Date.now() + 3600_000).toISOString();
		await harness.client.callTool({
			name: "content_schedule",
			arguments: { collection: "post", id, scheduledAt: future },
		});

		const publish = await harness.client.callTool({
			name: "content_publish",
			arguments: { collection: "post", id },
		});
		expect(publish.isError, extractText(publish)).toBeFalsy();

		const got = await harness.client.callTool({
			name: "content_get",
			arguments: { collection: "post", id },
		});
		const item = extractJson<{
			item: { status: string; scheduledAt: string | null };
		}>(got).item;
		expect(item.status).toBe("published");
		// Once published, the future schedule is moot — should be cleared.
		expect(item.scheduledAt).toBeNull();
	});

	it("delete twice is safe — second call returns NOT_FOUND, not a crash", async () => {
		const created = await harness.client.callTool({
			name: "content_create",
			arguments: { collection: "post", data: { title: "T" } },
		});
		const id = extractJson<{ item: { id: string } }>(created).item.id;

		await harness.client.callTool({
			name: "content_delete",
			arguments: { collection: "post", id },
		});
		const second = await harness.client.callTool({
			name: "content_delete",
			arguments: { collection: "post", id },
		});
		expect(second.isError).toBe(true);
		expect(extractText(second)).toMatch(/\bNOT_FOUND\b|\bnot found\b/i);
	});
});

// ---------------------------------------------------------------------------
// content_unschedule gap (no MCP tool for this, only on runtime)
// ---------------------------------------------------------------------------

describe("content_unschedule gap", () => {
	let db: Kysely<Database>;
	let harness: McpHarness;

	beforeEach(async () => {
		db = await setupTestDatabaseWithCollections();
		harness = await connectMcpHarness({ db, userId: ADMIN_ID, userRole: Role.ADMIN });
	});

	afterEach(async () => {
		if (harness) await harness.cleanup();
		await teardownTestDatabase(db);
	});

	it("MCP exposes content_unschedule", async () => {
		const tools = await harness.client.listTools();
		const names = tools.tools.map((t) => t.name);
		expect(names).toContain("content_unschedule");
	});

	it("schedule + unschedule clears scheduledAt and re-publish still works (F12)", async () => {
		// Create a draft item.
		const created = await harness.client.callTool({
			name: "content_create",
			arguments: { collection: "post", data: { title: "Scheduled post" } },
		});
		const id = extractJson<{ item: { id: string } }>(created).item.id;

		// Schedule for the near future.
		const future = new Date(Date.now() + 60_000).toISOString();
		const schedule = await harness.client.callTool({
			name: "content_schedule",
			arguments: { collection: "post", id, scheduledAt: future },
		});
		expect(schedule.isError, extractText(schedule)).toBeFalsy();

		// Sanity: scheduledAt is set.
		const afterSchedule = await harness.client.callTool({
			name: "content_get",
			arguments: { collection: "post", id },
		});
		const scheduled = extractJson<{ item: { scheduledAt: string | null; status: string } }>(
			afterSchedule,
		).item;
		expect(scheduled.scheduledAt).toBeTruthy();

		// Unschedule.
		const unschedule = await harness.client.callTool({
			name: "content_unschedule",
			arguments: { collection: "post", id },
		});
		expect(unschedule.isError, extractText(unschedule)).toBeFalsy();

		// scheduledAt is now null.
		const afterUnschedule = await harness.client.callTool({
			name: "content_get",
			arguments: { collection: "post", id },
		});
		const cleared = extractJson<{ item: { scheduledAt: string | null } }>(afterUnschedule).item;
		expect(cleared.scheduledAt).toBeNull();

		// Re-publish still works after unschedule.
		const republish = await harness.client.callTool({
			name: "content_publish",
			arguments: { collection: "post", id },
		});
		expect(republish.isError, extractText(republish)).toBeFalsy();
		const final = extractJson<{ item: { status: string } }>(republish).item;
		expect(final.status).toBe("published");
	});
});
