/**
 * MCP taxonomy tools — comprehensive integration tests.
 *
 * Covers:
 *   - taxonomy_list
 *   - taxonomy_list_terms
 *   - taxonomy_create_term
 *
 * Plus regression coverage for:
 *   - bug #7 (orphan taxonomy collection inconsistency)
 *   - bug #13 (no delete/update term tool — gap test)
 */

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

import { handleTaxonomyCreate } from "../../../src/api/handlers/taxonomies.js";
import type { Database } from "../../../src/database/types.js";
import { SchemaRegistry } from "../../../src/schema/registry.js";
import {
	connectMcpHarness,
	extractJson,
	extractText,
	type McpHarness,
} from "../../utils/mcp-runtime.js";
import { setupTestDatabase, teardownTestDatabase } from "../../utils/test-db.js";

const ADMIN_ID = "user_admin";
const AUTHOR_ID = "user_author";
const SUBSCRIBER_ID = "user_subscriber";

async function setupTaxonomy(
	db: Kysely<Database>,
	input: { name: string; label: string; hierarchical?: boolean; collections?: string[] },
): Promise<void> {
	const result = await handleTaxonomyCreate(db, input);
	if (!result.success) {
		throw new Error(`Failed to set up taxonomy: ${result.error?.message}`);
	}
}

// ---------------------------------------------------------------------------
// taxonomy_list
// ---------------------------------------------------------------------------

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

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

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

	it("returns only the seeded defaults when no extra taxonomies are added", async () => {
		// Migration 006 seeds two default taxonomies: 'category' (hierarchical)
		// and 'tag' (flat), both linked to the 'posts' collection. A fresh
		// install always has these.
		harness = await connectMcpHarness({ db, userId: ADMIN_ID, userRole: Role.ADMIN });
		const result = await harness.client.callTool({
			name: "taxonomy_list",
			arguments: {},
		});
		expect(result.isError, extractText(result)).toBeFalsy();
		const { taxonomies } = extractJson<{
			taxonomies: Array<{ name: string }>;
		}>(result);
		const names = taxonomies.map((t) => t.name).toSorted();
		expect(names).toEqual(["category", "tag"]);
	});

	it("lists user-created taxonomies alongside the defaults", async () => {
		const registry = new SchemaRegistry(db);
		await registry.createCollection({ slug: "post", label: "Posts" });
		// Use names that don't collide with the seeded `category` / `tag`.
		await setupTaxonomy(db, {
			name: "section",
			label: "Sections",
			hierarchical: true,
			collections: ["post"],
		});
		await setupTaxonomy(db, {
			name: "topic",
			label: "Topics",
			collections: ["post"],
		});

		harness = await connectMcpHarness({ db, userId: ADMIN_ID, userRole: Role.ADMIN });
		const result = await harness.client.callTool({
			name: "taxonomy_list",
			arguments: {},
		});
		const { taxonomies } = extractJson<{
			taxonomies: Array<{ name: string; hierarchical?: boolean; collections?: string[] }>;
		}>(result);
		const names = taxonomies.map((t) => t.name).toSorted();
		expect(names).toEqual(["category", "section", "tag", "topic"]);

		const section = taxonomies.find((t) => t.name === "section");
		expect(section?.hierarchical).toBe(true);
		expect(section?.collections).toEqual(["post"]);
	});

	it("any logged-in user (SUBSCRIBER) can read taxonomies", async () => {
		harness = await connectMcpHarness({ db, userId: SUBSCRIBER_ID, userRole: Role.SUBSCRIBER });
		const result = await harness.client.callTool({
			name: "taxonomy_list",
			arguments: {},
		});
		expect(result.isError, extractText(result)).toBeFalsy();
	});

	it("bug #7: orphaned collection slugs are filtered from taxonomy_list output", async () => {
		// The seed taxonomies (category, tag) both reference 'posts' — a
		// collection that doesn't exist in this test DB (no auto-seed). After
		// the bug #7 fix, `taxonomy_list` filters those orphans out. We don't
		// need to manufacture an orphan; the seed already gives us one.
		harness = await connectMcpHarness({ db, userId: ADMIN_ID, userRole: Role.ADMIN });

		const taxResult = await harness.client.callTool({
			name: "taxonomy_list",
			arguments: {},
		});
		const { taxonomies } = extractJson<{
			taxonomies: Array<{ name: string; collections?: string[] }>;
		}>(taxResult);

		// Each seeded taxonomy referenced 'posts'. After filtering, that
		// orphan slug is gone — the array should be empty for both seeds.
		for (const t of taxonomies) {
			expect(t.collections).not.toContain("posts");
		}

		// And schema_list_collections agrees: there is no 'posts' collection.
		const collResult = await harness.client.callTool({
			name: "schema_list_collections",
			arguments: {},
		});
		const { items } = extractJson<{ items: Array<{ slug: string }> }>(collResult);
		expect(items.find((c) => c.slug === "posts")).toBeUndefined();
	});
});

// ---------------------------------------------------------------------------
// taxonomy_list_terms
// ---------------------------------------------------------------------------

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

	beforeEach(async () => {
		db = await setupTestDatabase();
		await setupTaxonomy(db, { name: "categories", label: "Categories", hierarchical: true });
	});

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

	it("returns empty list when taxonomy has no terms", async () => {
		harness = await connectMcpHarness({ db, userId: ADMIN_ID, userRole: Role.ADMIN });
		const result = await harness.client.callTool({
			name: "taxonomy_list_terms",
			arguments: { taxonomy: "categories" },
		});
		expect(result.isError, extractText(result)).toBeFalsy();
		const { items } = extractJson<{ items: unknown[] }>(result);
		expect(items).toEqual([]);
	});

	it("returns terms after creation", async () => {
		harness = await connectMcpHarness({ db, userId: ADMIN_ID, userRole: Role.ADMIN });
		await harness.client.callTool({
			name: "taxonomy_create_term",
			arguments: { taxonomy: "categories", slug: "tech", label: "Tech" },
		});
		await harness.client.callTool({
			name: "taxonomy_create_term",
			arguments: { taxonomy: "categories", slug: "design", label: "Design" },
		});

		const result = await harness.client.callTool({
			name: "taxonomy_list_terms",
			arguments: { taxonomy: "categories" },
		});
		const { items } = extractJson<{
			items: Array<{ slug: string; label: string; parentId: string | null }>;
		}>(result);
		const slugs = items.map((t) => t.slug).toSorted();
		expect(slugs).toEqual(["design", "tech"]);
	});

	it("returns clear error for missing taxonomy name", async () => {
		harness = await connectMcpHarness({ db, userId: ADMIN_ID, userRole: Role.ADMIN });
		const result = await harness.client.callTool({
			name: "taxonomy_list_terms",
			arguments: { taxonomy: "nonexistent" },
		});
		expect(result.isError).toBe(true);
		expect(extractText(result)).toMatch(/\bNOT_FOUND\b|\bnot found\b/i);
		expect(extractText(result)).toContain("nonexistent");
	});

	it("paginates with limit + cursor", async () => {
		harness = await connectMcpHarness({ db, userId: ADMIN_ID, userRole: Role.ADMIN });
		// Insert 5 terms — labels chosen so alphabetical ordering is predictable
		for (const label of ["alpha", "bravo", "charlie", "delta", "echo"]) {
			await harness.client.callTool({
				name: "taxonomy_create_term",
				arguments: { taxonomy: "categories", slug: label, label },
			});
		}

		const page1 = await harness.client.callTool({
			name: "taxonomy_list_terms",
			arguments: { taxonomy: "categories", limit: 2 },
		});
		const p1 = extractJson<{ items: Array<{ slug: string; id: string }>; nextCursor?: string }>(
			page1,
		);
		expect(p1.items).toHaveLength(2);
		expect(p1.nextCursor).toBeTruthy();

		const page2 = await harness.client.callTool({
			name: "taxonomy_list_terms",
			arguments: { taxonomy: "categories", limit: 2, cursor: p1.nextCursor },
		});
		const p2 = extractJson<{ items: Array<{ slug: string }>; nextCursor?: string }>(page2);
		expect(p2.items).toHaveLength(2);

		// No overlap
		const p1Slugs = p1.items.map((i) => i.slug);
		for (const t of p2.items) expect(p1Slugs).not.toContain(t.slug);
	});

	it("paginates correctly when multiple terms share the same label", async () => {
		// Keyset pagination over (label, id) needs a stable id tiebreaker
		// at the SQL layer or tied-label rows can swap order between calls
		// — producing duplicates or skipped items. Three terms share
		// label "shared"; pagination must walk through them in a stable
		// order with no duplicates and no gaps.
		harness = await connectMcpHarness({ db, userId: ADMIN_ID, userRole: Role.ADMIN });
		const slugs = ["shared-1", "shared-2", "shared-3", "unique-a"];
		for (const slug of slugs) {
			await harness.client.callTool({
				name: "taxonomy_create_term",
				arguments: {
					taxonomy: "categories",
					slug,
					label: slug.startsWith("shared") ? "shared" : slug,
				},
			});
		}

		// Walk one item at a time so every cursor transition exercises the
		// (label, id) keyset.
		const collected: string[] = [];
		let cursor: string | undefined;
		// Hard cap to prevent the test hanging if pagination loops.
		for (let i = 0; i < 10; i++) {
			const page = await harness.client.callTool({
				name: "taxonomy_list_terms",
				arguments: { taxonomy: "categories", limit: 1, ...(cursor ? { cursor } : {}) },
			});
			const data = extractJson<{
				items: Array<{ slug: string; id: string }>;
				nextCursor?: string;
			}>(page);
			if (data.items.length === 0) break;
			for (const item of data.items) collected.push(item.slug);
			if (!data.nextCursor) break;
			cursor = data.nextCursor;
		}

		// Each slug appears exactly once. Order doesn't matter for this
		// assertion — just no duplicates and no missing entries.
		expect(collected.toSorted()).toEqual(slugs.toSorted());
	});

	it("survives concurrent deletion of the cursor-term", async () => {
		// The base64 keyset cursor encodes a (label, id) position rather
		// than a row reference, so deleting the cursor-term between pages
		// must not error — the next page just continues from the next
		// position in sort order.
		harness = await connectMcpHarness({ db, userId: ADMIN_ID, userRole: Role.ADMIN });
		for (const slug of ["alpha", "bravo", "charlie", "delta"]) {
			await harness.client.callTool({
				name: "taxonomy_create_term",
				arguments: { taxonomy: "categories", slug, label: slug },
			});
		}

		const page1 = await harness.client.callTool({
			name: "taxonomy_list_terms",
			arguments: { taxonomy: "categories", limit: 2 },
		});
		const p1 = extractJson<{
			items: Array<{ slug: string }>;
			nextCursor?: string;
		}>(page1);
		expect(p1.items.map((i) => i.slug)).toEqual(["alpha", "bravo"]);
		expect(p1.nextCursor).toBeTruthy();

		// Delete the cursor-term ('bravo') out of band.
		const { TaxonomyRepository } = await import("../../../src/database/repositories/taxonomy.js");
		const repo = new TaxonomyRepository(db);
		const bravo = await repo.findBySlug("categories", "bravo");
		if (!bravo) throw new Error("bravo missing — fixture broken");
		await db.deleteFrom("taxonomies").where("id", "=", bravo.id).execute();

		// Page 2 must still work and return the items strictly after the
		// cursor's position. Pre-fix the cursor stored 'bravo's id and
		// findIndex would have returned -1 → INVALID_CURSOR. Post-fix the
		// cursor stores ('bravo', '<bravo-id>') and the keyset comparison
		// finds the first term with (label, id) > ('bravo', '<bravo-id>')
		// — that's 'charlie'.
		const page2 = await harness.client.callTool({
			name: "taxonomy_list_terms",
			arguments: { taxonomy: "categories", limit: 2, cursor: p1.nextCursor },
		});
		expect(page2.isError, extractText(page2)).toBeFalsy();
		const p2 = extractJson<{ items: Array<{ slug: string }> }>(page2);
		expect(p2.items.map((i) => i.slug)).toEqual(["charlie", "delta"]);
	});

	it("malformed cursor returns INVALID_CURSOR", async () => {
		harness = await connectMcpHarness({ db, userId: ADMIN_ID, userRole: Role.ADMIN });
		await harness.client.callTool({
			name: "taxonomy_create_term",
			arguments: { taxonomy: "categories", slug: "t1", label: "T1" },
		});

		// taxonomy_list_terms uses a base64 keyset cursor over (label, id).
		// A completely bogus value fails decodeCursor and surfaces as a
		// structured INVALID_CURSOR error.
		const result = await harness.client.callTool({
			name: "taxonomy_list_terms",
			arguments: { taxonomy: "categories", cursor: "garbage_cursor_xyz" },
		});
		expect(result.isError).toBe(true);
		const meta = (result as { _meta?: { code?: string } })._meta;
		expect(meta?.code).toBe("INVALID_CURSOR");
	});

	it("any logged-in user (SUBSCRIBER) can read terms", async () => {
		harness = await connectMcpHarness({ db, userId: SUBSCRIBER_ID, userRole: Role.SUBSCRIBER });
		const result = await harness.client.callTool({
			name: "taxonomy_list_terms",
			arguments: { taxonomy: "categories" },
		});
		expect(result.isError, extractText(result)).toBeFalsy();
	});
});

// ---------------------------------------------------------------------------
// taxonomy_create_term
// ---------------------------------------------------------------------------

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

	beforeEach(async () => {
		db = await setupTestDatabase();
		await setupTaxonomy(db, { name: "categories", label: "Categories", hierarchical: true });
		await setupTaxonomy(db, { name: "tags", label: "Tags" });
	});

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

	it("creates a term with minimal arguments", async () => {
		harness = await connectMcpHarness({ db, userId: ADMIN_ID, userRole: Role.ADMIN });
		const result = await harness.client.callTool({
			name: "taxonomy_create_term",
			arguments: { taxonomy: "categories", slug: "tech", label: "Tech" },
		});
		expect(result.isError, extractText(result)).toBeFalsy();
		const { term } = extractJson<{ term: { slug: string; label: string } }>(result);
		expect(term.slug).toBe("tech");
		expect(term.label).toBe("Tech");
	});

	it("creates a child term with parentId", async () => {
		harness = await connectMcpHarness({ db, userId: ADMIN_ID, userRole: Role.ADMIN });
		const parent = await harness.client.callTool({
			name: "taxonomy_create_term",
			arguments: { taxonomy: "categories", slug: "tech", label: "Tech" },
		});
		const parentId = extractJson<{ term: { id: string } }>(parent).term.id;

		const child = await harness.client.callTool({
			name: "taxonomy_create_term",
			arguments: {
				taxonomy: "categories",
				slug: "ai",
				label: "AI",
				parentId,
			},
		});
		expect(child.isError, extractText(child)).toBeFalsy();
		const { term } = extractJson<{ term: { parentId: string | null } }>(child);
		expect(term.parentId).toBe(parentId);
	});

	it("rejects duplicate slug within the same taxonomy", async () => {
		harness = await connectMcpHarness({ db, userId: ADMIN_ID, userRole: Role.ADMIN });
		await harness.client.callTool({
			name: "taxonomy_create_term",
			arguments: { taxonomy: "categories", slug: "tech", label: "Tech" },
		});
		const result = await harness.client.callTool({
			name: "taxonomy_create_term",
			arguments: { taxonomy: "categories", slug: "tech", label: "Tech 2" },
		});
		expect(result.isError).toBe(true);
		expect(extractText(result)).toMatch(/exist|duplicate|conflict|unique|already/i);
	});

	it("allows same slug across different taxonomies", async () => {
		harness = await connectMcpHarness({ db, userId: ADMIN_ID, userRole: Role.ADMIN });
		const a = await harness.client.callTool({
			name: "taxonomy_create_term",
			arguments: { taxonomy: "categories", slug: "shared", label: "Shared" },
		});
		const b = await harness.client.callTool({
			name: "taxonomy_create_term",
			arguments: { taxonomy: "tags", slug: "shared", label: "Shared" },
		});
		expect(a.isError, extractText(a)).toBeFalsy();
		expect(b.isError, extractText(b)).toBeFalsy();
	});

	it("rejects creating a term in a non-existent taxonomy", async () => {
		harness = await connectMcpHarness({ db, userId: ADMIN_ID, userRole: Role.ADMIN });
		const result = await harness.client.callTool({
			name: "taxonomy_create_term",
			arguments: { taxonomy: "ghost", slug: "x", label: "X" },
		});
		expect(result.isError).toBe(true);
		expect(extractText(result)).toMatch(/\bNOT_FOUND\b|\bnot found\b/i);
		expect(extractText(result)).toContain("ghost");
	});

	it("rejects parentId pointing to a different taxonomy", async () => {
		harness = await connectMcpHarness({ db, userId: ADMIN_ID, userRole: Role.ADMIN });
		const tag = await harness.client.callTool({
			name: "taxonomy_create_term",
			arguments: { taxonomy: "tags", slug: "stuff", label: "Stuff" },
		});
		const tagId = extractJson<{ term: { id: string } }>(tag).term.id;

		const result = await harness.client.callTool({
			name: "taxonomy_create_term",
			arguments: {
				taxonomy: "categories",
				slug: "child",
				label: "Child",
				parentId: tagId,
			},
		});
		expect(result.isError).toBe(true);
	});

	it("rejects parentId pointing to a non-existent term", async () => {
		harness = await connectMcpHarness({ db, userId: ADMIN_ID, userRole: Role.ADMIN });
		const result = await harness.client.callTool({
			name: "taxonomy_create_term",
			arguments: {
				taxonomy: "categories",
				slug: "orphan",
				label: "Orphan",
				parentId: "01NEVEREXISTED",
			},
		});
		expect(result.isError).toBe(true);
	});

	it("requires EDITOR role (AUTHOR is blocked)", async () => {
		harness = await connectMcpHarness({ db, userId: AUTHOR_ID, userRole: Role.AUTHOR });
		const result = await harness.client.callTool({
			name: "taxonomy_create_term",
			arguments: { taxonomy: "categories", slug: "x", label: "X" },
		});
		expect(result.isError).toBe(true);
	});
});

// ---------------------------------------------------------------------------
// Bug #13 / F2 / F3 / F12 — happy paths for taxonomy_update_term and
// taxonomy_delete_term, plus parent validation, cycle detection, and
// empty-string rejection.
// ---------------------------------------------------------------------------

describe("taxonomy_update_term (bug #13 / F2 / F12)", () => {
	let db: Kysely<Database>;
	let harness: McpHarness;

	async function createTerm(
		taxonomy: string,
		slug: string,
		label: string,
		parentId?: string,
	): Promise<string> {
		const args: Record<string, unknown> = { taxonomy, slug, label };
		if (parentId) args.parentId = parentId;
		const result = await harness.client.callTool({
			name: "taxonomy_create_term",
			arguments: args,
		});
		expect(result.isError, extractText(result)).toBeFalsy();
		const { term } = extractJson<{ term: { id: string } }>(result);
		return term.id;
	}

	beforeEach(async () => {
		db = await setupTestDatabase();
		await setupTaxonomy(db, { name: "tags", label: "Tags" });
		await setupTaxonomy(db, { name: "sections", label: "Sections" });
		harness = await connectMcpHarness({ db, userId: ADMIN_ID, userRole: Role.ADMIN });
	});

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

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

	it("renames the slug when the new slug is free", async () => {
		await createTerm("tags", "old-slug", "Original");
		const result = await harness.client.callTool({
			name: "taxonomy_update_term",
			arguments: { taxonomy: "tags", termSlug: "old-slug", slug: "new-slug" },
		});
		expect(result.isError, extractText(result)).toBeFalsy();
		const { term } = extractJson<{ term: { slug: string } }>(result);
		expect(term.slug).toBe("new-slug");
	});

	it("changes the label", async () => {
		await createTerm("tags", "x", "Old Label");
		const result = await harness.client.callTool({
			name: "taxonomy_update_term",
			arguments: { taxonomy: "tags", termSlug: "x", label: "New Label" },
		});
		expect(result.isError, extractText(result)).toBeFalsy();
		const { term } = extractJson<{ term: { label: string } }>(result);
		expect(term.label).toBe("New Label");
	});

	it("reparents a term and detaches via parentId: null", async () => {
		const parentId = await createTerm("tags", "parent", "Parent");
		await createTerm("tags", "child", "Child");

		const reparent = await harness.client.callTool({
			name: "taxonomy_update_term",
			arguments: { taxonomy: "tags", termSlug: "child", parentId },
		});
		expect(reparent.isError, extractText(reparent)).toBeFalsy();
		const reparented = extractJson<{ term: { parentId: string | null } }>(reparent);
		expect(reparented.term.parentId).toBe(parentId);

		const detach = await harness.client.callTool({
			name: "taxonomy_update_term",
			arguments: { taxonomy: "tags", termSlug: "child", parentId: null },
		});
		expect(detach.isError, extractText(detach)).toBeFalsy();
		const detached = extractJson<{ term: { parentId: string | null } }>(detach);
		expect(detached.term.parentId).toBeNull();
	});

	it("rejects parents from a different taxonomy", async () => {
		const sectionId = await createTerm("sections", "news", "News");
		await createTerm("tags", "alpha", "Alpha");
		const result = await harness.client.callTool({
			name: "taxonomy_update_term",
			arguments: { taxonomy: "tags", termSlug: "alpha", parentId: sectionId },
		});
		expect(result.isError).toBe(true);
		expect(extractText(result)).toMatch(/VALIDATION_ERROR/);
	});

	it("rejects self-parent", async () => {
		const id = await createTerm("tags", "loop", "Loop");
		const result = await harness.client.callTool({
			name: "taxonomy_update_term",
			arguments: { taxonomy: "tags", termSlug: "loop", parentId: id },
		});
		expect(result.isError).toBe(true);
		expect(extractText(result)).toMatch(/own parent|VALIDATION_ERROR/i);
	});

	it("rejects a 2-cycle (descendant becoming ancestor)", async () => {
		// A is parent of B. Now try to make B the parent of A — that's a cycle.
		const aId = await createTerm("tags", "a", "A");
		const bId = await createTerm("tags", "b", "B", aId);
		const result = await harness.client.callTool({
			name: "taxonomy_update_term",
			arguments: { taxonomy: "tags", termSlug: "a", parentId: bId },
		});
		expect(result.isError).toBe(true);
		expect(extractText(result)).toMatch(/cycle|VALIDATION_ERROR/i);
	});

	it("rejects empty-string parentId on create", async () => {
		const result = await harness.client.callTool({
			name: "taxonomy_create_term",
			arguments: { taxonomy: "tags", slug: "x", label: "X", parentId: "" },
		});
		// Either returns a validation error, or treats it as no-parent.
		// We choose strict: empty string is normalized to undefined so it
		// succeeds with parentId === null (no parent attached). That's the
		// behavior we documented.
		if (result.isError) {
			expect(extractText(result)).toMatch(/VALIDATION_ERROR/);
		} else {
			const { term } = extractJson<{ term: { parentId: string | null } }>(result);
			expect(term.parentId).toBeNull();
		}
	});

	// ----- MAX_DEPTH boundary -----
	// validateParentTerm walks up the parent chain bounded by MAX_DEPTH=100
	// to prevent a pathological pre-existing cycle from hanging the
	// validator. The boundary is "more than 100 ancestors": exactly-100 is
	// accepted, 101+ is rejected.

	it("accepts a chain of exactly MAX_DEPTH (100) ancestors", async () => {
		const { TaxonomyRepository } = await import("../../../src/database/repositories/taxonomy.js");
		const repo = new TaxonomyRepository(db);
		// Build root → 1 → 2 → ... → 100. 101 terms total. The deepest
		// term has 100 ancestors; setting it as parent of a new term means
		// validateParentTerm walks 100 hops up before exhausting the chain.
		let parentId: string | undefined;
		const ids: string[] = [];
		for (let i = 0; i < 101; i++) {
			const term = await repo.create({
				name: "tags",
				slug: `chain-${i}`,
				label: `Chain ${i}`,
				parentId,
			});
			ids.push(term.id);
			parentId = term.id;
		}
		const deepest = ids.at(-1);

		const result = await harness.client.callTool({
			name: "taxonomy_create_term",
			arguments: { taxonomy: "tags", slug: "leaf", label: "Leaf", parentId: deepest },
		});
		// New term's parent is the 100-deep tail. Walking up from there
		// reaches the root after exactly 100 hops; cursor becomes null,
		// the depth-exceeded check does NOT fire.
		expect(result.isError, extractText(result)).toBeFalsy();
	});

	it("rejects a chain that exceeds MAX_DEPTH", async () => {
		const { TaxonomyRepository } = await import("../../../src/database/repositories/taxonomy.js");
		const repo = new TaxonomyRepository(db);
		// Build a 102-term chain. The deepest term has 101 ancestors —
		// one more than MAX_DEPTH allows.
		let parentId: string | undefined;
		const ids: string[] = [];
		for (let i = 0; i < 102; i++) {
			const term = await repo.create({
				name: "tags",
				slug: `chain-${i}`,
				label: `Chain ${i}`,
				parentId,
			});
			ids.push(term.id);
			parentId = term.id;
		}
		const deepest = ids.at(-1);

		const result = await harness.client.callTool({
			name: "taxonomy_create_term",
			arguments: { taxonomy: "tags", slug: "leaf", label: "Leaf", parentId: deepest },
		});
		expect(result.isError).toBe(true);
		expect(extractText(result)).toMatch(/maximum depth/i);
		const meta = (result as { _meta?: { code?: string } })._meta;
		expect(meta?.code).toBe("VALIDATION_ERROR");
	});
});

describe("taxonomy_delete_term (bug #13 / F12)", () => {
	let db: Kysely<Database>;
	let harness: McpHarness;

	beforeEach(async () => {
		db = await setupTestDatabase();
		await setupTaxonomy(db, { name: "tags", label: "Tags" });
		harness = await connectMcpHarness({ db, userId: ADMIN_ID, userRole: Role.ADMIN });
	});

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

	it("rejects deletion when children exist (matches handler behavior)", async () => {
		const parent = await harness.client.callTool({
			name: "taxonomy_create_term",
			arguments: { taxonomy: "tags", slug: "parent", label: "Parent" },
		});
		const { term } = extractJson<{ term: { id: string } }>(parent);
		await harness.client.callTool({
			name: "taxonomy_create_term",
			arguments: { taxonomy: "tags", slug: "child", label: "Child", parentId: term.id },
		});

		const result = await harness.client.callTool({
			name: "taxonomy_delete_term",
			arguments: { taxonomy: "tags", termSlug: "parent" },
		});
		expect(result.isError).toBe(true);
		expect(extractText(result)).toMatch(/VALIDATION_ERROR|children/i);
	});

	it("deletes a leaf term and the row is actually gone", async () => {
		await harness.client.callTool({
			name: "taxonomy_create_term",
			arguments: { taxonomy: "tags", slug: "leaf", label: "Leaf" },
		});

		// Pre-condition: the term is listable.
		const before = await harness.client.callTool({
			name: "taxonomy_list_terms",
			arguments: { taxonomy: "tags" },
		});
		const beforeSlugs = extractJson<{ items: Array<{ slug: string }> }>(before).items.map(
			(t) => t.slug,
		);
		expect(beforeSlugs).toContain("leaf");

		const result = await harness.client.callTool({
			name: "taxonomy_delete_term",
			arguments: { taxonomy: "tags", termSlug: "leaf" },
		});
		expect(result.isError, extractText(result)).toBeFalsy();

		// Post-condition: the term is no longer listable. A regression where
		// the handler returns success: true without actually deleting the row
		// fails this assertion.
		const after = await harness.client.callTool({
			name: "taxonomy_list_terms",
			arguments: { taxonomy: "tags" },
		});
		const afterSlugs = extractJson<{ items: Array<{ slug: string }> }>(after).items.map(
			(t) => t.slug,
		);
		expect(afterSlugs).not.toContain("leaf");
	});
});
