import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import { $ } from "bun";
import { McpServer } from "../mcp/server.ts";
import { registerDocumentTools } from "../mcp/tools/documents/index.ts";
import type { JsonSchema } from "../mcp/validation/validators.ts";
import { DOCUMENT_TYPE_VALUES } from "../types/index.ts";
import { createUniqueTestDir, initializeTestProject, safeCleanup } from "./test-utils.ts";

// Helper to extract text from MCP content (handles union types)
const getText = (content: unknown[] | undefined, index = 0): string => {
	const item = content?.[index] as { text?: string } | undefined;
	return item?.text ?? "";
};

let TEST_DIR: string;
let mcpServer: McpServer;

async function loadConfig(server: McpServer) {
	const config = await server.filesystem.loadConfig();
	if (!config) {
		throw new Error("Failed to load backlog configuration for tests");
	}
	return config;
}

describe("MCP document tools", () => {
	beforeEach(async () => {
		TEST_DIR = createUniqueTestDir("mcp-documents");
		mcpServer = new McpServer(TEST_DIR, "Test instructions");
		await mcpServer.filesystem.ensureBacklogStructure();

		await $`git init -b main`.cwd(TEST_DIR).quiet();
		await $`git config user.name "Test User"`.cwd(TEST_DIR).quiet();
		await $`git config user.email test@example.com`.cwd(TEST_DIR).quiet();

		await initializeTestProject(mcpServer, "Docs Project");
		const config = await loadConfig(mcpServer);
		registerDocumentTools(mcpServer, config);
	});

	afterEach(async () => {
		try {
			await mcpServer.stop();
		} catch {
			// ignore shutdown issues in tests
		}
		await safeCleanup(TEST_DIR);
	});

	it("creates and lists documents", async () => {
		const createResult = await mcpServer.testInterface.callTool({
			params: {
				name: "document_create",
				arguments: {
					title: "Engineering Guidelines",
					content: "# Overview\n\nFollow the documented practices.",
					path: "guides",
					tags: ["engineering"],
				},
			},
		});

		const createText = getText(createResult.content);
		expect(createText).toContain("Document created successfully.");
		expect(createText).toContain("Document doc-1 - Engineering Guidelines");
		expect(createText).toContain("Path: guides/doc-1 - Engineering-Guidelines.md");
		expect(createText).toMatch(/Created: \d{4}-\d{2}-\d{2} \d{2}:\d{2} \(UTC\)/);
		expect(createText).toContain("Tags: engineering");
		expect(createText).toContain("# Overview");

		const listResult = await mcpServer.testInterface.callTool({
			params: { name: "document_list", arguments: {} },
		});

		const listText = getText(listResult.content);
		expect(listText).toContain("Documents:");
		expect(listText).toContain("doc-1 - Engineering Guidelines");
		expect(listText).toContain("path: guides/doc-1 - Engineering-Guidelines.md");
		expect(listText).toMatch(/created: \d{4}-\d{2}-\d{2} \d{2}:\d{2} \(UTC\)/);
		expect(listText).toContain("tags: engineering");
	});

	it("exposes supported document type enums", async () => {
		const tools = await mcpServer.testInterface.listTools();
		const toolByName = new Map(tools.tools.map((tool) => [tool.name, tool]));

		const createSchema = toolByName.get("document_create")?.inputSchema as JsonSchema | undefined;
		const updateSchema = toolByName.get("document_update")?.inputSchema as JsonSchema | undefined;

		expect(createSchema?.properties?.type?.enum).toEqual([...DOCUMENT_TYPE_VALUES]);
		expect(updateSchema?.properties?.type?.enum).toEqual([...DOCUMENT_TYPE_VALUES]);
	});

	it("rejects unsupported document types", async () => {
		const invalidCreate = await mcpServer.testInterface.callTool({
			params: {
				name: "document_create",
				arguments: {
					title: "Invalid Type",
					content: "Content",
					type: "unexpected",
				},
			},
		});

		expect(invalidCreate.isError).toBe(true);
		expect(getText(invalidCreate.content)).toContain(
			"Field 'type' must be one of: readme, guide, specification, other",
		);

		await mcpServer.testInterface.callTool({
			params: {
				name: "document_create",
				arguments: {
					title: "Valid Type",
					content: "Content",
					type: "guide",
				},
			},
		});

		const invalidUpdate = await mcpServer.testInterface.callTool({
			params: {
				name: "document_update",
				arguments: {
					id: "doc-1",
					content: "Updated",
					type: "unexpected",
				},
			},
		});

		expect(invalidUpdate.isError).toBe(true);
		expect(getText(invalidUpdate.content)).toContain(
			"Field 'type' must be one of: readme, guide, specification, other",
		);
	});

	it("filters documents using substring search", async () => {
		await mcpServer.testInterface.callTool({
			params: {
				name: "document_create",
				arguments: {
					title: "Engineering Guidelines",
					content: "Content",
				},
			},
		});

		await mcpServer.testInterface.callTool({
			params: {
				name: "document_create",
				arguments: {
					title: "Product Strategy",
					content: "Strategy content",
				},
			},
		});

		const filteredResult = await mcpServer.testInterface.callTool({
			params: { name: "document_list", arguments: { search: "strat" } },
		});

		const filteredText = getText(filteredResult.content);
		expect(filteredText).toContain("Documents:");
		expect(filteredText).toContain("Product Strategy");
		expect(filteredText).not.toContain("Engineering Guidelines");
	});

	it("views documents regardless of ID casing or padding", async () => {
		await mcpServer.testInterface.callTool({
			params: {
				name: "document_create",
				arguments: {
					title: "Runbook",
					content: "Step 1: Do the thing.",
				},
			},
		});

		const withPrefix = await mcpServer.testInterface.callTool({
			params: { name: "document_view", arguments: { id: "doc-1" } },
		});
		const withoutPrefix = await mcpServer.testInterface.callTool({
			params: { name: "document_view", arguments: { id: "1" } },
		});
		const uppercase = await mcpServer.testInterface.callTool({
			params: { name: "document_view", arguments: { id: "DOC-0001" } },
		});
		const zeroPadded = await mcpServer.testInterface.callTool({
			params: { name: "document_view", arguments: { id: "0001" } },
		});

		const prefixText = getText(withPrefix.content);
		const noPrefixText = getText(withoutPrefix.content);
		const uppercaseText = getText(uppercase.content);
		const zeroPaddedText = getText(zeroPadded.content);
		expect(prefixText).toContain("Document doc-1 - Runbook");
		expect(prefixText).toContain("Step 1: Do the thing.");
		expect(noPrefixText).toContain("Document doc-1 - Runbook");
		expect(uppercaseText).toContain("Document doc-1 - Runbook");
		expect(zeroPaddedText).toContain("Document doc-1 - Runbook");
	});

	it("updates documents including title changes", async () => {
		await mcpServer.testInterface.callTool({
			params: {
				name: "document_create",
				arguments: {
					title: "Incident Response",
					content: "Initial content",
				},
			},
		});

		const updateResult = await mcpServer.testInterface.callTool({
			params: {
				name: "document_update",
				arguments: {
					id: "DOC-0001",
					title: "Incident Response Handbook",
					content: "Updated procedures",
					path: "runbooks",
				},
			},
		});

		const updateText = getText(updateResult.content);
		expect(updateText).toContain("Document updated successfully.");
		expect(updateText).toContain("Document doc-1 - Incident Response Handbook");
		expect(updateText).toContain("Path: runbooks/doc-1 - Incident-Response-Handbook.md");
		expect(updateText).toContain("Updated procedures");

		const viewResult = await mcpServer.testInterface.callTool({
			params: { name: "document_view", arguments: { id: "doc-1" } },
		});
		const viewText = getText(viewResult.content);
		expect(viewText).toContain("Incident Response Handbook");
		expect(viewText).toContain("Path: runbooks/doc-1 - Incident-Response-Handbook.md");
		expect(viewText).toContain("Updated procedures");
	});

	it("rejects unsafe document paths", async () => {
		const result = await mcpServer.testInterface.callTool({
			params: {
				name: "document_create",
				arguments: {
					title: "Unsafe",
					content: "Content",
					path: "../outside",
				},
			},
		});

		expect(result.isError).toBe(true);
		expect(getText(result.content)).toContain("Document path cannot include traversal segments.");
	});

	it("searches documents and includes formatted scores", async () => {
		await mcpServer.testInterface.callTool({
			params: {
				name: "document_create",
				arguments: {
					title: "Architecture Overview",
					content: "Contains service topology details.",
				},
			},
		});

		const searchResult = await mcpServer.testInterface.callTool({
			params: {
				name: "document_search",
				arguments: {
					query: "architecture",
				},
			},
		});

		const searchText = getText(searchResult.content);
		expect(searchText).toContain("Documents:");
		expect(searchText).toMatch(/Architecture Overview/);
		expect(searchText).toContain("doc-1 - Architecture Overview (doc-1 - Architecture-Overview.md)");
		expect(searchText).toMatch(/\[score [0-1]\.\d{3}]/);
	});
});
