import path from "path";
import fs from "fs";
import { SkyrampClient } from "@skyramp/skyramp";
import { analyzeOpenAPIWithGivenEndpoint } from "../utils/analyze-openapi.js";
import { isAuthorizationHeaderName, KNOWN_AUTH_HEADERS, getWorkspaceAuthConfig, getDefaultAuthHeader, getAuthScheme, WorkspaceAuthType, } from "../utils/workspaceAuth.js";
import { getPathParameterValidationError, OUTPUT_DIR_FIELD_NAME, PATH_PARAMS_FIELD_NAME, QUERY_PARAMS_FIELD_NAME, FORM_PARAMS_FIELD_NAME, validateParams, validatePath, validateRequestData, } from "../utils/utils.js";
import { getEntryPoint } from "../utils/telemetry.js";
import { getLanguageSteps } from "../utils/language-helper.js";
import { logger } from "../utils/logger.js";
import { normalizeLanguageParams } from "../utils/normalizeParams.js";
import { stageGeneratedPaths, resolveOutputDir } from "../utils/gitStaging.js";
import { getTestsRepoDir } from "../utils/AnalysisStateManager.js";
export class TestGenerationService {
    client;
    constructor() {
        this.client = new SkyrampClient();
    }
    async generateTest(params) {
        try {
            // Normalize language/framework to handle LLM case variations
            normalizeLanguageParams(params);
            // In cross-repo mode, redirect outputDir to the test repo clone.
            const resolved = resolveOutputDir(params.outputDir, getTestsRepoDir());
            if (resolved !== params.outputDir) {
                logger.info("Cross-repo: redirecting outputDir to test repo", {
                    original: params.outputDir,
                    redirected: resolved,
                });
                params.outputDir = resolved;
            }
            // Log prompt parameter using reusable utility
            logger.info("Generating test", {
                prompt: params.prompt,
                testType: this.getTestType(),
                language: params.language,
                outputDir: params.outputDir,
            });
            const validationResult = this.validateInputs(params);
            if (validationResult.isError) {
                return validationResult;
            }
            const generateOptions = this.buildGenerationOptions(params);
            const apiAnalysisResult = await this.handleApiAnalysis(params);
            if (apiAnalysisResult) {
                return apiAnalysisResult;
            }
            const result = await this.executeGeneration(generateOptions);
            const testType = this.getTestType();
            const languageSteps = getLanguageSteps({
                language: params.language || "",
                testType: testType,
                framework: params.framework,
            });
            // Orchestrate code optimization workflow based on flags
            let postGenerationMessage = "";
            if (params.codeReuse) {
                // Only code reuse
                postGenerationMessage += `
 Test generated successfully!

⏭️**CRITICAL NEXT STEP**: Running code reuse analysis with skyramp_reuse_code tool.
`;
            }
            else if (params.modularizeCode) {
                // Check if test type should be modularized
                if (!params.trace && !params.scenarioFile && !params.playwrightInput) {
                    postGenerationMessage += `
 Test generated successfully!

**NOTE**: Modularization is intentionally NOT applied to ${testType.toUpperCase()} tests.
${testType.toUpperCase()} tests are designed to be simple, straightforward, and deterministic. 
Modularization can introduce unpredictable results and unnecessary complexity for these test types.

The generated test file remains unchanged and ready to use as-is.
`;
                }
                else {
                    // Only modularization for UI, E2E, Integration tests
                    postGenerationMessage += `
 Test generated successfully!

⏭️**CRITICAL NEXT STEP**: Running modularization with skyramp_modularization tool.
`;
                }
            }
            return {
                content: [
                    {
                        type: "text",
                        text: `${postGenerationMessage}${result}

        ${languageSteps}`,
                    },
                ],
                isError: false,
            };
        }
        catch (error) {
            const errorMessage = error instanceof Error ? error.message : String(error);
            return {
                content: [
                    {
                        type: "text",
                        text: errorMessage,
                    },
                ],
                isError: true,
            };
        }
    }
    validateInputs(params) {
        const errList = { content: [], isError: true };
        const pathError = validatePath(params.outputDir, OUTPUT_DIR_FIELD_NAME);
        if (pathError?.content)
            errList.content.push(pathError.content[0]);
        [
            validateParams(params.pathParams ?? "", PATH_PARAMS_FIELD_NAME),
            validateParams(params.queryParams ?? "", QUERY_PARAMS_FIELD_NAME),
            validateParams(params.formParams ?? "", FORM_PARAMS_FIELD_NAME),
        ].forEach((error) => {
            if (error?.content)
                errList.content.push(error.content[0]);
        });
        if (params.requestData &&
            validateRequestData(params.requestData) === null) {
            errList.content.push({
                type: "text",
                text: "Error: requestData must be either a valid JSON string or an absolute path to a file.",
            });
        }
        const fw = (params.framework ?? "").toLowerCase();
        if (fw === "playwright" && params.output && params.output !== "") {
            const specPattern = /\.(spec|test)\.[tj]s$/;
            if (!specPattern.test(params.output)) {
                const parsed = path.parse(params.output);
                const suggested = /\.[tj]s$/.test(parsed.ext)
                    ? params.output.replace(/\.[tj]s$/, ".spec.ts")
                    : params.output + ".spec.ts";
                errList.content.push({
                    type: "text",
                    text: `Error: Playwright requires test files to match *.{spec}.{ts,js} (got "${params.output}"). ` +
                        `Rename to e.g. ${suggested} so Playwright can discover it.`,
                });
            }
        }
        return errList.content.length === 0
            ? { content: [], isError: false }
            : errList;
    }
    async handleApiAnalysis(params) {
        if (params.apiSchema && params.endpointURL && params.method === "") {
            try {
                const requiredPathParams = await analyzeOpenAPIWithGivenEndpoint(params.apiSchema, params.endpointURL, params.pathParams ?? "");
                if (requiredPathParams && requiredPathParams.trim().length > 0) {
                    return {
                        content: [
                            {
                                type: "text",
                                text: `${getPathParameterValidationError(requiredPathParams)}
                **IMPORTANT: MAKE SURE TO PROVIDE THE PATH PARAMS THAT DO NOT GIVE 404 ERROR FOR GET/DELETE METHODS. IT WILL BE USED AS PATH PARAMS IN THE TEST GENERATION.**`,
                            },
                        ],
                        isError: true,
                    };
                }
            }
            catch (error) {
                const errorMessage = error instanceof Error ? error.message : String(error);
                return {
                    content: [
                        {
                            type: "text",
                            text: `Error analyzing OpenAPI schema: ${errorMessage}`,
                        },
                    ],
                    isError: true,
                };
            }
        }
        return null;
    }
    // Standard HTTP headers that are never auth credentials.
    static STANDARD_HEADERS = new Set([
        "content-type",
        "accept",
        "user-agent",
        "host",
        "connection",
        "cache-control",
        "accept-encoding",
        "accept-language",
        "pragma",
        "origin",
        "referer",
        "content-length",
        "sec-ch-ua",
        "sec-ch-ua-mobile",
        "sec-ch-ua-platform",
        "sec-fetch-dest",
        "sec-fetch-mode",
        "sec-fetch-site",
        "sec-fetch-user",
        "upgrade-insecure-requests",
        "traceparent",
        "tracestate",
        "x-datadog-origin",
        "x-datadog-parent-id",
        "x-datadog-sampling-priority",
        "x-datadog-trace-id",
    ]);
    // KNOWN_AUTH_HEADERS is imported from workspaceAuth.ts — single source of truth
    // shared with trace-parser.ts for redaction. See workspaceAuth.ts for the full list.
    static simpleWildcardMatch(pattern, value) {
        const regex = new RegExp("^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$");
        return regex.test(value);
    }
    static traceMatchesFilters(entry, include, exclude) {
        const hostPath = `${entry.Destination ?? ""}${entry.Path ?? ""}`;
        if (include && include.length > 0) {
            if (!include.some((p) => this.simpleWildcardMatch(p, hostPath)))
                return false;
        }
        if (exclude && exclude.length > 0) {
            if (exclude.some((p) => this.simpleWildcardMatch(p, hostPath)))
                return false;
        }
        return true;
    }
    extractAuthFromTrace(traceFilePath, include, exclude) {
        try {
            const raw = fs.readFileSync(traceFilePath, "utf8");
            const requests = JSON.parse(raw);
            if (!Array.isArray(requests) || requests.length === 0)
                return null;
            const filtered = requests.filter((r) => TestGenerationService.traceMatchesFilters(r, include, exclude));
            // Pass 1: look for Authorization header (highest priority)
            for (const req of filtered) {
                const headers = req.RequestHeaders ?? {};
                for (const [name, values] of Object.entries(headers)) {
                    if (isAuthorizationHeaderName(name)) {
                        const value = (values ?? [])[0] ?? "";
                        const parts = value.split(" ", 2);
                        if (parts.length === 2) {
                            return { authHeader: name, authScheme: parts[0] };
                        }
                        return { authHeader: name, authScheme: "" };
                    }
                }
            }
            // Pass 2: look for known auth headers (Cookie, X-Api-Key, etc.)
            for (const req of filtered) {
                const headers = req.RequestHeaders ?? {};
                for (const [name] of Object.entries(headers)) {
                    const lower = name.toLowerCase();
                    if (TestGenerationService.STANDARD_HEADERS.has(lower))
                        continue;
                    if (KNOWN_AUTH_HEADERS.has(lower)) {
                        return { authHeader: name, authScheme: "" };
                    }
                }
            }
            return null;
        }
        catch {
            return null;
        }
    }
    async executeGeneration(generateOptions) {
        try {
            // Auto-resolve auth from workspace config when authHeader was not explicitly provided.
            // undefined = omit (auto-resolve); "" = explicit skip-auth for unauthenticated endpoints.
            if (generateOptions.authHeader === undefined) {
                try {
                    const repoPath = generateOptions.outputDir || process.cwd();
                    const wsAuth = await getWorkspaceAuthConfig(repoPath);
                    let resolvedHeader = wsAuth.authHeader ?? getDefaultAuthHeader(wsAuth.authType);
                    if (!resolvedHeader && wsAuth.authScheme !== undefined) {
                        resolvedHeader = "Authorization";
                    }
                    if (wsAuth.authType === WorkspaceAuthType.ApiKey && !resolvedHeader) {
                        logger.warning("workspace.yml has authType: apiKey but no authHeader is set — " +
                            "requests will be sent without an auth header and will likely receive 401/403. " +
                            "Add authHeader: <X-Your-Key-Header> to .skyramp/workspace.yml.");
                    }
                    if (resolvedHeader && wsAuth.authType !== WorkspaceAuthType.None) {
                        logger.info("authHeader not provided — resolved from workspace config", {
                            authHeader: resolvedHeader,
                            authType: wsAuth.authType,
                        });
                        generateOptions.authHeader = resolvedHeader;
                        if (isAuthorizationHeaderName(resolvedHeader)) {
                            generateOptions.authScheme =
                                generateOptions.authScheme ?? wsAuth.authScheme ?? getAuthScheme(wsAuth.authType);
                        }
                    }
                }
                catch {
                    logger.warning("Could not resolve auth from workspace config");
                }
            }
            if (generateOptions.traceFilePath) {
                const traceAuth = this.extractAuthFromTrace(generateOptions.traceFilePath, generateOptions.generateInclude, generateOptions.generateExclude);
                if (traceAuth) {
                    if (!generateOptions.authHeader) {
                        generateOptions.authHeader = traceAuth.authHeader;
                    }
                    if (!generateOptions.authScheme && traceAuth.authScheme) {
                        generateOptions.authScheme = traceAuth.authScheme;
                    }
                    else if (generateOptions.authScheme &&
                        traceAuth.authScheme &&
                        generateOptions.authScheme !== traceAuth.authScheme) {
                        logger.warning(`authScheme mismatch: parameter says "${generateOptions.authScheme}" ` +
                            `but trace contains "${traceAuth.authScheme}"; using trace value`);
                        generateOptions.authScheme = traceAuth.authScheme;
                    }
                }
            }
            // Map authScheme → authType for the npm client which uses authType internally
            if (generateOptions.authScheme !== undefined) {
                generateOptions.authType = generateOptions.authScheme;
                // authType handles the Authorization header implicitly — the Go CLI treats
                // --auth-type and --auth-header as mutually exclusive, so clear authHeader
                // when it's the standard Authorization header to avoid the conflict.
                if (isAuthorizationHeaderName(generateOptions.authHeader || '')) {
                    delete generateOptions.authHeader;
                }
            }
            delete generateOptions.authScheme;
            const result = await this.client.generateRestTest(generateOptions);
            // Check if the result indicates failure
            if (result && result.length > 0) {
                const lowerResult = result.toLowerCase();
                if (!lowerResult.includes("success")) {
                    throw new Error(`Test generation failed: ${result}`);
                }
            }
            // Stage so testbot includes the generated files in its output commit.
            await stageGeneratedPaths(generateOptions.outputDir);
            return `
**Generated Test Details:**
- Test Type: ${this.getTestType()}
- Output Directory: ${generateOptions.outputDir}
- Language: ${generateOptions.language}
- Framework: ${generateOptions.framework}

${result}`;
        }
        catch (error) {
            const errorMessage = error instanceof Error ? error.message : String(error);
            throw new Error(`Failed to execute test generation: ${errorMessage}`);
        }
    }
    buildBaseGenerationOptions(params) {
        return {
            testType: this.getTestType(),
            uri: params.endpointURL,
            method: params.method,
            apiSchema: params.apiSchema ? [params.apiSchema] : [],
            language: params.language ?? "python",
            framework: params.framework ??
                (params.language === "typescript" || params.language === "javascript"
                    ? "playwright"
                    : ""),
            output: params.output,
            outputDir: params.outputDir,
            force: params.force ?? true,
            deployDashboard: params.deployDashboard,
            runtime: params.runtime,
            dockerNetwork: params.dockerNetwork,
            dockerWorkerPort: params.dockerWorkerPort?.toString(),
            k8sNamespace: params.k8sNamespace,
            k8sConfig: params.k8sConfig,
            k8sContext: params.k8sContext,
            authHeader: params.authHeader,
            authScheme: params.authScheme,
            requestData: params.requestData,
            pathParams: params.pathParams,
            queryParams: params.queryParams,
            formParams: params.formParams,
            responseStatusCode: params.responseStatusCode,
            traceFilePath: params.trace ?? params.scenarioFile,
            generateInclude: params.include,
            generateExclude: params.exclude,
            generateInsecure: params.insecure,
            entrypoint: getEntryPoint(),
            chainingKey: params.chainingKey,
            mockPort: params.mockPort ?? 0,
            optionalFields: params.optionalFields ?? false,
        };
    }
}
