import { ScenarioSource } from "../types/RepositoryAnalysis.js";
import { TestType } from "../types/TestTypes.js";
import { CATEGORY_PRIORITY } from "../types/TestRecommendation.js";
import { inferExpectedStatus } from "./httpDefaults.js";
import { WorkspaceAuthType } from "./workspaceAuth.js";
// Only tokens that are structurally non-testable (meta, infra, static assets).
// Semantic tokens like login, search, webhook, payment are removed — the LLM
// already knows how to handle them and the filter was suppressing valid surfaces.
const ACTION_PATTERN = /^(me|merge|archive|dashboard|migration|favicon|health|status|ping|metrics)$/i;
const ACTION_VERB_HYPHEN = /^(forgot-|reset-|verify-|confirm-|send-|check-|get-|set-|update-|delete-|create-|trigger-|start-|stop-)/i;
/** Matches paths whose last segment is an execution verb (e.g. /jobs/execute, /actions/run). */
const EXECUTION_SUFFIX = /\/(execute|run|trigger|process|invoke|dispatch|send|apply)$/i;
function isExecutionEndpoint(path) {
    return EXECUTION_SUFFIX.test(path);
}
/** Extract the execution verb from the path's last segment (e.g. "/jobs/run" → "run"). */
function extractExecutionVerb(endpointPath) {
    const match = EXECUTION_SUFFIX.exec(endpointPath);
    return match ? match[1].toLowerCase() : "execute";
}
/**
 * Returns true when `a` starts with `b` at a path-segment boundary.
 * Plain `startsWith` is not sufficient — "/orders-archive".startsWith("/orders") is true
 * even though they are different resources. Requiring the next character to be "/" or
 * end-of-string ensures only genuine path prefixes are matched.
 */
const isSegmentPrefix = (a, b) => a.startsWith(b) && (a[b.length] === "/" || a[b.length] === undefined);
export function isRealResource(r) {
    return !ACTION_PATTERN.test(r) && !ACTION_VERB_HYPHEN.test(r);
}
const SKIP_SEGMENTS = new Set(["api", "v1", "v2", "v3", "public"]);
/**
 * Convert a plural resource name to its singular form for use in field names
 * and step descriptions.
 *
 * Handles the most common irregular plurals found in REST API paths:
 *   -ies → -y  (categories → category, companies → company)
 *   -ses → -s  (statuses → status, classes → class)
 * Falls back to removing the trailing "s" for regular plurals.
 */
function singularize(word) {
    if (word.endsWith("ies") && word.length > 3) {
        return word.slice(0, -3) + "y";
    }
    if (word.endsWith("ses") && word.length > 4) {
        return word.slice(0, -2); // statuses → status
    }
    return word.endsWith("s") && word.length > 1 ? word.slice(0, -1) : word;
}
/**
 * Extract the primary resource name from an endpoint path.
 * E.g. "/api/v1/flow-costs/{cost_id}" → "flow-costs"
 *      "/collections/{id}/links"      → "links"
 */
function extractResourceName(path) {
    const segments = path.split("/").filter(Boolean);
    const nonParam = segments.filter(s => !s.startsWith("{") && !SKIP_SEGMENTS.has(s));
    const name = nonParam[nonParam.length - 1];
    return name && isRealResource(name) ? name : null;
}
/**
 * Infer parent→child resource relationships from two signals already present
 * in the endpoint data — no source code scanning required.
 *
 * Signal 1 — path nesting (always available):
 *   /collections/{id}/links  →  links depends on collections
 *
 * Signal 2 — request body FK fields (available when trace data is merged):
 *   POST /orders body: { product_id: "..." }  →  orders depends on products
 *
 * Returns a map of `dependent → Set<parent>`. Only confirmed, real-resource
 * pairs are included. When no signal is found the map is empty, which tells
 * the caller to fall back to heuristic pairing.
 */
export function inferResourceRelationships(endpoints) {
    const relationships = new Map();
    const addRelationship = (dependent, parent) => {
        if (dependent === parent)
            return;
        if (!isRealResource(dependent) || !isRealResource(parent))
            return;
        const deps = relationships.get(dependent) ?? new Set();
        deps.add(parent);
        relationships.set(dependent, deps);
    };
    // ── Signal 1: path nesting ──
    // Pattern: /…/parentResource/{param}/childResource
    for (const ep of endpoints) {
        const segments = ep.path.split("/").filter(Boolean);
        for (let i = 0; i + 2 < segments.length; i++) {
            const seg = segments[i];
            const paramSeg = segments[i + 1];
            const childSeg = segments[i + 2];
            if (!seg.startsWith("{") &&
                paramSeg.startsWith("{") &&
                !childSeg.startsWith("{") &&
                !SKIP_SEGMENTS.has(seg) &&
                !SKIP_SEGMENTS.has(childSeg)) {
                addRelationship(childSeg, seg);
            }
        }
    }
    // ── Signal 2: FK fields in request body interactions ──
    // Collect all known resource names first so we can validate FK targets.
    const knownResources = new Set(endpoints.map(ep => extractResourceName(ep.path)).filter(Boolean));
    // Map singular form → actual resource name to handle irregular plurals.
    // e.g. singularize("companies") = "company", so "company_id" resolves to "companies".
    const singularToResource = new Map([...knownResources].map(r => [singularize(r), r]));
    for (const ep of endpoints) {
        const resource = extractResourceName(ep.path);
        if (!resource)
            continue;
        for (const m of ep.methods) {
            if (typeof m === "string")
                continue;
            if (!["POST", "PUT", "PATCH"].includes(m.method))
                continue;
            for (const interaction of m.interactions ?? []) {
                const body = interaction.request?.body;
                if (!body || typeof body !== "object")
                    continue;
                for (const key of Object.keys(body)) {
                    if (!key.endsWith("_id"))
                        continue;
                    const depSingular = key.slice(0, -3); // "product_id" → "product"
                    const depPlural = depSingular + "s"; // naive plural, fine for regular nouns
                    // Only create relationship when the dependency actually exists as an endpoint.
                    // Check naive plural first (products), then exact singular (product),
                    // then reverse-singularize to catch irregular plurals (company → companies).
                    if (knownResources.has(depPlural)) {
                        addRelationship(resource, depPlural);
                    }
                    else if (knownResources.has(depSingular)) {
                        addRelationship(resource, depSingular);
                    }
                    else if (singularToResource.has(depSingular)) {
                        addRelationship(resource, singularToResource.get(depSingular));
                    }
                }
            }
        }
    }
    return relationships;
}
export function draftScenariosFromEndpoints(endpoints, newEndpoints = [], wsAuthType, options = {}) {
    const scenarios = [];
    const resourceGroups = new Map();
    for (const ep of endpoints) {
        const segments = ep.path.split("/").filter(Boolean);
        const nonParamSegs = segments.filter(s => !s.startsWith("{") && !SKIP_SEGMENTS.has(s));
        const resource = nonParamSegs[nonParamSegs.length - 1] || "unknown";
        const hasParam = /\{/.test(ep.path);
        const resourceSegIdx = segments.lastIndexOf(resource);
        const basePath = "/" + segments.slice(0, resourceSegIdx + 1).join("/");
        let paramPath;
        if (hasParam && resourceSegIdx + 1 < segments.length && segments[resourceSegIdx + 1].startsWith("{")) {
            paramPath = basePath + "/" + segments[resourceSegIdx + 1];
        }
        const existing = resourceGroups.get(resource);
        if (existing) {
            // Only merge if the paths are related at a segment boundary.
            const related = isSegmentPrefix(basePath, existing.basePath) || isSegmentPrefix(existing.basePath, basePath);
            if (related) {
                for (const m of ep.methods)
                    existing.methods.add(typeof m === "string" ? m : m.method);
                if (basePath.split("/").length < existing.basePath.split("/").length) {
                    existing.basePath = basePath;
                }
                if (paramPath && (!existing.paramPath || paramPath.split("/").length < existing.paramPath.split("/").length)) {
                    existing.paramPath = paramPath;
                }
            }
            else {
                // Use the raw basePath as the disambiguating suffix — replacing "/" with "_" could
                // produce collisions between e.g. /orders/search and /orders_search.
                const disambiguatedKey = `${resource}::${basePath}`;
                resourceGroups.set(disambiguatedKey, {
                    basePath,
                    methods: new Set(ep.methods.map((m) => typeof m === "string" ? m : m.method)),
                    paramPath,
                });
            }
        }
        else {
            resourceGroups.set(resource, {
                basePath,
                methods: new Set(ep.methods.map((m) => typeof m === "string" ? m : m.method)),
                paramPath,
            });
        }
    }
    scenarios.push(...draftDiffDirectScenarios(newEndpoints, resourceGroups, wsAuthType));
    scenarios.push(...draftAttackSurfaceExpansionScenarios(endpoints, options.changedEndpoints ?? [], wsAuthType, options.securityRelevantDiff ?? false));
    return capScenarios(scenarios);
}
const MAX_TOTAL_SCENARIOS = 30;
const TIER_ORDER = { CRITICAL: 4, HIGH: 3, MEDIUM: 2, LOW: 1 };
/**
 * Enforce a global cap on drafted scenarios while preserving category diversity.
 *
 * 1. CRITICAL (new_endpoint / diff-direct) scenarios prioritized first.
 * 2. One scenario per non-empty category guaranteed (breadth).
 * 3. Remaining budget filled by priority tier (HIGH > MEDIUM > LOW).
 * 4. Hard cap at MAX_TOTAL_SCENARIOS — applied to the combined output.
 */
function capScenarios(scenarios) {
    if (scenarios.length <= MAX_TOTAL_SCENARIOS)
        return scenarios;
    const critical = scenarios.filter(s => CATEGORY_PRIORITY[s.category] === "CRITICAL");
    const rest = scenarios.filter(s => CATEGORY_PRIORITY[s.category] !== "CRITICAL");
    const breadthPicks = [];
    const seenCategories = new Set();
    for (const s of rest) {
        if (!seenCategories.has(s.category)) {
            seenCategories.add(s.category);
            breadthPicks.push(s);
        }
    }
    const breadthSet = new Set(breadthPicks);
    const remaining = rest.filter(s => !breadthSet.has(s));
    remaining.sort((a, b) => {
        const ta = TIER_ORDER[CATEGORY_PRIORITY[a.category] ?? "LOW"] ?? 1;
        const tb = TIER_ORDER[CATEGORY_PRIORITY[b.category] ?? "LOW"] ?? 1;
        return tb - ta;
    });
    const budget = MAX_TOTAL_SCENARIOS - critical.length - breadthPicks.length;
    const filler = remaining.slice(0, Math.max(budget, 0));
    const combined = [...critical, ...breadthPicks, ...filler];
    // Enforce the hard cap even if critical set alone exceeds it
    return combined.slice(0, MAX_TOTAL_SCENARIOS);
}
// ── Diff-direct scenario drafting ──
// Generates targeted scenarios for each new endpoint in the branch diff.
// These get category "new_endpoint" which maps to the CRITICAL priority tier
// in the scorer, so they always fill the GENERATE slots before any structural
// scenario — aligning the plan with what the LLM naturally wants to do anyway.
/**
 * Build the minimum steps for a diff-direct integration scenario.
 * Prerequisite resources (e.g. POST /products before POST /orders) are NOT
 * computed here — the execution plan instructs the LLM to discover them from
 * source code (FK fields in request bodies) and prepend the steps itself.
 */
function diffDirectIntegration(method, resource, singular, group) {
    const steps = [];
    if (method === "POST") {
        steps.push({
            order: 1, method: "POST", path: group.basePath,
            description: `Create ${singular} and verify response`,
            interactionType: "success", expectedStatusCode: 201,
        });
        if (group.paramPath && group.methods.has("GET")) {
            steps.push({
                order: 2, method: "GET", path: group.paramPath,
                description: `Retrieve created ${singular} and verify fields`,
                interactionType: "success", expectedStatusCode: 200,
                chainsFrom: { sourceStep: 1, sourceField: "<id-field>", sourceLocation: "body", targetParam: `${singular}_id`, targetLocation: "path" },
            });
        }
    }
    else if (method === "DELETE") {
        if (group.methods.has("POST")) {
            steps.push({
                order: 1, method: "POST", path: group.basePath,
                description: `Create ${singular} to delete`,
                interactionType: "success", expectedStatusCode: 201,
            });
        }
        const targetPath = group.paramPath ?? group.basePath;
        steps.push({
            order: steps.length + 1, method: "DELETE", path: targetPath,
            description: `Delete ${singular}`,
            interactionType: "success", expectedStatusCode: 204,
            ...(steps.length > 0 ? { chainsFrom: { sourceStep: 1, sourceField: "<id-field>", sourceLocation: "body", targetParam: `${singular}_id`, targetLocation: "path" } } : {}),
        });
        if (group.paramPath && group.methods.has("GET")) {
            steps.push({
                order: steps.length + 1, method: "GET", path: group.paramPath,
                description: `Verify ${singular} is deleted (404)`,
                interactionType: "error", expectedStatusCode: 404,
            });
        }
    }
    const suffix = method === "DELETE" ? "delete" : method.toLowerCase();
    return {
        scenarioName: `${resource}-${suffix}-lifecycle`,
        description: `Lifecycle test: ${method} ${group.basePath} — verify ${method === "DELETE" ? "delete and subsequent 404" : "create and read-back"} flow`,
        category: "new_endpoint",
        priority: "high",
        steps,
        chainingKeys: ["id", `${singular}_id`],
        requiresAuth: true,
        estimatedComplexity: "moderate",
        source: ScenarioSource.CodeInferred,
        testType: TestType.INTEGRATION,
    };
}
function diffDirectContract(method, path, resource) {
    const expectedStatus = inferExpectedStatus(method);
    return {
        scenarioName: `${resource}-${method.toLowerCase()}-new-endpoint-contract`,
        description: `Contract: ${method} ${path} returns a schema-conformant response`,
        category: "new_endpoint",
        priority: "high",
        steps: [{ order: 1, method, path, description: `${method} ${path} with valid input — verify response schema`, interactionType: "success", expectedStatusCode: expectedStatus }],
        chainingKeys: [],
        requiresAuth: true,
        estimatedComplexity: "simple",
        source: ScenarioSource.CodeInferred,
        testType: TestType.CONTRACT,
    };
}
function diffDirectNotFound(method, path, resource, singular) {
    return {
        scenarioName: `${resource}-${method.toLowerCase()}-new-endpoint-not-found`,
        description: `Edge case: ${method} ${path} with a non-existent ${singular} ID returns 404`,
        category: "new_endpoint",
        priority: "high",
        steps: [{ order: 1, method, path, description: `${method} ${path} with non-existent ${singular} ID — expect 404 Not Found`, interactionType: "error", expectedStatusCode: 404 }],
        chainingKeys: [],
        requiresAuth: true,
        estimatedComplexity: "simple",
        source: ScenarioSource.CodeInferred,
        testType: TestType.CONTRACT,
    };
}
function diffDirectValidation(method, path, resource) {
    return {
        scenarioName: `${resource}-${method.toLowerCase()}-new-endpoint-validation`,
        description: `Validation: ${method} ${path} with missing required fields returns 422`,
        category: "new_endpoint",
        priority: "high",
        steps: [{ order: 1, method, path, description: `${method} ${path} with empty/missing required fields — expect 422`, interactionType: "error", expectedStatusCode: 422 }],
        chainingKeys: [],
        requiresAuth: true,
        estimatedComplexity: "simple",
        source: ScenarioSource.CodeInferred,
        testType: TestType.CONTRACT,
    };
}
/**
 * Draft a "mutation with collection modification" scenario for PUT/PATCH endpoints.
 * This tests adding/removing child items (e.g., order line items) and verifying that
 * derived totals (total_amount, item_count, subtotal) are recalculated.
 * This pattern catches the most common class of user-reported bugs.
 */
function diffDirectBoundaryValues(method, resource, singular, group) {
    const steps = [];
    const targetPath = group.paramPath ?? group.basePath;
    const pathParamName = group.paramPath?.match(/\{([^}]+)\}/)?.[1] ?? `${singular}_id`;
    if (group.methods.has("POST")) {
        steps.push({
            order: 1,
            method: "POST",
            path: group.basePath,
            description: `Create a ${singular} for boundary testing`,
            interactionType: "success",
            expectedStatusCode: 201,
        });
    }
    steps.push({
        order: steps.length + 1,
        method,
        path: targetPath,
        description: `${method} with valid boundary values (e.g. 0, maximum allowed, empty collection) — expect 200`,
        interactionType: "success",
        expectedStatusCode: 200,
        ...(steps.length > 0 && group.paramPath
            ? { chainsFrom: { sourceStep: 1, sourceField: "<id-field>", sourceLocation: "body", targetParam: pathParamName, targetLocation: "path" } }
            : {}),
    });
    return {
        scenarioName: `${resource}-${method.toLowerCase()}-boundary-values`,
        description: `Boundary test: ${method} ${targetPath} — test valid boundary values (0%, 100%, minimum, maximum allowed by schema). Read source to identify numeric/percentage fields and their constraints; assert the response reflects the boundary value correctly.`,
        category: "new_endpoint",
        priority: "high",
        steps,
        chainingKeys: ["id", pathParamName],
        requiresAuth: true,
        estimatedComplexity: "moderate",
        source: ScenarioSource.CodeInferred,
        testType: TestType.INTEGRATION,
    };
}
function diffDirectMutationRecalc(method, resource, singular, group) {
    const steps = [];
    if (group.methods.has("POST")) {
        steps.push({
            order: 1,
            method: "POST",
            path: group.basePath,
            description: `Create a ${singular} for testing`,
            interactionType: "success",
            expectedStatusCode: 201,
        });
    }
    const targetPath = group.paramPath ?? group.basePath;
    const pathParamName = group.paramPath?.match(/\{([^}]+)\}/)?.[1] ?? `${singular}_id`;
    const sourceStep = steps.length;
    steps.push({
        order: steps.length + 1,
        method,
        path: targetPath,
        description: `${method} the ${singular} with valid changes — read source to find mutable fields`,
        interactionType: "success",
        expectedStatusCode: 200,
        ...(sourceStep > 0 && group.paramPath
            ? { chainsFrom: { sourceStep, sourceField: "<id-field>", sourceLocation: "body", targetParam: pathParamName, targetLocation: "path" } }
            : {}),
    });
    if (group.paramPath && group.methods.has("GET")) {
        steps.push({
            order: steps.length + 1,
            method: "GET",
            path: group.paramPath,
            description: `Verify the ${singular} reflects changes — check any derived/calculated fields`,
            interactionType: "success",
            expectedStatusCode: 200,
            ...(sourceStep > 0 && group.paramPath
                ? { chainsFrom: { sourceStep, sourceField: "<id-field>", sourceLocation: "body", targetParam: pathParamName, targetLocation: "path" } }
                : {}),
        });
    }
    return {
        scenarioName: `${resource}-${method.toLowerCase()}-mutation-verify`,
        description: `Mutation test: ${method} ${targetPath} — update fields and verify derived calculations (totals, discounts, sums, quantities) are recomputed correctly. Test at least one calculation-affecting change (e.g. discount percentage, quantity update, item addition/removal).`,
        category: "new_endpoint",
        priority: "high",
        steps,
        chainingKeys: ["id", pathParamName],
        requiresAuth: true,
        estimatedComplexity: "complex",
        source: ScenarioSource.CodeInferred,
        testType: TestType.INTEGRATION,
    };
}
function diffDirectExecutionHappy(method, path, resource) {
    const verb = extractExecutionVerb(path);
    return {
        scenarioName: `${resource}-${method.toLowerCase()}-${verb}-happy`,
        description: `Execution test: ${method} ${path} with valid payload — read source to identify required input fields; assert response reflects expected output (result, status, processed data)`,
        category: "new_endpoint",
        priority: "high",
        steps: [{
                order: 1,
                method,
                path,
                description: `${method} ${path} with valid input payload — expect 200 OK with execution result`,
                interactionType: "success",
                expectedStatusCode: 200,
            }],
        chainingKeys: [],
        requiresAuth: true,
        estimatedComplexity: "moderate",
        source: ScenarioSource.CodeInferred,
        testType: TestType.INTEGRATION,
    };
}
function diffDirectExecutionInvalid(method, path, resource) {
    const verb = extractExecutionVerb(path);
    return {
        scenarioName: `${resource}-${method.toLowerCase()}-${verb}-invalid`,
        description: `Validation test: ${method} ${path} with missing or invalid request body — read source to find required fields; assert 422 Unprocessable Entity with meaningful error`,
        category: "new_endpoint",
        priority: "high",
        steps: [{
                order: 1,
                method,
                path,
                description: `${method} ${path} with missing required fields — expect 422 Unprocessable Entity`,
                interactionType: "error",
                expectedStatusCode: 422,
            }],
        chainingKeys: [],
        requiresAuth: true,
        estimatedComplexity: "simple",
        source: ScenarioSource.CodeInferred,
        testType: TestType.CONTRACT,
    };
}
function diffDirectAuthBoundary(method, path, resource, wsAuthType) {
    // Bearer and OAuth both use the "Authorization: Bearer <token>" scheme. These setups
    // commonly return 403 (Forbidden) rather than 401 (Unauthorized) when a token is
    // absent, because the framework treats a missing credential as insufficient permission
    // rather than unauthenticated. All other auth types use 401.
    const expectedStatusCode = wsAuthType === WorkspaceAuthType.Bearer || wsAuthType === WorkspaceAuthType.OAuth ? 403 : 401;
    const statusLabel = expectedStatusCode === 403 ? "403 Forbidden" : "401 Unauthorized";
    return {
        scenarioName: `${resource}-${method.toLowerCase()}-auth-boundary`,
        description: `Auth boundary: ${method} ${path} without authentication returns ${statusLabel}`,
        category: "security_boundary",
        priority: "high",
        steps: [{ order: 1, method, path, description: `${method} ${path} without Authorization header — expect ${statusLabel}`, interactionType: "error", expectedStatusCode }],
        chainingKeys: [],
        requiresAuth: false,
        estimatedComplexity: "simple",
        source: ScenarioSource.CodeInferred,
        testType: TestType.CONTRACT,
    };
}
const DESTRUCTIVE_ACTION_TOKENS = new Set(["delete", "destroy", "remove", "cancel", "archive"]);
const SOURCE_RESOURCE_SKIP_SEGMENTS = new Set([
    ...SKIP_SEGMENTS,
    "app", "apps", "handler", "handlers", "index", "page", "pages", "route", "routes", "router", "server", "src",
]);
function pathSegments(path) {
    return path.split("/").filter(Boolean);
}
function isPathParam(segment) {
    return /^\{[^}]+\}$/.test(segment) ||
        /^:[A-Za-z_][A-Za-z0-9_]*$/.test(segment) ||
        /^\[\.\.\.[^\]]+\]$/.test(segment) ||
        /^\[\[\.\.\.[^\]]+\]\]$/.test(segment) ||
        /^\[[^\].][^\]]*\]$/.test(segment);
}
function actionSegmentTokens(segment) {
    return segment
        .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
        .split(/[^a-z0-9]+/i)
        .filter(Boolean)
        .map(token => token.toLowerCase());
}
function isDestructiveActionSegment(segment) {
    return actionSegmentTokens(segment).some(token => DESTRUCTIVE_ACTION_TOKENS.has(token));
}
function sourceFileResource(sourceFile) {
    if (!sourceFile)
        return undefined;
    const sourceSegments = sourceFile.split(/[\\/]/).filter(Boolean);
    const filename = sourceSegments.at(-1)?.replace(/\.[^.]+$/, "").toLowerCase();
    if (filename && isRealResource(filename) && !SOURCE_RESOURCE_SKIP_SEGMENTS.has(filename))
        return filename;
    return [...sourceSegments]
        .slice(0, -1)
        .reverse()
        .map(segment => segment.replace(/\.[^.]+$/, "").toLowerCase())
        .find(segment => segment && isRealResource(segment) && !SOURCE_RESOURCE_SKIP_SEGMENTS.has(segment));
}
function collectionPathForChangedEndpoint(path, sourceFile) {
    const segments = pathSegments(path);
    const sourceResource = sourceFileResource(sourceFile);
    if (segments.length > 0 &&
        isPathParam(segments[0]) &&
        sourceResource &&
        (segments.length === 1 || segments[1] !== sourceResource)) {
        return `/${sourceResource}`;
    }
    if (segments.length >= 2) {
        const last = segments[segments.length - 1];
        if (isPathParam(last)) {
            return `/${segments.slice(0, -1).join("/")}`;
        }
        const paramIdx = segments.findIndex(isPathParam);
        if (paramIdx > 0) {
            return `/${segments.slice(0, paramIdx).join("/")}`;
        }
    }
    return sourceResource ? `/${sourceResource}` : undefined;
}
function resourceFromCollectionPath(collectionPath) {
    const segments = pathSegments(collectionPath);
    return segments.filter(s => !SKIP_SEGMENTS.has(s)).at(-1) ?? "resource";
}
function actionNameFromPath(path) {
    const action = pathSegments(path).at(-1) ?? "destructive";
    return action.replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "").toLowerCase() || "destructive";
}
function endpointMethods(endpoint) {
    return endpoint.methods.map(m => (typeof m === "string" ? m : m.method).toUpperCase());
}
function endpointSourceFiles(endpoint) {
    return new Set(endpoint.methods
        .filter((m) => typeof m !== "string")
        .map(m => m.sourceFile)
        .filter((sourceFile) => Boolean(sourceFile)));
}
function resolveSiblingPath(endpointPath, collectionPath) {
    if (isSegmentPrefix(endpointPath, collectionPath))
        return endpointPath;
    const segments = pathSegments(endpointPath);
    if (segments.length > 0 && isDestructiveActionSegment(segments[0])) {
        return `${collectionPath}/${segments.join("/")}`;
    }
    return endpointPath;
}
function isDestructiveSiblingEndpoint(endpoint, changed, collectionPath) {
    const methods = endpointMethods(endpoint);
    if (!methods.includes("POST"))
        return false;
    const endpointSegments = pathSegments(endpoint.path);
    const samePathFamily = isSegmentPrefix(endpoint.path, collectionPath) && endpoint.path !== collectionPath;
    const routerRelativeDestructive = endpointSegments.length > 0 &&
        isDestructiveActionSegment(endpointSegments[0]);
    const sameSourceFile = Boolean(routerRelativeDestructive && changed.sourceFile && endpointSourceFiles(endpoint).has(changed.sourceFile));
    if (!samePathFamily && !sameSourceFile)
        return false;
    const collectionSegments = samePathFamily ? pathSegments(collectionPath) : [];
    const suffix = endpointSegments.slice(collectionSegments.length);
    return suffix.some(segment => isDestructiveActionSegment(segment));
}
function draftAttackSurfaceExpansionScenarios(endpoints, changedEndpoints, wsAuthType, securityRelevantDiff) {
    if (!securityRelevantDiff || changedEndpoints.length === 0)
        return [];
    const scenarios = [];
    const seen = new Set();
    for (const changed of changedEndpoints) {
        if (changed.method.toUpperCase() !== "DELETE")
            continue;
        const collectionPath = collectionPathForChangedEndpoint(changed.path, changed.sourceFile);
        if (!collectionPath)
            continue;
        const resource = resourceFromCollectionPath(collectionPath);
        for (const endpoint of endpoints) {
            if (!isDestructiveSiblingEndpoint(endpoint, changed, collectionPath))
                continue;
            const siblingPath = resolveSiblingPath(endpoint.path, collectionPath);
            const scenario = diffDirectAuthBoundary("POST", siblingPath, `${resource}-${actionNameFromPath(siblingPath)}`, wsAuthType);
            scenario.description = `Attack-surface auth boundary: ${siblingPath} is a destructive sibling of changed ${changed.method.toUpperCase()} ${changed.path}; verify it rejects missing authentication`;
            scenario.isAttackSurfaceSecurityBoundary = true;
            scenario.steps[0].description = `POST ${siblingPath} without the new auth/admin guard — expect ${scenario.steps[0].expectedStatusCode}`;
            if (!seen.has(scenario.scenarioName)) {
                seen.add(scenario.scenarioName);
                scenarios.push(scenario);
            }
        }
    }
    return scenarios;
}
/**
 * Draft a unified scenario for GET endpoints without path params (collection or search).
 * Rather than hardcoding keyword detection, the description instructs the LLM to read
 * the diff source and determine whether query params are involved, then draft variations
 * accordingly.
 */
function diffDirectGetCollection(path, resource, singular, group) {
    const steps = [];
    if (group.methods.has("POST")) {
        steps.push({
            order: 1,
            method: "POST",
            path: group.basePath,
            description: `Create a ${singular} for query verification`,
            interactionType: "success",
            expectedStatusCode: 201,
        });
        steps.push({
            order: 2,
            method: "GET",
            path,
            description: `Query ${resource} and verify results include the created item`,
            interactionType: "success",
            expectedStatusCode: 200,
        });
    }
    else {
        steps.push({
            order: 1,
            method: "GET",
            path,
            description: `Query ${resource} and verify response structure`,
            interactionType: "success",
            expectedStatusCode: 200,
        });
    }
    // Include a path-specific suffix in scenarioName to avoid collision when multiple GET
    // collection endpoints exist for the same resource (e.g. /products and /products/featured).
    // Strip API prefix segments (api, v1, ...) and param segments, take the last 1-2 meaningful ones.
    const meaningfulSegs = path.split("/").filter(s => s && !s.startsWith("{") && !SKIP_SEGMENTS.has(s));
    const pathSlug = meaningfulSegs.slice(-2).join("-") || resource;
    const uniqueName = pathSlug === resource ? `${resource}-list-query` : `${pathSlug}-list-query`;
    return {
        scenarioName: uniqueName,
        description: `List/query test: GET ${path} — read source to find supported query params (filters, pagination, search), then test with and without them`,
        category: "new_endpoint",
        priority: "high",
        steps,
        chainingKeys: ["id", `${singular}_id`],
        requiresAuth: true,
        estimatedComplexity: group.methods.has("POST") ? "moderate" : "simple",
        source: ScenarioSource.CodeInferred,
        testType: group.methods.has("POST") ? TestType.INTEGRATION : TestType.CONTRACT,
    };
}
/**
 * Draft scenarios that directly test each new endpoint in the branch diff.
 *
 * For each new endpoint, method-specific scenarios are produced:
 *   PUT/PATCH  → mutation-recalc integration, boundary-values integration, contract, not-found
 *   POST       → lifecycle integration (create + optional GET), contract, validation error
 *   DELETE     → lifecycle integration (create + delete + 404), contract
 *   GET /{id}  → contract, not-found
 *   GET /coll  → contract + collection-list integration (create-then-GET) or contract-only
 *
 * Additionally, PUT/PATCH/DELETE methods get an auth-boundary scenario
 * (category "security_boundary" → HIGH priority, not CRITICAL) so it
 * lands in ADDITIONAL deterministically instead of depending on LLM
 * supplement. GET is excluded (often public); POST is excluded because
 * the security-boundary supplement tier covers it. Exception: execution
 * endpoints (/execute, /run, /trigger, etc.) include auth-boundary for
 * all non-GET methods (including POST) since they are high-value targets.
 */
export function draftDiffDirectScenarios(newEndpoints, resourceGroups, wsAuthType) {
    if (newEndpoints.length === 0)
        return [];
    const scenarios = [];
    const seen = new Set();
    // Group flat list by path
    const grouped = new Map();
    for (const ep of newEndpoints) {
        const methods = grouped.get(ep.path) ?? new Set();
        methods.add(ep.method.toUpperCase());
        grouped.set(ep.path, methods);
    }
    // Segment-boundary suffix match: avoids "/orders" matching "/preorders".
    const pathSuffixMatches = (fullPath, suffix) => {
        if (fullPath === suffix)
            return true;
        const fullSegs = fullPath.split("/").filter(Boolean);
        const suffixSegs = suffix.split("/").filter(Boolean);
        if (suffixSegs.length === 0 || suffixSegs.length > fullSegs.length)
            return false;
        const offset = fullSegs.length - suffixSegs.length;
        for (let i = 0; i < suffixSegs.length; i++) {
            if (fullSegs[offset + i] !== suffixSegs[i])
                return false;
        }
        return true;
    };
    for (const [epPath, methods] of grouped) {
        let resource = extractResourceName(epPath);
        let resolvedPath = epPath;
        // The map key to use for the final resourceGroups lookup — may differ from `resource`
        // when the entry was stored under a disambiguated "resource::basePath" key.
        let resourceGroupKey = resource;
        // When resource was found but epPath may be router-relative (e.g. "/orders"
        // instead of "/api/v1/orders"), try to upgrade resolvedPath to the full path.
        if (resource) {
            const existingGroup = resourceGroups.get(resource);
            if (existingGroup) {
                if (existingGroup.paramPath && existingGroup.paramPath !== epPath && pathSuffixMatches(existingGroup.paramPath, epPath)) {
                    resolvedPath = existingGroup.paramPath;
                }
                else if (existingGroup.basePath !== epPath && pathSuffixMatches(existingGroup.basePath, epPath)) {
                    resolvedPath = existingGroup.basePath;
                }
            }
        }
        // When extractResourceName returns null OR the resource isn't in resourceGroups,
        // fall back to finding a matching group via prefix or suffix matching.
        if (!resource || !resourceGroups.has(resource)) {
            let bestMatch = null;
            for (const [rName, rGroup] of resourceGroups) {
                // Prefix match: epPath starts with group's basePath (e.g. /products/search → /products)
                if (epPath.startsWith(rGroup.basePath + "/") || epPath === rGroup.basePath) {
                    if (!bestMatch || rGroup.basePath.length > bestMatch.length) {
                        bestMatch = { name: rName, path: rGroup.basePath, length: rGroup.basePath.length, isPrefixMatch: true };
                    }
                }
                // Suffix match for router-relative paths (e.g. /{order_id} → /api/v1/orders/{order_id})
                if (rGroup.paramPath && pathSuffixMatches(rGroup.paramPath, epPath)) {
                    if (!bestMatch || rGroup.paramPath.length > bestMatch.length) {
                        bestMatch = { name: rName, path: rGroup.paramPath, length: rGroup.paramPath.length, isPrefixMatch: false };
                    }
                }
            }
            if (!bestMatch)
                continue;
            // Keep the full map key for lookup — disambiguated keys use "resource::basePath" format.
            // Use only the prefix before "::" as the resource name for scenario naming/singularization.
            resourceGroupKey = bestMatch.name;
            resource = resourceGroupKey.includes("::") ? resourceGroupKey.split("::")[0] : resourceGroupKey;
            // For prefix matches (action sub-paths), keep original path; for suffix matches, use resolved path
            resolvedPath = bestMatch.isPrefixMatch ? epPath : bestMatch.path;
        }
        const group = resourceGroups.get(resourceGroupKey);
        if (!group)
            continue;
        const singular = singularize(resource);
        const hasPathParam = resolvedPath.includes("{");
        const add = (s) => {
            if (!seen.has(s.scenarioName)) {
                seen.add(s.scenarioName);
                scenarios.push(s);
            }
        };
        for (const method of methods) {
            // Execution endpoints get their own focused templates instead of CRUD lifecycle ones.
            // Caller-traced execution endpoints (Bug 5 scenario) are handled by the LLM prompt;
            // this fires only when a new /execute-style endpoint appears directly in the diff.
            // GET is excluded — execution endpoints use request bodies, which don't apply to GET.
            // MULTI is excluded — it's a sentinel for catch-all handlers, not a real HTTP verb.
            if (isExecutionEndpoint(resolvedPath) && method !== "GET" && method !== "MULTI") {
                add(diffDirectExecutionHappy(method, resolvedPath, resource));
                add(diffDirectExecutionInvalid(method, resolvedPath, resource));
                add(diffDirectAuthBoundary(method, resolvedPath, resource, wsAuthType));
                continue;
            }
            if (method === "PUT" || method === "PATCH") {
                // mutation-recalc tests the primary calculation flow
                add(diffDirectMutationRecalc(method, resource, singular, group));
                // boundary-values tests edge cases (0, max, empty)
                add(diffDirectBoundaryValues(method, resource, singular, group));
                add(diffDirectContract(method, resolvedPath, resource));
                if (hasPathParam)
                    add(diffDirectNotFound(method, resolvedPath, resource, singular));
            }
            else if (method === "POST") {
                add(diffDirectIntegration(method, resource, singular, group));
                add(diffDirectContract(method, resolvedPath, resource));
                add(diffDirectValidation(method, resolvedPath, resource));
            }
            else if (method === "DELETE") {
                add(diffDirectIntegration(method, resource, singular, group));
                add(diffDirectContract(method, resolvedPath, resource));
            }
            else if (method === "GET" && hasPathParam) {
                add(diffDirectContract(method, resolvedPath, resource));
                add(diffDirectNotFound(method, resolvedPath, resource, singular));
            }
            else if (method === "GET" && !hasPathParam) {
                add(diffDirectContract(method, resolvedPath, resource));
                add(diffDirectGetCollection(resolvedPath, resource, singular, group));
            }
            if (method === "PUT" || method === "PATCH" || method === "DELETE")
                add(diffDirectAuthBoundary(method, resolvedPath, resource, wsAuthType));
        }
    }
    return scenarios;
}
