/**
 * MCP error envelope fidelity tests.
 *
 * Specific failure modes (unknown collection, duplicate slug, unknown
 * field, bad orderBy, etc.) must return discriminated error codes so
 * callers can act on them programmatically:
 *
 *   - Handlers detect known failure shapes and return one of:
 *     `SLUG_CONFLICT`, `COLLECTION_NOT_FOUND`, `UNKNOWN_FIELD`,
 *     `INVALID_ORDER_BY`, `VALIDATION_ERROR`.
 *   - The MCP envelope emits the code as a `[CODE]` prefix on the
 *     message text and as `_meta.code` for SDK-aware clients.
 *
 * Each test asserts:
 *   (a) the response is `isError: true`
 *   (b) the code/message names the specific failure, not a generic
 *       "Failed to ..." string
 */

import { Role } from "@emdash-cms/auth";
import 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, extractText, type McpHarness } from "../../utils/mcp-runtime.js";
import { setupTestDatabaseWithCollections, teardownTestDatabase } from "../../utils/test-db.js";

// Generic placeholders that should NOT survive after the fix.
const GENERIC_CREATE = /^Failed to create content$/;
const GENERIC_LIST = /^Failed to list content$/;
const GENERIC_UPDATE = /^Failed to update content$/;
const UNKNOWN_ERROR = /^Unknown error$/;

describe("MCP error envelope — content_create (bug #3)", () => {
	let db: Kysely<Database>;
	let harness: McpHarness;

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

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

	it("unknown collection slug returns a discriminated NOT_FOUND-style error", async () => {
		const result = await harness.client.callTool({
			name: "content_create",
			arguments: { collection: "nonexistent", data: { title: "Hi" } },
		});

		expect(result.isError).toBe(true);
		const text = extractText(result);
		// Message should name the specific failure (collection not found).
		expect(text).not.toMatch(GENERIC_CREATE);
		expect(text).not.toMatch(UNKNOWN_ERROR);
		// Tight match: explicitly the COLLECTION_NOT_FOUND code (or message),
		// rather than any text that happens to contain "collection".
		expect(text).toMatch(/COLLECTION_NOT_FOUND|Collection ['"]?nonexistent['"]? not found/i);
	});

	it("duplicate slug returns a SLUG_CONFLICT-style error", async () => {
		// Seed an item with a known slug
		const repo = new ContentRepository(db);
		await repo.create({
			type: "post",
			data: { title: "First" },
			slug: "duplicate-me",
			status: "draft",
			authorId: "seed",
		});

		const result = await harness.client.callTool({
			name: "content_create",
			arguments: {
				collection: "post",
				data: { title: "Second" },
				slug: "duplicate-me",
			},
		});

		expect(result.isError).toBe(true);
		const text = extractText(result);
		expect(text).not.toMatch(GENERIC_CREATE);
		expect(text).not.toMatch(UNKNOWN_ERROR);
		// Either explicit "slug" wording or a UNIQUE/conflict signal.
		expect(text).toMatch(/slug|unique|conflict|duplicate|exists/i);
	});

	it("unknown field in data returns an UNKNOWN_FIELD-style error", async () => {
		const result = await harness.client.callTool({
			name: "content_create",
			arguments: {
				collection: "post",
				// `nonexistent_field` was never created on the post collection
				data: { title: "Hello", nonexistent_field: "boom" },
			},
		});

		expect(result.isError).toBe(true);
		const text = extractText(result);
		expect(text).not.toMatch(GENERIC_CREATE);
		expect(text).not.toMatch(UNKNOWN_ERROR);
		expect(text).toMatch(/field|unknown|nonexistent_field|column/i);
	});
});

describe("MCP error envelope — content_list (bug #3)", () => {
	let db: Kysely<Database>;
	let harness: McpHarness;

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

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

	it("unknown collection returns a COLLECTION_NOT_FOUND-style error, not a generic one", async () => {
		const result = await harness.client.callTool({
			name: "content_list",
			arguments: { collection: "nonexistent" },
		});

		expect(result.isError).toBe(true);
		const text = extractText(result);
		expect(text).not.toMatch(GENERIC_LIST);
		expect(text).not.toMatch(UNKNOWN_ERROR);
		expect(text).toMatch(/COLLECTION_NOT_FOUND|Collection ['"]?nonexistent['"]? not found/i);
	});

	it("invalid orderBy column returns an INVALID_ORDER_BY-style error", async () => {
		const result = await harness.client.callTool({
			name: "content_list",
			arguments: { collection: "post", orderBy: "definitely_not_a_column" },
		});

		expect(result.isError).toBe(true);
		const text = extractText(result);
		expect(text).not.toMatch(GENERIC_LIST);
		expect(text).not.toMatch(UNKNOWN_ERROR);
		// Concrete: response must echo the offending column AND carry a
		// stable validation-style code. Avoids matching unrelated phrases
		// that happen to contain "order" or "column".
		expect(text).toContain("definitely_not_a_column");
		const meta = (result as { _meta?: { code?: string } })._meta;
		expect(meta?.code).toBe("VALIDATION_ERROR");
	});
});

describe("MCP error envelope — content_get (bug #3)", () => {
	let db: Kysely<Database>;
	let harness: McpHarness;

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

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

	it("missing item returns a clear NOT_FOUND error including the id (already works — regression guard)", async () => {
		const result = await harness.client.callTool({
			name: "content_get",
			arguments: { collection: "post", id: "01NONEXISTENT" },
		});

		expect(result.isError).toBe(true);
		const text = extractText(result);
		expect(text).toMatch(/\bNOT_FOUND\b|\bnot found\b/i);
		expect(text).toContain("01NONEXISTENT");
	});
});

describe("MCP error envelope — content_update (bug #3)", () => {
	let db: Kysely<Database>;
	let harness: McpHarness;

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

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

	it("update on missing id returns a NOT_FOUND-style error", async () => {
		const result = await harness.client.callTool({
			name: "content_update",
			arguments: { collection: "post", id: "01NEVEREXISTED", data: { title: "x" } },
		});

		expect(result.isError).toBe(true);
		const text = extractText(result);
		expect(text).not.toMatch(GENERIC_UPDATE);
		expect(text).not.toMatch(UNKNOWN_ERROR);
		expect(text).toMatch(/\bNOT_FOUND\b|\bnot found\b/i);
		expect(text).toContain("01NEVEREXISTED");
	});

	it("stale _rev returns a CONFLICT-style error (not a generic one)", async () => {
		const repo = new ContentRepository(db);
		const item = await repo.create({
			type: "post",
			data: { title: "Original" },
			slug: "rev-test",
			status: "draft",
			authorId: "user_admin",
		});

		const result = await harness.client.callTool({
			name: "content_update",
			arguments: {
				collection: "post",
				id: item.id,
				data: { title: "x" },
				_rev: "obviously-stale-rev",
			},
		});

		expect(result.isError).toBe(true);
		const text = extractText(result);
		expect(text).not.toMatch(GENERIC_UPDATE);
		expect(text).not.toMatch(UNKNOWN_ERROR);
		expect(text).toMatch(/conflict|rev|stale|outdated|modified/i);
	});
});

describe("MCP error envelope — error code preservation through unwrap()", () => {
	let db: Kysely<Database>;
	let harness: McpHarness;

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

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

	/**
	 * The MCP SDK forwards `_meta` on tool results when present — once
	 * `unwrap()` propagates it, callers can read structured codes
	 * programmatically. Until then, codes must at least appear in the
	 * message text so callers can match on a stable token.
	 */
	it("a NOT_FOUND error from a handler surfaces 'NOT_FOUND' or equivalent", async () => {
		const result = await harness.client.callTool({
			name: "content_get",
			arguments: { collection: "post", id: "01MISSING" },
		});
		expect(result.isError).toBe(true);
		// Either the structured _meta carries the code, or the message
		// includes a stable token. Today: only `Content item not found:` —
		// no machine-readable code.
		const text = extractText(result);
		const meta = (result as { _meta?: { code?: string } })._meta;
		const codeFromMeta = meta?.code;
		expect(codeFromMeta === "NOT_FOUND" || /\bNOT_FOUND\b/.test(text)).toBe(true);
	});
});

// ---------------------------------------------------------------------------
// F7: error envelope correctly carries codes for SchemaError, McpError,
// and SDK-thrown auth errors.
// ---------------------------------------------------------------------------

describe("MCP error envelope — F7 (codes propagated for SchemaError + auth)", () => {
	let db: Kysely<Database>;
	let harness: McpHarness;

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

	it("INSUFFICIENT_SCOPE for a token without the required scope", async () => {
		db = await setupTestDatabaseWithCollections();
		// Only grant content:read; content_create needs content:write.
		harness = await connectMcpHarness({
			db,
			userId: "user_admin",
			userRole: Role.ADMIN,
			tokenScopes: ["content:read"],
		});
		const result = await harness.client.callTool({
			name: "content_create",
			arguments: { collection: "post", data: { title: "x" } },
		});
		expect(result.isError).toBe(true);
		const meta = (result as { _meta?: { code?: string } })._meta;
		expect(meta?.code).toBe("INSUFFICIENT_SCOPE");
		expect(extractText(result)).toMatch(/INSUFFICIENT_SCOPE/);
	});

	it("backwards compat: content:write token can call menu_create (implicit grant)", async () => {
		// PATs issued before menus:manage was split out of content:write
		// must continue to work. Verify the implicit grant flows through
		// the full MCP stack.
		db = await setupTestDatabaseWithCollections();
		harness = await connectMcpHarness({
			db,
			userId: "user_admin",
			userRole: Role.ADMIN,
			tokenScopes: ["content:write"],
		});
		const result = await harness.client.callTool({
			name: "menu_create",
			arguments: { name: "main", label: "Main" },
		});
		expect(result.isError, extractText(result)).toBeFalsy();
	});

	it("menus:manage token cannot call content_create (no reverse grant)", async () => {
		db = await setupTestDatabaseWithCollections();
		harness = await connectMcpHarness({
			db,
			userId: "user_admin",
			userRole: Role.ADMIN,
			tokenScopes: ["menus:manage"],
		});
		const result = await harness.client.callTool({
			name: "content_create",
			arguments: { collection: "post", data: { title: "x" } },
		});
		expect(result.isError).toBe(true);
		const meta = (result as { _meta?: { code?: string } })._meta;
		expect(meta?.code).toBe("INSUFFICIENT_SCOPE");
	});

	it("INSUFFICIENT_PERMISSIONS for a role that's too low", async () => {
		db = await setupTestDatabaseWithCollections();
		harness = await connectMcpHarness({
			db,
			userId: "user_subscriber",
			userRole: Role.SUBSCRIBER,
		});
		const result = await harness.client.callTool({
			name: "content_create",
			arguments: { collection: "post", data: { title: "x" } },
		});
		expect(result.isError).toBe(true);
		const meta = (result as { _meta?: { code?: string } })._meta;
		expect(meta?.code).toBe("INSUFFICIENT_PERMISSIONS");
	});

	it("SchemaError code (RESERVED_SLUG) propagates through schema_create_collection", async () => {
		db = await setupTestDatabaseWithCollections();
		harness = await connectMcpHarness({
			db,
			userId: "user_admin",
			userRole: Role.ADMIN,
		});
		// '_emdash_collections' is the prefix used for system tables — that
		// kind of slug is reserved. Pick a guaranteed reserved slug
		// (the '_emdash' prefix or e.g. 'media' — see RESERVED_COLLECTION_SLUGS).
		const result = await harness.client.callTool({
			name: "schema_create_collection",
			arguments: { slug: "media", label: "Reserved" },
		});
		expect(result.isError).toBe(true);
		const meta = (result as { _meta?: { code?: string } })._meta;
		// SchemaError carries `code` directly; respondHandlerError should
		// forward it. Whichever specific reserved-slug code applies is fine
		// — just assert it's a stable string that isn't the generic fallback.
		expect(meta?.code).toBeDefined();
		expect(meta?.code).not.toBe("INTERNAL_ERROR");
		expect(meta?.code).not.toBe("");
	});
});
