import path from "path";
import { z } from "zod";
import { baseTestSchema, TestType } from "../../types/TestTypes.js";
import { TestGenerationService, } from "../../services/TestGenerationService.js";
import { AnalyticsService } from "../../services/AnalyticsService.js";
import { getPersonaPrefix } from "../../prompts/personas.js";
import { isContractConsumerModeEnabled, isTestbotEnabled } from "../../utils/featureFlags.js";
// SKYRAMP_FEATURE_CONTRACT_CONSUMER_MODE gates BOTH:
//   1. Consumer-side contract test generation (`consumerMode` /
//      `consumerOutput` schema fields, validation, post-gen instructions,
//      and the consumer wording in the tool description).
//   2. The SDK's "default mode" behavior — when neither `providerMode` nor
//      `consumerMode` is set, the SDK generates BOTH provider and consumer
//      tests. With the flag OFF that fallback is also disabled, so the only
//      mode supported is provider.
//
// `providerMode` itself is exposed regardless of the flag.
const CONSUMER_MODE_ENABLED = isContractConsumerModeEnabled();
const ADD_ASSERTIONS_DEFAULT = isTestbotEnabled();
// "Requires apiSchema." plus a mode-aware "Not allowed with ..." clause for
// the parent-provisioning fields. Generated once so wording stays in lockstep
// with the feature flag.
const PARENT_FIELD_NOT_ALLOWED_NOTE = CONSUMER_MODE_ENABLED
    ? "Requires apiSchema. Not allowed with consumerMode or skipProvisionParents."
    : "Requires apiSchema. Not allowed with skipProvisionParents.";
const baseContractTestSchema = {
    enhanceAssertions: z
        .boolean()
        .default(ADD_ASSERTIONS_DEFAULT)
        .describe("When true, calls skyramp_enhance_assertions after test generation to add richer response-body assertions. Disabled by default. Automatically enabled when running as testbot-feature. Do not override the default value of this parameter, unless the user explicitly asks to enable it."),
    ...baseTestSchema,
    pathParams: z
        .string()
        .default("")
        .describe("Comma-separated path parameter values in the form 'key=value,key2=value2' (e.g., 'id=1,name=John'). " +
        "IMPORTANT: If the user-provided URL contains path parameter placeholders (e.g., '/products/{product_id}'), do NOT fill in values for them here — pass the URL as-is and leave pathParams empty. " +
        "The backend resolves path parameters automatically via the parent provisioning mechanism (parentRequestData). " +
        "Only populate this field if the user has explicitly stated specific path parameter values to use."),
    assertOptions: z
        .string()
        .optional()
        .describe("Custom assertion options for contract validation"),
    responseData: z
        .string()
        .optional()
        .describe("Sample response body data, provided either as an inline JSON/YAML string or as an absolute file path prefixed with '@' (e.g., @/absolute/path/to/file)."),
    providerMode: z
        .boolean()
        // Default mirrors the runtime gate: when the consumer-mode feature flag
        // is OFF, provider is the only supported mode, so the schema documents
        // it as the default. When ON, both modes default to false (caller must
        // opt in) to preserve the historical behavior.
        .default(!CONSUMER_MODE_ENABLED)
        .describe(CONSUMER_MODE_ENABLED
        ? "Generate provider-side contract test that validates the API implementation against the contract"
        : "Generate provider-side contract test that validates the API implementation against the contract. Defaults to true — provider is the only supported mode in this build."),
    providerOutput: z
        .string()
        .optional()
        .refine((val) => {
        if (!val || val === "")
            return true;
        const validExtensions = [".py", ".ts", ".js", ".java"];
        return validExtensions.some((ext) => val.endsWith(ext));
    }, { message: "Output file must have one of these extensions: .py, .ts, .js, .java" })
        .describe("Output file for the generated provider contract test. " +
        "For Playwright framework, the file must match *.spec.ts so Playwright can discover it " +
        "(e.g. contract_provider_orders.spec.ts). " +
        "For other frameworks (pytest, junit), standard extensions are fine (.py, .java). " +
        "Optional — when omitted, Skyramp generates a default name."),
    parentRequestData: z
        .record(z.string(), z.string())
        .optional()
        .describe("Map of sample request bodies for provisioning parent resources before the contract test. " +
        "IMPORTANT: Each key MUST be the exact path parameter variable name as it appears inside the curly braces in the URL path — NOT an operation ID, endpoint name, or any other identifier. " +
        "For example, for endpoint '/products/{product_id}/reviews', the key is 'product_id' (not 'create_product', not '/products', not any operation name). " +
        "The value is the sample request body JSON string used to create that parent resource. " +
        "For example: {\"product_id\": \"{\\\"name\\\": \\\"sample product\\\", \\\"price\\\": 10}\"}. " +
        "For nested parents like '/a/{a_id}/b/{b_id}/c', provide an entry for each: {\"a_id\": \"{...}\", \"b_id\": \"{...}\"}. " +
        "Path parameters without an entry here should be supplied via pathParams instead. " +
        PARENT_FIELD_NOT_ALLOWED_NOTE),
    parentStatusCode: z
        .record(z.string(), z.string())
        .optional()
        .describe("Map of expected HTTP status codes for parent resource provisioning requests. " +
        "IMPORTANT: Each key MUST be the exact path parameter variable name as it appears inside the curly braces in the URL path — the same keys used in parentRequestData. " +
        "For example, for endpoint '/products/{product_id}/reviews', the key is 'product_id' (not an operation name or endpoint path). " +
        "The value is the expected HTTP status code string for the provisioning call that creates that parent resource. " +
        "For example: {\"product_id\": \"201\"}. " +
        PARENT_FIELD_NOT_ALLOWED_NOTE),
    parentFormParams: z
        .record(z.string(), z.string())
        .optional()
        .describe("Map of form parameters for provisioning parent resources. " +
        "Key is the path param variable name (e.g. 'product_id'); " +
        "value is comma-separated key=value pairs (e.g. 'field1=val1,field2=val2'). " +
        PARENT_FIELD_NOT_ALLOWED_NOTE),
    skipProvisionParents: z
        .boolean()
        .default(false)
        .describe("When true, skips generating setup/teardown functions for the provider contract test. Requires providerMode to be enabled. Not allowed together with parentRequestData or parentStatusCode."),
};
const consumerModeSchemaFields = {
    consumerMode: z
        .boolean()
        .default(false)
        .describe("Generate consumer-side contract test that validates consumer expectations against the API"),
    consumerOutput: z
        .string()
        .optional()
        .refine((val) => {
        if (!val || val === "")
            return true;
        const validExtensions = [".py", ".ts", ".js", ".java"];
        return validExtensions.some((ext) => val.endsWith(ext));
    }, { message: "Output file must have one of these extensions: .py, .ts, .js, .java" })
        .describe("Output file for the generated consumer contract test. " +
        "For Playwright framework, the file must match *.spec.ts so Playwright can discover it " +
        "(e.g. contract_consumer_orders.spec.ts). " +
        "For other frameworks (pytest, junit), standard extensions are fine (.py, .java). " +
        "Optional — when omitted, Skyramp generates a default name."),
};
const contractTestSchema = CONSUMER_MODE_ENABLED
    ? { ...baseContractTestSchema, ...consumerModeSchemaFields }
    : baseContractTestSchema;
export class ContractTestService extends TestGenerationService {
    getTestType() {
        return TestType.CONTRACT;
    }
    async handleApiAnalysis(params) {
        // When apiSchema is provided, path parameters are resolved via parentRequestData
        // provisioning — skip the base-class path-parameter validation prompt.
        // Without apiSchema, fall through to normal validation.
        if (params.apiSchema) {
            return null;
        }
        return super.handleApiAnalysis(params);
    }
    async generateTest(params) {
        // With the consumer-mode flag OFF, force provider-only behavior up front
        // so every downstream consumer of `params.providerMode` (validateInputs,
        // the post-gen instruction branch below, and buildGenerationOptions) sees
        // a consistent value. Otherwise a caller that omits `providerMode` would
        // hit provider-only validation rules with `providerMode === false` and
        // get the wrong post-gen instructions even though generation itself runs
        // in provider mode.
        if (!CONSUMER_MODE_ENABLED) {
            params.providerMode = true;
        }
        const result = await super.generateTest(params);
        if (result.isError)
            return result;
        const content = [...result.content];
        if (params.providerMode) {
            content.push({
                type: "text",
                text: this.buildProviderPostGenInstructions(params, params.enhanceAssertions),
            });
        }
        else {
            const sampleDataInstructions = this.buildSampleDataInstructions(params);
            if (sampleDataInstructions) {
                content.push({ type: "text", text: sampleDataInstructions });
            }
        }
        // Consumer wiring guidance is additive — emit it whenever a consumer
        // test was generated, even if provider was also generated in the same
        // call (`providerMode: true` + `consumerMode: true`). Lives outside the
        // provider branch so the both-modes case is not silently dropped.
        // (When the consumer-mode flag is OFF, providerMode is normalized to
        // true above and `consumerMode` is not on the schema, so this branch is
        // unreachable.)
        if (CONSUMER_MODE_ENABLED && params.consumerMode) {
            content.push({
                type: "text",
                text: this.buildConsumerStubReplacementInstructions(),
            });
        }
        return { ...result, content };
    }
    buildSampleDataSection(params) {
        const sections = [];
        sections.push("- **Request body sample data**: the inline JSON/dict literal passed as the request body in the test function.");
        if (!params.skipProvisionParents) {
            sections.push("- **Parent provisioning request data**: the inline JSON/dict literals passed as request bodies inside setup/teardown functions that create parent resources.");
        }
        return sections.join("\n");
    }
    buildPlaceholderReplacementBlock(params, nextStepNote) {
        const step5 = nextStepNote
            ? `5. Write the updated file, then ${nextStepNote}.`
            : `5. Write the updated file.`;
        return `Inspect the following locations in the generated file and replace any placeholder values with realistic ones:

${this.buildSampleDataSection(params)}

**A value is a placeholder if it is any of the following — regardless of where it came from:**
- A JSON/OpenAPI type name: "string", "integer", "number", "boolean", "object", "array"
- A format descriptor: "uuid", "date", "date-time", "email", "uri", "url", "binary", "byte", "password"
- A generic filler: "example", "sample", "test", "foo", "bar", "baz", "value", "unknown", "none", "null", "N/A"
- An integer that is exactly 0 or 1 with no semantic meaning from the field name
- Any value that is clearly a type name or format name rather than a real datum

**How to replace:**
- Use the field name, OpenAPI schema descriptions/enums, and repository context to infer a realistic value.
- Examples: an "email" field → "user@example.com"; a "price" field → 29.99; a "status" field → the first valid enum value from the spec; a "uuid" value for an "id" field → "a1b2c3d4-e5f6-7890-abcd-ef1234567890".

**Exactly what to do:**
1. Open the generated file.
2. Find only the inline JSON/dict literals identified above.
3. Replace every placeholder value using the rules above.
4. Leave all realistic values unchanged.
${step5}

**What NOT to do — any of these is a violation:**
- Do NOT change function signatures, method names, class names, imports, or variable names.
- Do NOT add, remove, or reorder any functions, classes, or test cases.
- Do NOT change assertion logic, HTTP methods, URLs, headers, or status code checks.
- Do NOT reformat, reorder, or rewrite any code outside the identified JSON/dict literals.
- Do NOT add comments or docstrings.
- Do NOT extract, hoist, or expose any request body or data as a global variable, module-level constant, or shared fixture — keep all data inline exactly where it already is.
- Do NOT reference or reuse request body data across functions for comparison or any other purpose.
- Do NOT make any other "improvements" beyond the value replacements described above.`;
    }
    buildSampleDataInstructions(params) {
        return `
### CRITICAL NEXT STEP — Replace placeholder sample data

${this.buildPlaceholderReplacementBlock(params)}
`;
    }
    buildProviderPostGenInstructions(params, enhanceAssertions = false) {
        const steps = [];
        const nextStepNote = enhanceAssertions ? "continue to Step 2" : undefined;
        steps.push(`### Step ${steps.length + 1} — Replace placeholder values [REQUIRED]\n\n${this.buildPlaceholderReplacementBlock(params, nextStepNote)}`);
        if (enhanceAssertions) {
            steps.push(`### Step ${steps.length + 1} — Enhance response body assertions [REQUIRED]\nCall \`skyramp_enhance_assertions\` with \`testFile\` set to the absolute path of the generated provider contract test file, \`testType: "contract"\`, and \`enhanceType: "generation"\`. Apply every instruction returned to that file.`);
        }
        const heading = enhanceAssertions
            ? "Replace placeholder data and enhance assertions"
            : "Replace placeholder data";
        return `\n### CRITICAL NEXT STEP — ${heading}\n\n${steps.join("\n\n")}`;
    }
    buildConsumerStubReplacementInstructions() {
        return `
⏭️ **Next step — Wire in real consumer code:**

The generated consumer contract test contains a stub test function that uses Skyramp's \`send_request\` library to directly call the mocked endpoint. The purpose of a consumer contract test is to verify that the consumer's own code correctly interacts with the mocked endpoint — not to call it directly. You must create a meaningful version of this test alongside the original stub.

**What to do:**

1. **Keep the original stub test function exactly as generated** — do not modify it.

2. **Duplicate the test function** and give the copy a distinct name (e.g., append \`_consumer\` or \`_real\` to the function name).

3. **In the duplicate only**, replace the stub function body (the \`send_request\` call and any surrounding boilerplate that directly hits the endpoint) with the actual consumer-side code that triggers the same endpoint:
   - Search the codebase for the consumer code that calls this endpoint (look for the HTTP method + path pattern, SDK client calls, or service layer methods).
   - Import or invoke that consumer code in place of the \`send_request\` stub.
   - Ensure the mock is still active during the call so the response is intercepted by Skyramp.

4. **Preserve everything else in the duplicate unchanged**: mock setup, teardown, response assertions, test structure, imports (add only what is needed for the consumer code).

**Scope rules:**
- Do NOT modify the original stub test function or any other function in the file.
- Only change the stub call inside the duplicated test function — not assertions, mock configuration, or setup/teardown.
- If the consumer code cannot be found in the codebase, leave a clearly marked \`TODO\` comment in the duplicate's body and do not fabricate code.
- Do NOT restructure, reformat, or reorder any existing code outside the new duplicate function.
- Do NOT add comments or docstrings beyond the \`TODO\` if needed.
`;
    }
    validateInputs(params) {
        const errList = super.validateInputs(params);
        if (errList.isError)
            return errList;
        const errors = [];
        if ((params.parentRequestData || params.parentStatusCode || params.parentFormParams) &&
            !params.apiSchema) {
            errors.push("parentRequestData, parentStatusCode, and parentFormParams are only allowed when apiSchema is provided.");
        }
        if (params.providerOutput && !params.providerMode) {
            errors.push("providerOutput is only valid when providerMode is enabled.");
        }
        if (CONSUMER_MODE_ENABLED) {
            if (params.consumerOutput && !params.consumerMode) {
                errors.push("consumerOutput is only valid when consumerMode is enabled.");
            }
            if (params.consumerMode && (params.parentRequestData || params.parentStatusCode || params.parentFormParams)) {
                errors.push("parentRequestData, parentStatusCode, and parentFormParams are not allowed when consumerMode is enabled.");
            }
        }
        const fw = (params.framework ?? "").toLowerCase();
        if (fw === "playwright") {
            const specPattern = /\.(spec|test)\.[tj]s$/;
            const playwrightOutputFields = [
                ["providerOutput", params.providerOutput],
            ];
            if (CONSUMER_MODE_ENABLED) {
                playwrightOutputFields.push(["consumerOutput", params.consumerOutput]);
            }
            for (const [field, value] of playwrightOutputFields) {
                if (value && value !== "") {
                    if (!specPattern.test(value)) {
                        const parsed = path.parse(value);
                        const suggested = /\.[tj]s$/.test(parsed.ext)
                            ? value.replace(/\.[tj]s$/, ".spec.ts")
                            : value + ".spec.ts";
                        errors.push(`Error: Playwright requires test files to match *.{spec}.{ts,js} (got "${value}" for ${field}). ` +
                            `Rename to e.g. ${suggested} so Playwright can discover it.`);
                    }
                }
            }
        }
        if (params.skipProvisionParents) {
            if (!params.providerMode) {
                errors.push("skipProvisionParents requires providerMode to be enabled.");
            }
            if (params.parentRequestData || params.parentStatusCode || params.parentFormParams) {
                errors.push("parentRequestData, parentStatusCode, and parentFormParams are not allowed when skipProvisionParents is enabled.");
            }
        }
        if (errors.length > 0) {
            return {
                content: errors.map((text) => ({ type: "text", text })),
                isError: true,
            };
        }
        return { content: [], isError: false };
    }
    buildGenerationOptions(params) {
        // `params.providerMode` is normalized to `true` in `generateTest` when the
        // consumer-mode flag is OFF, so reading it directly here is safe and stays
        // consistent with `validateInputs` and the post-gen instruction branch.
        // This also prevents the SDK's "neither mode set ⇒ generate both" default
        // from leaking through.
        const baseOptions = {
            ...super.buildBaseGenerationOptions(params),
            assertOption: params.assertOptions,
            responseData: params.responseData,
            providerMode: params.providerMode,
            providerOutput: params.providerOutput,
            parentRequestData: params.parentRequestData,
            parentStatusCode: params.parentStatusCode,
            parentFormParams: params.parentFormParams,
            skipProvisionParents: params.skipProvisionParents,
        };
        if (CONSUMER_MODE_ENABLED) {
            return {
                ...baseOptions,
                consumerMode: params.consumerMode,
                consumerOutput: params.consumerOutput,
            };
        }
        return baseOptions;
    }
}
const TOOL_NAME = "skyramp_contract_test_generation";
const MODES_SECTION = CONSUMER_MODE_ENABLED
    ? `**Modes:**
- Default (no mode set): both \`providerMode\` and \`consumerMode\` default to false. This generates both provider and consumer contract tests — equivalent to setting both modes to true.
- \`providerMode\`: set to true ONLY if the user explicitly requests a provider-side contract test. Optionally specify \`providerOutput\` for the output file path.
- \`consumerMode\`: set to true ONLY if the user explicitly requests a consumer-side contract test. Optionally specify \`consumerOutput\` for the output file path.
- Both \`providerMode\` and \`consumerMode\` can be enabled simultaneously to generate both sides.`
    : `**Mode:**
- Only provider-side contract tests are generated. Pass \`providerMode: true\` (or omit it — the tool always behaves as provider-only). Optionally specify \`providerOutput\` for the output file path.`;
const CHAINING_SECTION = CONSUMER_MODE_ENABLED
    ? `**Chaining (requires \`apiSchema\`):**
- \`parentRequestData\`: map of parent request data for chained test generation. Key = exact path parameter name. Value = JSON string of the request body to create that parent resource. Not allowed with \`consumerMode\` or \`skipProvisionParents\`.
- \`parentStatusCode\`: expected HTTP status code for each parent provisioning call (e.g. \`{"id": "201"}\`). Not allowed with \`consumerMode\` or \`skipProvisionParents\`.`
    : `**Chaining (requires \`apiSchema\`):**
- \`parentRequestData\`: map of parent request data for chained test generation. Key = exact path parameter name. Value = JSON string of the request body to create that parent resource. Not allowed with \`skipProvisionParents\`.
- \`parentStatusCode\`: expected HTTP status code for each parent provisioning call (e.g. \`{"id": "201"}\`). Not allowed with \`skipProvisionParents\`.`;
export function registerContractTestTool(server) {
    server.registerTool(TOOL_NAME, {
        description: `${getPersonaPrefix()}Before calling this tool, you MUST output a <thinking> block that covers:
1. The endpoint URL and HTTP method being tested
2. Whether the endpoint is a nested resource (URL contains a path parameter like \`{id}\`, \`{flow_id}\`, etc.) — if YES, decide: do I have the request body to provision the parent, or should I use skipProvisionParents?
3. Assertion planning — answer all four:
   a. Echo-back fields: which fields in the response are returned unchanged from the request body? (e.g. name, email, status) — these must be asserted with the exact sent value, not just not-null
   b. Computed fields: are there any numeric fields derived from other values? (e.g. total_amount, discount_amount) — these must be asserted with the formula, never a hardcoded number
   c. Test data isolation: does this test create its own resources via parentRequestData, or does it depend on pre-existing data? All IDs must come from provisioned resources, never hardcoded.
   d. Edge cases and business logic bugs: if \`skyramp_analyze_changes\` already ran and returned recommendations for this endpoint, revisit them now — did the recommendation phase flag any boundary conditions, invalid-input behaviors, or business logic bugs? Each flagged risk should map to a targeted assertion in this test. A test that doesn't target a known risk is low-value.
4. Each required parameter and what value it will take, with source (workspace config / diff / schema / user input)
NEVER use a hardcoded ID (UUID or integer) as a path parameter value. If a real resource ID is needed and cannot be provisioned, use skipProvisionParents instead.

**Dynamic context (use this before generating):**
If \`skyramp_analyze_changes\` has already run and returned a \`sessionId\`, fetch the endpoint detail before generating:
\`skyramp://analysis/{sessionId}/endpoints/{path}/{method}\`
This gives you the exact request body shape, response schema, and auth config for this endpoint. Use it to fill parameters and write accurate assertions — do not infer from source code when this resource is available.

---

Generate a contract test using Skyramp's deterministic test generation platform.

Contract tests ensure your API implementation matches its OpenAPI/Swagger specification exactly. They validate request/response schemas, status codes, headers, and data types to prevent contract violations and API breaking changes.

**IMPORTANT: If an apiSchema parameter (OpenAPI/Swagger file path or URL) is provided, DO NOT attempt to read or analyze the file contents. These files can be very large. Simply pass the path/URL to the tool - the backend will handle reading and processing the schema file.**

**IMPORTANT: If the endpoint URL contains path parameter placeholders (e.g., \`/products/{product_id}/reviews\`), pass the URL exactly as provided — do NOT substitute values for the placeholders. Leave \`pathParams\` empty unless the user has explicitly provided specific values.**

**Nested resource decision tree (follow this every time):**
Does the endpoint URL contain a path parameter (e.g. \`/flows/{id}\`, \`/work_queues/{id}/stats\`)?
- **YES, and \`apiSchema\` is provided** → use \`parentRequestData\` to supply the request body that creates the parent resource. The key must be the exact path parameter name (e.g. \`id\`, \`flow_id\`). The backend will provision the parent, extract the real ID, and inject it into the test.
- **YES, but \`apiSchema\` is NOT available** → set \`skipProvisionParents: true\` (with \`providerMode: true\`). The test will verify the error-path contract (404) rather than the success path.
- **NO path parameters** → no action needed; proceed normally.
NEVER substitute a hardcoded UUID or integer for a path parameter. A hardcoded ID will always 404 in a clean environment and produces a useless test.

${MODES_SECTION}

${CHAINING_SECTION}

**Provider setup/teardown:**
- \`skipProvisionParents\`: when true, skips generating setup/teardown functions for the provider contract test. Use this when \`apiSchema\` is unavailable and the endpoint requires a parent resource. Requires \`providerMode\`. Not allowed with \`parentRequestData\` or \`parentStatusCode\`.
`,
        inputSchema: contractTestSchema,
    }, async (params) => {
        const service = new ContractTestService();
        const result = await service.generateTest(params);
        AnalyticsService.pushTestGenerationToolEvent(TOOL_NAME, result, params).catch(() => {
            // Silently ignore analytics errors
        });
        return result;
    });
}
