"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createTestWorkflowTool = void 0;
exports.testWorkflow = testWorkflow;
const constants_1 = require("@n8n/constants");
const workflow_sdk_1 = require("@n8n/workflow-sdk");
const n8n_workflow_1 = require("n8n-workflow");
const zod_1 = __importDefault(require("zod"));
const mcp_constants_1 = require("../mcp.constants");
const mcp_errors_1 = require("../mcp.errors");
const execution_utils_1 = require("./execution-utils");
const workflow_validation_utils_1 = require("./workflow-validation.utils");
const inputSchema = zod_1.default.object({
    workflowId: zod_1.default.string().describe('The ID of the workflow to test'),
    pinData: zod_1.default
        .record(zod_1.default.array(zod_1.default.record(zod_1.default.unknown())))
        .describe('Pin data for all workflow nodes. Use the prepare_test_pin_data tool to generate this. Keys are node names, values are arrays of items. Each item MUST be wrapped in a "json" property, e.g. [{"json": {"id": "123", "name": "test"}}]. Do NOT pass flat objects like [{"id": "123"}].'),
    triggerNodeName: zod_1.default
        .string()
        .optional()
        .describe('Optional name of the trigger node to start execution from. Useful for workflows with multiple triggers. Defaults to the first trigger node found.'),
});
const outputSchema = {
    executionId: zod_1.default.string().nullable(),
    status: zod_1.default
        .enum(['success', 'error', 'running', 'waiting', 'canceled', 'crashed', 'new', 'unknown'])
        .describe('The status of the test execution'),
    error: zod_1.default.string().optional().describe('Error message if the execution failed'),
};
const createTestWorkflowTool = (user, workflowFinderService, activeExecutions, workflowRunner, nodeTypes, telemetry, mcpService) => ({
    name: 'test_workflow',
    config: {
        description: 'Test a workflow using pin data to bypass external services. Trigger nodes, nodes with credentials, and HTTP Request nodes are pinned (use simulated data). Other nodes (Set, If, Code, etc.) execute normally — including credential-free I/O nodes like Execute Command or file read/write nodes. Use prepare_test_pin_data to generate the pin data first.',
        inputSchema: inputSchema.shape,
        outputSchema,
        annotations: {
            title: 'Test Workflow',
            readOnlyHint: false,
            destructiveHint: true,
            idempotentHint: false,
            openWorldHint: false,
        },
    },
    handler: async ({ workflowId, pinData, triggerNodeName }) => {
        const telemetryPayload = {
            user_id: user.id,
            tool_name: 'test_workflow',
            parameters: {
                workflowId,
                nodeCount: Object.keys(pinData).length,
                hasTriggerNodeName: !!triggerNodeName,
            },
        };
        try {
            const output = await testWorkflow(user, workflowFinderService, activeExecutions, workflowRunner, nodeTypes, mcpService, workflowId, pinData, triggerNodeName);
            telemetryPayload.results = {
                success: output.status === 'success',
                data: { executionId: output.executionId, status: output.status },
            };
            if (output.status === 'error' && output.error) {
                telemetryPayload.results.error = output.error;
            }
            telemetry.track(mcp_constants_1.USER_CALLED_MCP_TOOL_EVENT, telemetryPayload);
            return {
                content: [{ type: 'text', text: (0, n8n_workflow_1.jsonStringify)(output) }],
                structuredContent: output,
            };
        }
        catch (er) {
            const error = (0, n8n_workflow_1.ensureError)(er);
            const isTimeout = error instanceof mcp_errors_1.McpExecutionTimeoutError;
            const isAccessError = error instanceof mcp_errors_1.WorkflowAccessError;
            const output = {
                executionId: isTimeout ? error.executionId : null,
                status: 'error',
                error: isTimeout
                    ? `Workflow execution timed out after ${execution_utils_1.WORKFLOW_EXECUTION_TIMEOUT_MS * constants_1.Time.milliseconds.toSeconds} seconds`
                    : (error.message ?? `${error.constructor.name}: (no message)`),
            };
            telemetryPayload.results = {
                success: false,
                error: isTimeout ? 'Workflow execution timed out' : error.message,
                error_reason: isAccessError ? error.reason : undefined,
            };
            telemetry.track(mcp_constants_1.USER_CALLED_MCP_TOOL_EVENT, telemetryPayload);
            return {
                content: [{ type: 'text', text: (0, n8n_workflow_1.jsonStringify)(output) }],
                structuredContent: output,
            };
        }
    },
});
exports.createTestWorkflowTool = createTestWorkflowTool;
async function testWorkflow(user, workflowFinderService, activeExecutions, workflowRunner, nodeTypes, mcpService, workflowId, pinData, triggerNodeName) {
    const workflow = await (0, workflow_validation_utils_1.getMcpWorkflow)(workflowId, user, ['workflow:execute'], workflowFinderService);
    const nodes = workflow.nodes ?? [];
    const connections = workflow.connections ?? {};
    const triggerNode = findTriggerNode(nodes, nodeTypes, triggerNodeName);
    if (!triggerNode) {
        throw new mcp_errors_1.WorkflowAccessError(triggerNodeName
            ? `Trigger node "${triggerNodeName}" not found in the workflow. Check the node name and ensure it is a trigger node.`
            : 'Workflow has no trigger node. A trigger node is required to test the workflow.', 'unsupported_trigger');
    }
    const nodeNames = new Set(nodes.map((n) => n.name));
    const unknownKeys = Object.keys(pinData).filter((key) => !nodeNames.has(key));
    if (unknownKeys.length > 0) {
        throw new mcp_errors_1.WorkflowAccessError(`Pin data contains unknown node names: ${unknownKeys.join(', ')}. Check for typos — node names must match exactly.`, 'invalid_pin_data');
    }
    const normalizedPinData = (0, workflow_sdk_1.normalizePinData)(pinData);
    const triggerPinData = normalizedPinData[triggerNode.name] ?? [
        { json: {} },
    ];
    const mcpMessageId = `mcp-test-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
    const runData = {
        executionMode: 'manual',
        workflowData: { ...workflow, nodes, connections },
        userId: user.id,
        isMcpExecution: mcpService.isQueueMode,
        mcpType: 'service',
        mcpSessionId: mcpMessageId,
        mcpMessageId,
        startNodes: [{ name: triggerNode.name, sourceData: null }],
        pinData: normalizedPinData,
        executionData: (0, n8n_workflow_1.createRunExecutionData)({
            startData: {},
            resultData: {
                pinData: normalizedPinData,
                runData: {},
            },
            executionData: {
                contextData: {},
                metadata: {},
                nodeExecutionStack: [
                    {
                        node: triggerNode,
                        data: {
                            main: [triggerPinData],
                        },
                        source: null,
                    },
                ],
                waitingExecution: {},
                waitingExecutionSource: {},
            },
        }),
    };
    const executionId = await workflowRunner.run(runData);
    const data = await (0, execution_utils_1.waitForExecutionResult)(executionId, activeExecutions, mcpService);
    const hasError = data.status === 'error' || data.data.resultData?.error;
    return {
        executionId,
        status: hasError ? 'error' : data.status,
        error: hasError
            ? (data.data.resultData?.error?.message ?? 'Execution completed with errors')
            : undefined,
    };
}
function findTriggerNode(nodes, nodeTypes, triggerNodeName) {
    for (const node of nodes) {
        if (node.disabled)
            continue;
        if (triggerNodeName && node.name !== triggerNodeName)
            continue;
        try {
            const nodeType = nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
            if ((0, n8n_workflow_1.isTriggerNode)(nodeType.description)) {
                return node;
            }
        }
        catch {
        }
    }
    return undefined;
}
//# sourceMappingURL=test-workflow.tool.js.map