import { nanoid } from 'nanoid';

import { test, expect } from '../../../fixtures/base';

/**
 * E2E tests for the Internal MCP Service (/mcp-server/http).
 *
 * This tests the built-in MCP server that exposes n8n workflows to external
 * MCP clients (like Claude AI). It provides 6 core tools and 7 builder tools:
 *
 * Core tools:
 * - search_workflows: Search for workflows available in MCP
 * - get_workflow_details: Get detailed information about a workflow
 * - execute_workflow: Execute a workflow and get results
 * - get_execution: Get full execution details by ID
 * - publish_workflow: Publish (activate) a workflow
 * - unpublish_workflow: Unpublish (deactivate) a workflow
 *
 * Builder tools (enabled via N8N_MCP_BUILDER_ENABLED):
 * - search_nodes: Search for n8n nodes by service name/trigger type
 * - get_node_types: Get TypeScript type definitions for nodes
 * - get_suggested_nodes: Get curated node recommendations by category
 * - validate_workflow: Validate n8n Workflow SDK code
 * - create_workflow_from_code: Create a workflow from validated SDK code
 * - archive_workflow: Archive a workflow by ID
 * - update_workflow: Update a workflow with new SDK code
 *
 * Authentication is via Bearer token (MCP API key).
 *
 * NOTE: Tests run serially because n8n only supports ONE MCP API key at a time.
 * Each test uses rotateMcpApiKey() to get a usable key (since getMcpApiKey()
 * returns REDACTED after the first call), and rotation invalidates the previous
 * key. Running in parallel would cause race conditions where tests invalidate
 * each other's keys.
 */

test.describe(
	'MCP Service',
	{
		annotation: [{ type: 'owner', description: 'AI' }],
	},
	() => {
		// Run tests serially - n8n only supports one MCP API key at a time,
		// and rotation invalidates the previous key
		test.describe.configure({ mode: 'serial' });

		// Enable MCP access before each test
		test.beforeEach(async ({ api }) => {
			await api.setMcpAccess(true);
		});

		test.describe('Authentication', () => {
			test('should reject requests without bearer token', async ({ api }) => {
				const message = api.mcp.createMessage('tools/list');
				const response = await api.mcp.internalMcpSendMessageNoAuth(message);

				expect(response.status()).toBe(401);
			});

			test('should reject requests with invalid bearer token', async ({ api }) => {
				const message = api.mcp.createMessage('tools/list');
				const response = await api.mcp.internalMcpSendMessageNoAuth(message, {
					Authorization: 'Bearer invalid-token-12345',
				});

				expect(response.status()).toBe(401);
			});

			test('should accept valid API key', async ({ api }) => {
				const { apiKey } = await api.rotateMcpApiKey();

				const message = api.mcp.createMessage('tools/list');
				const response = await api.mcp.internalMcpSendMessage(apiKey, message);

				expect(response.status()).toBeLessThan(300);
			});

			test('should reject requests after key rotation with old key', async ({ api }) => {
				const { apiKey: oldKey } = await api.rotateMcpApiKey();
				const { apiKey: newKey } = await api.rotateMcpApiKey();

				const message = api.mcp.createMessage('tools/list');
				const responseWithOldKey = await api.mcp.internalMcpSendMessageNoAuth(message, {
					Authorization: `Bearer ${oldKey}`,
				});
				expect(responseWithOldKey.status()).toBe(401);

				const responseWithNewKey = await api.mcp.internalMcpSendMessage(newKey, message);
				expect(responseWithNewKey.status()).toBeLessThan(300);
			});
		});

		test.describe('MCP Settings', () => {
			test('should reject when MCP access is disabled', async ({ api }) => {
				await api.setMcpAccess(false);

				try {
					const { apiKey } = await api.rotateMcpApiKey();
					const message = api.mcp.createMessage('tools/list');
					const response = await api.mcp.internalMcpSendMessage(apiKey, message);

					expect(response.status()).toBe(403);
					const body = await response.json();
					expect(body.message).toContain('MCP access is disabled');
				} finally {
					await api.setMcpAccess(true);
				}
			});
		});

		test.describe('tools/list', () => {
			test('should return all built-in tools including builder tools', async ({ api }) => {
				const { apiKey } = await api.rotateMcpApiKey();
				const tools = await api.mcp.internalMcpListTools(apiKey);

				const toolNames = tools.map((t) => t.name).sort();

				// Guard against major regressions (e.g. half the tools disappearing)
				// without coupling to an exact list that breaks on every add/remove.
				expect(toolNames.length).toBeGreaterThanOrEqual(10);

				// Every tool must have the required MCP structure
				for (const tool of tools) {
					expect(tool.name).toBeTruthy();
					expect(tool.description).toBeTruthy();
					expect(tool.inputSchema).toBeDefined();
				}

				// Spot-check a few stable core tools
				expect(toolNames).toContain('search_workflows');
				expect(toolNames).toContain('execute_workflow');
				expect(toolNames).toContain('get_workflow_details');
			});

			test('should include proper tool descriptions and schemas', async ({ api }) => {
				const { apiKey } = await api.rotateMcpApiKey();
				const tools = await api.mcp.internalMcpListTools(apiKey);

				const searchTool = tools.find((t) => t.name === 'search_workflows');
				expect(searchTool).toBeDefined();
				expect(searchTool!.description).toContain('Search');
				expect(searchTool!.inputSchema).toBeDefined();

				const detailsTool = tools.find((t) => t.name === 'get_workflow_details');
				expect(detailsTool).toBeDefined();
				expect(detailsTool!.description).toContain('workflow');
				expect(detailsTool!.inputSchema).toBeDefined();

				const executeTool = tools.find((t) => t.name === 'execute_workflow');
				expect(executeTool).toBeDefined();
				expect(executeTool!.description).toContain('Execute');
				expect(executeTool!.inputSchema).toBeDefined();
			});
		});

		test.describe('search_workflows', () => {
			test('should return workflows marked as available in MCP', async ({ api }) => {
				const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
					'mcp-service/mcp-available-basic.json',
				);
				await api.workflows.activate(workflowId, createdWorkflow.versionId!);

				const { apiKey } = await api.rotateMcpApiKey();
				const result = await api.mcp.internalMcpSearchWorkflows(apiKey);

				expect(result.count).toBeGreaterThanOrEqual(1);
				expect(result.data.length).toBeGreaterThanOrEqual(1);

				const foundWorkflow = result.data.find((w) => w.id === workflowId);
				expect(foundWorkflow).toBeDefined();
				expect(foundWorkflow!.active).toBe(true);
				expect(foundWorkflow!.scopes).toBeDefined();
				expect(foundWorkflow!.availableInMCP).toBe(true);
			});

			test('should return workflows not marked as available in MCP with availableInMCP: false', async ({
				api,
			}) => {
				const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
					'mcp-service/mcp-unavailable.json',
				);
				await api.workflows.activate(workflowId, createdWorkflow.versionId!);

				const { apiKey } = await api.rotateMcpApiKey();
				const result = await api.mcp.internalMcpSearchWorkflows(apiKey);

				const foundWorkflow = result.data.find((w) => w.id === workflowId);
				expect(foundWorkflow).toBeDefined();
				expect(foundWorkflow!.availableInMCP).toBe(false);
			});

			test('should support limit parameter', async ({ api }) => {
				const { apiKey } = await api.rotateMcpApiKey();

				const result = await api.mcp.internalMcpSearchWorkflows(apiKey, { limit: 1 });

				expect(result.data.length).toBeLessThanOrEqual(1);
			});

			test('should support query filter for name search', async ({ api }) => {
				const uniqueName = `Searchable-${nanoid(8)}`;
				const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
					'mcp-service/mcp-available-basic.json',
					{
						transform: (wf) => {
							wf.name = uniqueName;
							return wf;
						},
					},
				);
				await api.workflows.activate(workflowId, createdWorkflow.versionId!);

				const { apiKey } = await api.rotateMcpApiKey();

				const result = await api.mcp.internalMcpSearchWorkflows(apiKey, { query: uniqueName });

				expect(result.data.length).toBe(1);
				expect(result.data[0].id).toBe(workflowId);
			});

			test('should return workflow metadata (id, name, scopes)', async ({ api }) => {
				const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
					'mcp-service/mcp-available-basic.json',
				);
				await api.workflows.activate(workflowId, createdWorkflow.versionId!);

				const { apiKey } = await api.rotateMcpApiKey();
				const result = await api.mcp.internalMcpSearchWorkflows(apiKey);

				const foundWorkflow = result.data.find((w) => w.id === workflowId);
				expect(foundWorkflow).toBeDefined();

				expect(foundWorkflow!.id).toBe(workflowId);
				expect(foundWorkflow!.name).toBeTruthy();
				expect(foundWorkflow!.scopes).toBeInstanceOf(Array);
				expect(typeof foundWorkflow!.canExecute).toBe('boolean');
				expect(typeof foundWorkflow!.availableInMCP).toBe('boolean');
			});
		});

		test.describe('get_workflow_details', () => {
			test('should return detailed info for accessible workflow', async ({ api }) => {
				const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
					'mcp-service/mcp-available-basic.json',
				);
				await api.workflows.activate(workflowId, createdWorkflow.versionId!);

				const { apiKey } = await api.rotateMcpApiKey();
				const result = await api.mcp.internalMcpGetWorkflowDetails(apiKey, workflowId);

				expect(result.workflow).toBeDefined();
				expect(result.workflow.id).toBe(workflowId);
				expect(result.workflow.nodes).toBeDefined();
				expect(result.workflow.connections).toBeDefined();
				expect(result.workflow.settings).toBeDefined();
				expect(result.workflow.scopes).toBeDefined();
				expect(typeof result.workflow.canExecute).toBe('boolean');
			});

			test('should return error for non-existent workflow', async ({ api }) => {
				const { apiKey } = await api.rotateMcpApiKey();

				const fakeWorkflowId = 'nonexistent-workflow-id-12345';

				await expect(
					api.mcp.internalMcpGetWorkflowDetails(apiKey, fakeWorkflowId),
				).rejects.toThrow();
			});

			test('should return error for workflow not available in MCP', async ({ api }) => {
				const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
					'mcp-service/mcp-unavailable.json',
				);
				await api.workflows.activate(workflowId, createdWorkflow.versionId!);

				const { apiKey } = await api.rotateMcpApiKey();

				await expect(api.mcp.internalMcpGetWorkflowDetails(apiKey, workflowId)).rejects.toThrow();
			});

			test('should include trigger info in response', async ({ api }) => {
				const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
					'mcp-service/mcp-available-webhook.json',
				);
				await api.workflows.activate(workflowId, createdWorkflow.versionId!);

				const { apiKey } = await api.rotateMcpApiKey();
				const result = await api.mcp.internalMcpGetWorkflowDetails(apiKey, workflowId);

				expect(result.triggerInfo).toBeDefined();
			});
		});

		test.describe('execute_workflow', () => {
			test('should execute workflow successfully', async ({ api }) => {
				const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
					'mcp-service/mcp-available-basic.json',
				);
				await api.workflows.activate(workflowId, createdWorkflow.versionId!);

				const { apiKey } = await api.rotateMcpApiKey();
				const result = await api.mcp.internalMcpExecuteWorkflow(apiKey, workflowId);

				expect(result.status).toBe('started');
				expect(result.executionId).toBeTruthy();
			});

			test('should return error for non-existent workflow', async ({ api }) => {
				const { apiKey } = await api.rotateMcpApiKey();
				const fakeWorkflowId = 'nonexistent-workflow-id-12345';

				const result = await api.mcp.internalMcpExecuteWorkflow(apiKey, fakeWorkflowId);

				expect(result.status).toBe('error');
				expect(result.error).toBeTruthy();
			});

			test('should return error for workflow not available in MCP', async ({ api }) => {
				const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
					'mcp-service/mcp-unavailable.json',
				);
				await api.workflows.activate(workflowId, createdWorkflow.versionId!);

				const { apiKey } = await api.rotateMcpApiKey();
				const result = await api.mcp.internalMcpExecuteWorkflow(apiKey, workflowId);

				expect(result.status).toBe('error');
				expect(result.error).toBeTruthy();
			});

			test('should execute webhook workflow with inputs', async ({ api }) => {
				const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
					'mcp-service/mcp-available-webhook.json',
				);
				await api.workflows.activate(workflowId, createdWorkflow.versionId!);

				const { apiKey } = await api.rotateMcpApiKey();
				const result = await api.mcp.internalMcpExecuteWorkflow(apiKey, workflowId, {
					type: 'webhook',
					webhookData: {
						method: 'POST',
						body: { message: 'Hello from MCP test' },
					},
				});

				expect(result.status).toBe('started');
				expect(result.executionId).toBeTruthy();
			});
		});

		test.describe('get_execution', () => {
			test('should return full execution data after workflow execution', async ({ api }) => {
				const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
					'mcp-service/mcp-available-basic.json',
				);
				await api.workflows.activate(workflowId, createdWorkflow.versionId!);

				const { apiKey } = await api.rotateMcpApiKey();

				const execResult = await api.mcp.internalMcpExecuteWorkflow(apiKey, workflowId);
				expect(execResult.status).toBe('started');
				expect(execResult.executionId).toBeTruthy();

				// Poll for execution completion since executions are asynchronous
				await expect
					.poll(
						async () => {
							const r = await api.mcp.internalMcpGetExecution(
								apiKey,
								workflowId,
								execResult.executionId!,
							);
							return r.execution?.status;
						},
						{ timeout: 30_000, intervals: [1_000] },
					)
					.toBe('success');

				const result = await api.mcp.internalMcpGetExecution(
					apiKey,
					workflowId,
					execResult.executionId!,
					{ includeData: true },
				);

				expect(result.execution).toBeDefined();
				expect(result.execution!.id).toBe(execResult.executionId);
				expect(result.execution!.workflowId).toBe(workflowId);
				expect(result.data).toBeDefined();
			});
		});

		test.describe('publish_workflow', () => {
			test('should publish a workflow successfully', async ({ api }) => {
				const { workflowId } = await api.workflows.importWorkflowFromFile(
					'mcp-service/mcp-available-basic.json',
				);

				const { apiKey } = await api.rotateMcpApiKey();
				const result = await api.mcp.internalMcpPublishWorkflow(apiKey, workflowId);

				expect(result.success).toBe(true);
				expect(result.workflowId).toBe(workflowId);
				expect(result.activeVersionId).toBeTruthy();
			});
		});

		test.describe('unpublish_workflow', () => {
			test('should unpublish a workflow successfully', async ({ api }) => {
				const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
					'mcp-service/mcp-available-basic.json',
				);
				await api.workflows.activate(workflowId, createdWorkflow.versionId!);

				const { apiKey } = await api.rotateMcpApiKey();
				const result = await api.mcp.internalMcpUnpublishWorkflow(apiKey, workflowId);

				expect(result.success).toBe(true);
				expect(result.workflowId).toBe(workflowId);
			});
		});

		test.describe('Error Handling', () => {
			test('should handle malformed JSON-RPC messages', async ({ api }) => {
				const { apiKey } = await api.rotateMcpApiKey();

				// Missing required 'jsonrpc: "2.0"' field
				const malformedMessage = {
					id: nanoid(),
					method: 'tools/list',
				};

				const response = await api.mcp.internalMcpSendMessage(apiKey, malformedMessage);

				// Server returns 400 Bad Request for malformed JSON-RPC
				expect(response.status()).toBe(400);

				const body = await response.json();
				expect(body.error).toBeDefined();
				expect(body.error.code).toBe(-32700); // Parse error
				expect(body.error.message).toBeTruthy();
			});

			test('should handle unknown methods', async ({ api }) => {
				const { apiKey } = await api.rotateMcpApiKey();

				const message = api.mcp.createMessage('unknown/method');
				const response = await api.mcp.internalMcpSendMessage(apiKey, message);

				// Server returns 200 OK with SSE response containing error
				expect(response.status()).toBe(200);
				expect(response.headers()['content-type']).toContain('text/event-stream');

				// Parse SSE format: extract JSON from "data: {...}" line
				const text = await response.text();
				const dataLine = text.split('\n').find((line) => line.startsWith('data:'))!;
				const body = JSON.parse(dataLine.slice(5).trim()) as {
					error: { code: number; message: string };
				};

				expect(body.error).toBeDefined();
				expect(body.error.code).toBe(-32601); // Method not found
				expect(body.error.message).toBeTruthy();
			});

			test('should handle invalid tool parameters', async ({ api }) => {
				const { apiKey } = await api.rotateMcpApiKey();

				const message = api.mcp.createMessage('tools/call', {
					name: 'search_workflows',
					arguments: {
						limit: 'not-a-number',
					},
				});

				const response = await api.mcp.internalMcpSendMessage(apiKey, message);

				// Server returns 200 OK with SSE response
				expect(response.ok()).toBe(true);
				expect(response.headers()['content-type']).toContain('text/event-stream');

				// Parse SSE format: extract JSON from "data: {...}" line
				const text = await response.text();
				const dataLine = text.split('\n').find((line) => line.startsWith('data:'))!;
				const body = JSON.parse(dataLine.slice(5).trim()) as {
					error?: unknown;
					result?: unknown;
					jsonrpc: string;
				};

				expect(body.jsonrpc).toBe('2.0');
				// Should return either a JSON-RPC error or a successful result
				expect(body.error !== undefined || body.result !== undefined).toBe(true);
			});
		});
	},
);
