import { z } from "zod";
import { logger } from "../utils/logger.js";
import * as fs from "fs/promises";
import * as path from "path";
import { AnalyticsService } from "../services/AnalyticsService.js";
import { TEST_CATEGORIES, externalCategory } from "../types/TestRecommendation.js";
import { TestType, HttpMethod } from "../types/TestTypes.js";
import { DriftAction } from "../types/TestAnalysis.js";
// Drift actions that actually modify a test file. VERIFY and IGNORE are
// no-ops (the test was assessed but left unchanged), so they must not count
// toward "tests maintained" telemetry.
const MAINTENANCE_CHANGE_ACTIONS = new Set([
    DriftAction.Update,
    DriftAction.Regenerate,
    DriftAction.Delete,
]);
const TOOL_NAME = "skyramp_submit_report";
const DEFAULT_COMMIT_MESSAGE = "Added recommendations by Skyramp Testbot.";
const testResultSchema = z.object({
    testType: z.nativeEnum(TestType).describe("Type of test. Do not include priority or other metadata in this field."),
    endpoint: z.string().describe("HTTP verb and path, e.g. 'GET /api/v1/products'"),
    status: z.enum(["Pass", "Fail", "Skipped"]).describe("Test execution result"),
    details: z.string().describe("One sentence — no embedded newlines, no markdown. e.g. '10.8s, products_contract_test.py' or 'failed: <one-line error summary>, products_contract_test.py'"),
});
// TODO: Unify newTestSchema and additionalRecommendationSchema into a single
// interface that adds an `implemented: boolean` field. Both describe the same
// concept (a test recommendation) — the only difference is whether it was
// generated in this run or left for later. Tracked per Archit's review comment.
const newTestSchema = z.object({
    testId: z.string().describe("Human-readable kebab-case identifier, e.g. 'contract-get-products' or 'integration-users-orders-workflow'. Format: '<testType>-<method>-<resource>' for single-endpoint tests or '<testType>-<scenario-slug>' for multi-step tests. Must be unique within the report."),
    testType: z.nativeEnum(TestType).describe("Type of test created. Do not include priority or other metadata in this field."),
    category: z.preprocess((val) => externalCategory(val), z.enum(TEST_CATEGORIES)).describe("Test category — critical categories (security_boundary, business_rule, data_integrity, breaking_change) get generation priority over workflow"),
    endpoint: z.string().describe("HTTP verb and path, e.g. 'GET /api/v1/products'"),
    fileName: z.string().describe("Name of the generated test file"),
    description: z.string().optional().describe("What the test does — the steps and assertions, not the bugs it finds. e.g. 'Creates a collection, adds a link, then verifies the link exists'. Do NOT describe expected failures or bugs here — those belong in issuesFound."),
    scenarioFile: z.string().optional().describe("Path to the scenario JSON file if one was generated (e.g. 'tests/scenario_collections-links.json')"),
    traceFile: z.string().optional().describe("Path to the backend trace file if used or created"),
    frontendTrace: z.string().optional().describe("Path to the Playwright/UI trace file if used or created"),
    reasoning: z.string().describe("Why this test was created: what production risk it mitigates, what code pattern it targets, or what coverage gap it fills"),
});
const descriptionSchema = z.object({
    description: z.string().describe("One-line description. Do NOT prefix with the severity level — severity is a separate field. Include code logic bugs from the diff, test generation/execution failures, and environment misconfiguration."),
    severity: z
        .enum(["critical", "high", "medium", "low"])
        .optional()
        .describe("Issue severity. critical = feature broken/unusable (e.g. page doesn't load, data corruption). " +
        "high = incorrect behavior (e.g. wrong calculation, stale data returned). " +
        "medium = minor functional gap. low = cosmetic or informational."),
});
const scenarioStepSchema = z.object({
    method: z.nativeEnum(HttpMethod).optional().describe("HTTP method. Required for API steps, omit for UI/E2E actions."),
    path: z.string().optional().describe("Endpoint or page path (e.g. '/api/v1/products' or '/products'). Required for API steps, omit for UI actions."),
    description: z.string().describe("What this step does, e.g. 'Create a product' or 'Click checkout button and verify confirmation'"),
    expectedStatusCode: z.number().optional().describe("Expected HTTP status code, e.g. 200, 201, 404"),
    requestBody: z.record(z.any()).optional().describe("Example request body with realistic field values"),
    responseBody: z.record(z.any()).optional().describe("Key response fields to verify, e.g. { id: 'number', name: 'string', in_stock: 'boolean?' }"),
});
export const additionalRecommendationSchema = z.object({
    testId: z.string().describe("Human-readable kebab-case identifier, e.g. 'integration-products-orders-workflow' or 'e2e-checkout-flow'. Format: '<testType>-<scenario-slug>'. Must be unique within the report."),
    testType: z.nativeEnum(TestType).describe("Type of test. Do not include priority or other metadata in this field."),
    category: z.preprocess((val) => externalCategory(val), z.enum(TEST_CATEGORIES)).describe("Test category — critical categories get generation priority over workflow"),
    primaryEndpoint: z.string().optional().describe("The focal endpoint this test targets, e.g. 'PATCH /api/v1/orders/{order_id}'. Required for single-step contract tests. For multi-step integration or E2E scenarios, omit — the steps array is the authoritative source of all endpoints involved."),
    scenarioName: z.string().optional().describe("Proposed scenario name for future generation, e.g. 'products-orders-workflow'. No file exists yet — this is a suggestion only. Omit if not applicable."),
    // TODO: replace text with max(3) and check for regression
    steps: z.array(scenarioStepSchema).describe("Ordered sequence of API/UI steps in this test scenario (at most 3). Each step must include method and path so the endpoints are explicit. Omit requestBody and responseBody from steps."),
    description: z.string().describe("What this test does and why it is valuable. For multi-step scenarios, list the endpoints involved (e.g. 'POST /api/v1/products → POST /api/v1/orders → PATCH /api/v1/orders/{id}') so the recommendation is actionable without primaryEndpoint."),
    priority: z.preprocess((val) => (typeof val === "string" ? val.toLowerCase() : val), z.enum(["high", "medium", "low"])).describe("Priority level: high, medium, or low. First check diff relevance — does the test target an endpoint changed in this PR? HIGH: diff-relevant security/auth/error tests, cross-resource isolation for diff endpoints, CRUD lifecycle for NEW endpoints in the diff. MEDIUM: diff-relevant business-rule happy paths, multi-resource workflows involving diff endpoints, security/error tests for NON-diff endpoints. LOW: tests targeting only unchanged endpoints, trivially discoverable happy paths duplicating generated tests."),
    openApiSpec: z.string().optional().describe("Path to OpenAPI/Swagger spec file if available, e.g. 'openapi.yaml'"),
    backendTrace: z.string().optional().describe("Path to backend trace file if available, e.g. 'tests/skyramp-traces.json'. Used by integration and E2E tests."),
    frontendTrace: z.string().optional().describe("Path to Playwright/UI trace file if available, e.g. 'tests/skyramp-playwright.zip'. UI tests need this; E2E tests need both frontend and backend traces."),
    reasoning: z.string().describe("Why this test is recommended: the specific production risk, business rule, or security boundary it would validate"),
}).superRefine((rec, ctx) => {
    if (rec.testType === TestType.CONTRACT && !rec.primaryEndpoint) {
        ctx.addIssue({
            code: z.ZodIssueCode.custom,
            path: ["primaryEndpoint"],
            message: "primaryEndpoint is required for contract tests — provide the HTTP verb and path, e.g. 'PATCH /api/v1/orders/{order_id}'.",
        });
    }
});
const testMaintenanceSchema = z.object({
    testType: z.nativeEnum(TestType).describe("Type of test."),
    endpoint: z.string().describe("HTTP verb and path, e.g. 'GET /api/v1/products'"),
    fileName: z.string().describe("Test file that was maintained, e.g. 'products_smoke_test.py'"),
    action: z.nativeEnum(DriftAction).optional().describe("The drift action taken for this test, exactly as decided by the Action Decision Matrix: UPDATE, REGENERATE, or DELETE modify the test; VERIFY or IGNORE leave it unchanged (no-op)."),
    description: z.string().describe("What was changed and why"),
    beforeStatus: z.enum(["Pass", "Fail", "Error"]).describe("Test result BEFORE modification"),
    beforeDetails: z.string().describe("Execution output/timing before modification, or 'baseline from CI workflow <name>' if a parallel workflow provided the baseline"),
    afterStatus: z.enum(["Pass", "Fail", "Error", "Skipped"]).describe("Test result AFTER modification"),
    afterDetails: z.string().describe("Execution output/timing after modification"),
});
/**
 * Derive per-run analytics counts from a submitted report. These power the
 * alpha-launch dashboards (tests generated/maintained, suite growth, bugs vs
 * test failures, flakiness recovery). All values are stringified for Amplitude.
 *
 * Bug-vs-failure distinction:
 *  - issuesFound* = bugs/issues the agent flagged (real defects + severity)
 *  - testsFailed = test executions that returned "Fail" (test-level outcome)
 * These are intentionally separate metrics — a failing test is not always a bug.
 *
 * maintenanceRecovered approximates flakiness/regression fixes: tests that were
 * Fail/Error before maintenance and Pass afterward.
 *
 * testsMaintained counts only entries that actually changed a test file
 * (action UPDATE/REGENERATE/DELETE). VERIFY/IGNORE entries are reported for
 * transparency but are no-ops, so they are excluded. When `action` is absent
 * (older reports), we fall back to the status heuristic: an IGNORE no-op sets
 * afterStatus to "Skipped", so anything else is treated as a real change.
 */
function isMaintenanceChange(m) {
    if (m.action) {
        return MAINTENANCE_CHANGE_ACTIONS.has(m.action);
    }
    return m.afterStatus !== "Skipped";
}
function computeReportMetrics(params) {
    const recommendations = params.additionalRecommendations ?? [];
    const countBy = (items, pred) => items.filter(pred).length;
    const changedMaintenance = params.testMaintenance.filter(isMaintenanceChange);
    const maintenanceRecovered = countBy(changedMaintenance, (m) => m.beforeStatus !== "Pass" && m.afterStatus === "Pass");
    return {
        testsGenerated: String(params.newTestsCreated.length),
        testsMaintained: String(changedMaintenance.length),
        recommendationsCount: String(recommendations.length),
        maintenanceRecovered: String(maintenanceRecovered),
        testsPassed: String(countBy(params.testResults, (t) => t.status === "Pass")),
        testsFailed: String(countBy(params.testResults, (t) => t.status === "Fail")),
        testsSkipped: String(countBy(params.testResults, (t) => t.status === "Skipped")),
        issuesFound: String(params.issuesFound.length),
        issuesCritical: String(countBy(params.issuesFound, (i) => i.severity === "critical")),
        issuesHigh: String(countBy(params.issuesFound, (i) => i.severity === "high")),
        issuesMedium: String(countBy(params.issuesFound, (i) => i.severity === "medium")),
        issuesLow: String(countBy(params.issuesFound, (i) => i.severity === "low")),
    };
}
export function registerSubmitReportTool(server) {
    server.registerTool(TOOL_NAME, {
        description: "Submit the final testbot report. Call this tool once after completing all test analysis, generation, and execution. " +
            "This is the ONLY way to submit the report — do NOT write the report to a file manually.",
        inputSchema: {
            summaryOutputFile: z
                .string()
                .describe("The file path where the report should be written (provided in the task instructions)"),
            businessCaseAnalysis: z
                .string()
                .describe("1-2 sentences describing what user-facing interactions this PR enables or changes (e.g. 'customers can now leave and view product reviews'). Focus on the user journey, not technical implementation. Flag backend-only or frontend-only gaps."),
            newTestsCreated: z
                .array(newTestSchema)
                .describe("List of new tests created. Use empty array [] if none."),
            additionalRecommendations: z
                .array(additionalRecommendationSchema)
                .optional()
                .default([])
                .describe("Recommended tests that were not generated (lower priority). Only include recommendations that add distinct coverage beyond generated tests — do not pad with variants testing the same endpoint and flow."),
            testMaintenance: z
                .array(testMaintenanceSchema)
                .describe("List of existing test modifications with before/after execution results. Use empty array [] if none."),
            testResults: z
                .array(testResultSchema)
                .describe("List of ALL test execution results. One entry per test executed."),
            issuesFound: z
                .array(descriptionSchema)
                .describe("List of issues, failures, or bugs found. Use empty array [] if none."),
            nextSteps: z
                .array(z.string())
                .optional()
                .default([])
                .describe("Actionable follow-ups for the PR author. Each entry must be a single-line string (no embedded newlines). " +
                "Include a next step for every critical/high severity issue in issuesFound. No next steps for low-severity issues. " +
                "If multiple tests fail with 404 or connection refused: suggest checking targetSetupCommand/targetReadyCheckCommand. " +
                "If 401/403 on auth endpoints: suggest authTokenCommand. " +
                "When referencing code, use file name and relevant code pattern — no line numbers unless certain."),
            commitMessage: z
                .string()
                .optional()
                .default(DEFAULT_COMMIT_MESSAGE)
                .describe("Succinct commit message (if possible, under 72 chars) summarizing what Testbot did, " +
                "e.g. 'add contract tests for /products endpoint' or 'update smoke tests for order API changes'. " +
                "Used as both the git commit subject and the side PR title — the consumer applies truncation as needed."),
        },
        _meta: {
            keywords: ["report", "summary", "testbot", "submit"],
        },
    }, async (params) => {
        const startTime = Date.now();
        let errorResult;
        const deduplicateById = (items) => {
            const seen = new Set();
            const result = [];
            for (const item of items) {
                let id = item.testId;
                if (seen.has(id)) {
                    let suffix = 2;
                    while (seen.has(`${id}-${suffix}`))
                        suffix++;
                    id = `${id}-${suffix}`;
                    item.testId = id;
                }
                seen.add(id);
                result.push(item);
            }
            return result;
        };
        const dedupedNewTests = deduplicateById([...params.newTestsCreated]);
        const dedupedRecommendations = deduplicateById([...(params.additionalRecommendations ?? [])]);
        // Strip generation-artifact fields from newTestsCreated before writing.
        // scenarioFile, traceFile, frontendTrace are internal paths used during
        // generation — downstream scoring scripts don't expect them and fail if
        // they encounter these string fields while traversing the object.
        const sanitizedNewTests = dedupedNewTests.map(({ scenarioFile: _sf, traceFile: _tf, frontendTrace: _ft, ...rest }) => rest);
        const reportJson = JSON.stringify({
            businessCaseAnalysis: params.businessCaseAnalysis,
            newTestsCreated: sanitizedNewTests,
            additionalRecommendations: dedupedRecommendations,
            testMaintenance: params.testMaintenance,
            testResults: params.testResults,
            issuesFound: params.issuesFound,
            nextSteps: params.nextSteps ?? [],
            commitMessage: (params.commitMessage ?? "").replace(/[\r\n]+/g, " ").trim() || DEFAULT_COMMIT_MESSAGE,
        }, null, 2);
        logger.info("Submitting testbot report", {
            outputFile: params.summaryOutputFile,
            payloadBytes: reportJson.length,
            testResultCount: params.testResults.length,
        });
        try {
            await fs.mkdir(path.dirname(params.summaryOutputFile), { recursive: true });
            await fs.writeFile(params.summaryOutputFile, reportJson, "utf-8");
            const elapsed = Date.now() - startTime;
            logger.info("Testbot report written successfully", {
                outputFile: params.summaryOutputFile,
                elapsedMs: elapsed,
            });
            return {
                content: [
                    {
                        type: "text",
                        text: `Report submitted successfully to ${params.summaryOutputFile}`,
                    },
                ],
            };
        }
        catch (error) {
            const elapsed = Date.now() - startTime;
            const errorMessage = `Failed to write report: ${error.message}`;
            logger.error(errorMessage, { error, elapsedMs: elapsed });
            errorResult = {
                content: [{ type: "text", text: errorMessage }],
                isError: true,
            };
            return errorResult;
        }
        finally {
            AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, {
                summary_output_file: params.summaryOutputFile,
                testResultCount: String(params.testResults.length),
                payloadBytes: String(reportJson.length),
                ...computeReportMetrics(params),
            }).catch(() => { });
        }
    });
}
