import { generateObject } from "ai";
import { z } from "zod";
import { getLogger } from "../../../../util/logger.js";
import { sendUserMessageChannel } from "../../../state-machine/helpers/peer.js";
import { buildTestSubagentSystemPrompt } from "../../subagents/testing/prompt-builder.js";
import { SubagentType } from "../../subagents/types.js";
import { processTestStreamChunk } from "../../utils.js";
import { createToolFactory, ToolCategory, PermissionLevel } from "../types.js";
const EMBED_HOOK_CONFIGS = [
    {
        name: "useEmbedEvent",
        pattern: /\buseEmbedEvent\s*\(\s*["']([^"']+)["']/g,
        buildInstruction: (values) => `- useEmbedEvent is used for events: ${values.map((e) => `"${e}"`).join(", ")}. Test by calling browser_playwright_action with action: "triggerEmbedEvent" and eventName set to one of these values, then verify the UI reacts correctly.`,
    },
    {
        name: "useEmitEmbedEvent",
        pattern: /\buseEmitEmbedEvent\s*\(/g,
        buildInstruction: () => `- useEmitEmbedEvent is used. After an action that should emit an event (e.g., form submit, button click), call browser_playwright_action with action: "getEmbedEvents" and verify events were emitted.`,
    },
    {
        name: "useEmbedProperties",
        pattern: /\buseEmbedProperties\s*\(/g,
        buildInstruction: () => `- useEmbedProperties is used. Call browser_playwright_action with action: "setEmbedProperties" and a properties object, then verify the UI reflects the updated values.`,
    },
];
function detectEmbedHooksAndBuildInstruction(fileSystemInterface) {
    const logger = getLogger();
    const detected = new Map();
    try {
        const currentFiles = fileSystemInterface.getCurrentFiles();
        for (const fileMeta of Object.values(currentFiles)) {
            if (!fileMeta.code)
                continue;
            for (const config of EMBED_HOOK_CONFIGS) {
                for (const match of fileMeta.code.matchAll(config.pattern)) {
                    const values = detected.get(config.name) ?? [];
                    if (match[1] && !values.includes(match[1])) {
                        values.push(match[1]);
                    }
                    detected.set(config.name, values);
                }
            }
        }
        if (detected.size > 0) {
            logger.info(`[TestSubagent] Found embed hooks: ${JSON.stringify(Object.fromEntries(detected))}`);
        }
    }
    catch {
        logger.warn(`[TestSubagent] Could not check for embed hooks, skipping`);
        return "";
    }
    const instructions = EMBED_HOOK_CONFIGS.filter((config) => detected.has(config.name)).map((config) => config.buildInstruction(detected.get(config.name) ?? []));
    if (instructions.length === 0) {
        return "";
    }
    return `- Embed hooks: This app uses embed hooks. ALWAYS include one test case for:\n${instructions.join("\n")}`;
}
/**
 * startTestRun - Invokes the Testing Agent subagent to perform end-to-end testing.
 *
 * This tool:
 * 1. Creates a test run with a unique ID
 * 2. Sends test_run_start message to UI
 * 3. Invokes the TEST subagent with filtered tools
 * 4. Suppresses text/reasoning output (all context in tool descriptions)
 * 5. Streams tool results back to the user
 */
export const startTestRunToolFactory = createToolFactory("startTestRun", ({ clark, services, }) => {
    const logger = getLogger();
    return {
        description: `
Invoke the Testing Agent to perform end-to-end testing of the application.

Use this tool to start a test run when:
- The plan has enableTesting set to true
- The user explicitly asks to test (e.g., "test this", "make sure it works", "verify the changes")

The Testing Agent will:
- Take screenshots to verify UI state
- Interact with form fields, buttons, etc.
- Validate that the plan was executed correctly
- Report test results

**IMPORTANT**: Call this tool when:
- enableTesting is set on the plan context (REQUIRED)
- User explicitly asks to test the app
- After completing a significant feature build
- When the user says things like "make sure it works", "verify", "test it out"

The Testing Agent has access to Playwright browser automation and can interact with the running application.

**IF TESTS FAIL (status: "failed")**:
You MUST fix the issues and re-test. Do NOT call \`build_finalize\` until tests pass.
1. Review the summary to understand what failed
2. Fix the code to address the failures
3. Call \`startTestRun\` again to verify your fixes
4. Repeat until status is "completed" (max 3 attempts)
`,
        category: ToolCategory.SUBAGENT,
        defaultPermission: PermissionLevel.ALLOW,
        readOnly: true,
        inputSchema: z.object({
            header: z
                .string()
                .describe("Short, concise header for display (e.g., 'Calculator Test', 'Login Flow', 'Form Validation')"),
            testDescription: z
                .string()
                .describe("Detailed description of what to test for the prompt (e.g., 'Test basic arithmetic operations, special functions, and edge cases') based on the the user prompt and/or the plan to verify"),
        }),
        execute: async ({ header, testDescription }) => {
            const testRunId = crypto.randomUUID();
            const displayHeader = header || "End-to-end testing";
            // check if planContext.enableTesting is explicitly set to false. if so exit this tool
            if (clark.context.planContext?.enableTesting === false) {
                logger.info(`[TestSubagent] Plan context enableTesting is false, exiting tool`);
                return {
                    success: true,
                    testRunId,
                    status: "completed",
                    summary: "Testing disabled in plan context",
                };
            }
            logger.info(`[TestSubagent] Test description: ${testDescription}`);
            // Phase 1: Generate test cases via LLM call (before subagent execution)
            const testCaseSchema = z.object({
                testCases: z
                    .array(z.object({
                    id: z.string().describe("Unique ID for this test case"),
                    name: z.string().describe("Short name for the test case"),
                    description: z
                        .string()
                        .describe("Brief description of what this test verifies"),
                }))
                    .min(1)
                    .max(7)
                    .describe("1-7 test cases covering the key functionality to verify. For simpler functionality, generate fewer test cases."),
            });
            let generatedTestCases = [];
            const embedHooksInstruction = detectEmbedHooksAndBuildInstruction(services.fileSystemInterface);
            try {
                const { object: testCasesResult } = await generateObject({
                    model: services.llmProvider.modelForTask("classifyPrompt"),
                    schema: testCaseSchema,
                    prompt: `You are a QA expert. Generate 3-7 test cases to verify the following functionality.

Test Description: ${testDescription}

${clark.context.planContext?.plan ? `Plan Context:\n${clark.context.planContext.plan}` : ""}

Guidelines:
- Each test case should verify ONE specific piece of functionality
- Test cases should be feature-level, not click-level (e.g., "Login form submission" not "Click login button")
- Use unique IDs like "test-1", "test-2", etc.
- Keep names concise (max 50 chars)
- Cover both happy path and edge cases where relevant
- The test cases are meant to be executed in order, so if there are dependencies between test cases, make sure to order the test cases accordingly.
- Generate 1-3 test cases for simple changes (styling, colors, text)
- Generate 3-5 test cases for medium complexity (form fields, simple interactions)
- Generate 5-7 test cases only for complex features (multi-step flows, validation logic)
- ONLY test what is described - do NOT test unrelated functionality${embedHooksInstruction ? `\n${embedHooksInstruction}` : ""}

Generate test cases that would be useful for verifying this functionality works correctly.`,
                });
                generatedTestCases = testCasesResult.testCases;
                logger.info(`[TestSubagent] Generated ${generatedTestCases.length} test cases`);
            }
            catch (error) {
                // If test case generation fails, continue without predefined cases
                logger.warn(`[TestSubagent] Failed to generate test cases: ${error instanceof Error ? error.message : String(error)}`);
            }
            // Prepare test cases with initial status
            const testCases = generatedTestCases.map((tc) => ({
                ...tc,
                status: "not_started",
            }));
            // Store test run ID and test cases in context
            clark.updateContext({
                currentTestRunId: testRunId,
                currentTestCases: testCases,
                testCaseStatuses: new Map(testCases.map((tc) => [tc.id, "not_started"])),
            });
            // Send test_run_start message to UI with test cases
            const sendUserMessage = sendUserMessageChannel(clark, services.chatSessionStore);
            await sendUserMessage({
                type: "test_run_start",
                testRunId,
                header: displayHeader,
                testCases: testCases.length > 0 ? testCases : undefined,
                role: "assistant",
                content: "",
            });
            // Capture interactive elements from the current page for DOM context
            // This helps the LLM know which elements are available for interaction
            // without needing to explore the DOM with evaluate()
            let interactiveElements = [];
            try {
                interactiveElements =
                    await services.mcpServerManager.captureInteractiveElements();
                logger.info(`[TestSubagent] Captured ${interactiveElements.length} interactive elements for DOM context`);
            }
            catch (error) {
                logger.warn(`[TestSubagent] Failed to capture interactive elements: ${error instanceof Error ? error.message : String(error)}`);
            }
            // Build test-specific system prompt with test cases and DOM context
            const testSystemPrompt = buildTestSubagentSystemPrompt({
                testRunId,
                header: displayHeader,
                planContext: clark.context.planContext,
                testCases,
                interactiveElements,
            });
            // Get tools filtered for TEST subagent
            const accessControl = clark.getAccessControl(services.features);
            const testTools = await services.toolRegistry.getToolsRecord({
                ...accessControl,
                subagent: SubagentType.TEST,
            });
            // Invoke the test subagent
            try {
                const maxSteps = 100;
                const testCaseList = testCases.length > 0
                    ? `\n\nYou MUST execute ALL ${testCases.length} test cases listed in the system prompt:\n${testCases.map((tc) => `- ${tc.id}: ${tc.name}`).join("\n")}`
                    : "";
                const result = await clark.streamText(services, {
                    system: testSystemPrompt,
                    user: {
                        role: "user",
                        content: `Test the application.${testCaseList}\n\nContext: ${testDescription}`,
                    },
                    tools: testTools,
                    context: {
                        // don't send over the full chat history to improve performance
                        messages: [],
                    },
                    maxSteps,
                    // Stop when endTestRun is called (like judge stops on submitFeedback)
                    stopWhen: ({ steps }) => {
                        const lastStep = steps[steps.length - 1];
                        const hasEndTestRun = lastStep?.toolCalls?.some((tc) => tc.toolName === "endTestRun");
                        return hasEndTestRun ?? false;
                    },
                    // Warn about remaining steps and force endTestRun on final step
                    prepareStep: (step) => {
                        const remainingSteps = maxSteps - step.stepNumber - 1;
                        const halfwayPoint = Math.floor(maxSteps / 2);
                        let modifiedStep = step;
                        // Check if user requested to skip a test case
                        const skipRequestedForTestCase = clark.context.skipRequestedForTestCase;
                        if (skipRequestedForTestCase) {
                            logger.info(`[TestSubagent] User requested skip for test case: ${skipRequestedForTestCase}`);
                            // Find the next test case to run
                            const currentTestCases = clark.context.currentTestCases ?? [];
                            const skippedIndex = currentTestCases.findIndex((tc) => tc.id === skipRequestedForTestCase);
                            const nextTestCase = skippedIndex >= 0 &&
                                skippedIndex < currentTestCases.length - 1
                                ? currentTestCases[skippedIndex + 1]
                                : null;
                            let skipInstruction;
                            if (nextTestCase) {
                                skipInstruction = `**USER SKIPPED TEST CASE**: The user has skipped test case "${skipRequestedForTestCase}". Move immediately to the next test case "${nextTestCase.id}" (${nextTestCase.name}). Do NOT continue testing the skipped case. Call \`updateTestCaseStatus\` with status "running" for "${nextTestCase.id}" and proceed with testing it.`;
                            }
                            else {
                                skipInstruction = `**USER SKIPPED TEST CASE**: The user has skipped test case "${skipRequestedForTestCase}" which was the last test case. Call \`endTestRun\` to complete the test run.`;
                            }
                            const skipMessage = {
                                role: "user",
                                content: [
                                    {
                                        type: "text",
                                        text: skipInstruction,
                                    },
                                ],
                            };
                            // We'll keep injecting this instruction until the agent actually starts a different
                            // test case (cleared in updateTestCaseStatus) or ends the run.
                            const lastMessage = modifiedStep.messages.at(-1);
                            const lastText = typeof lastMessage === "object" &&
                                lastMessage &&
                                "content" in lastMessage &&
                                Array.isArray(lastMessage.content)
                                ? lastMessage.content?.[0]?.text
                                : undefined;
                            modifiedStep =
                                lastText === skipInstruction
                                    ? modifiedStep
                                    : {
                                        ...modifiedStep,
                                        messages: [...modifiedStep.messages, skipMessage],
                                    };
                        }
                        // Final step: restrict to endTestRun only
                        if (step.stepNumber === maxSteps - 1) {
                            const warningMessage = {
                                role: "user",
                                content: [
                                    {
                                        type: "text",
                                        text: "**CRITICAL: This is your FINAL step. You MUST call `endTestRun` now. No other tools are available.**",
                                    },
                                ],
                            };
                            return {
                                ...modifiedStep,
                                messages: [...modifiedStep.messages, warningMessage],
                                activeTools: ["endTestRun"],
                            };
                        }
                        // Warn from halfway point onwards
                        else if (step.stepNumber >= halfwayPoint && remainingSteps > 0) {
                            const warningMessage = {
                                role: "user",
                                content: [
                                    {
                                        type: "text",
                                        text: `**IMPORTANT: You have ${remainingSteps} step${remainingSteps === 1 ? "" : "s"} remaining. Wrap up testing and call \`endTestRun\` soon.**`,
                                    },
                                ],
                            };
                            return {
                                ...modifiedStep,
                                messages: [...modifiedStep.messages, warningMessage],
                            };
                        }
                        return modifiedStep;
                    },
                });
                let testSummary = null;
                // Process stream chunks with test-specific handling
                // - Text/reasoning is suppressed (not sent to UI)
                // - browser_playwright_action results become test steps
                // - endTestRun handles sending test_run_end message with reconciliation
                for await (const chunk of result.fullStream) {
                    // Extract summary from endTestRun result for the return value
                    if (chunk.type === "tool-result" &&
                        "toolName" in chunk &&
                        chunk.toolName === "endTestRun") {
                        const output = chunk.output;
                        if (output?.success) {
                            testSummary = {
                                testStatus: output.testStatus,
                                summary: output.summary,
                            };
                            logger.info(`[TestSubagent] Test summary extracted - ${output.summary}`);
                        }
                    }
                    await processTestStreamChunk(chunk, clark, services);
                }
                logger.info(`[TestSubagent] Test run completed: ${testRunId}`);
                // Build the final result from testSummary, using defaults for missing values
                const status = testSummary?.testStatus ?? "completed";
                const summary = testSummary?.summary ?? "Testing completed without summary.";
                // Note: test_run_end message is sent by endTestRun tool with reconciliation
                // We need to send it here only if endTestRun wasn't called (shouldn't happen normally)
                if (!testSummary) {
                    void sendUserMessage({
                        type: "test_run_end",
                        testRunId,
                        testStatus: "completed",
                        summary: "Testing completed.",
                        role: "assistant",
                        content: "",
                    });
                }
                return {
                    success: true,
                    testRunId,
                    status,
                    summary,
                };
            }
            catch (error) {
                logger.error(`[TestSubagent] Test run failed: ${error instanceof Error ? error.message : String(error)}`);
                // Send test_run_end with failed status
                await sendUserMessage({
                    type: "test_run_end",
                    testRunId,
                    testStatus: "failed",
                    role: "assistant",
                    content: "",
                });
                // Clear test run context
                clark.updateContext({
                    currentTestRunId: undefined,
                });
                throw error;
            }
        },
        getSummary: (input, output) => {
            const status = output.success ? output.status : "failed";
            const header = input.header || "test run";
            return {
                input: `"${header}"`,
                output: `Test run ${status}`,
            };
        },
    };
});
//# sourceMappingURL=start-test-run.js.map