// @ts-ignore
import { isRealResource, draftScenariosFromEndpoints, draftDiffDirectScenarios, inferResourceRelationships, } from "./scenarioDrafting.js";
import { WorkspaceAuthType } from "./workspaceAuth.js";
describe("isRealResource", () => {
    it("accepts normal resource names", () => {
        expect(isRealResource("users")).toBe(true);
        expect(isRealResource("products")).toBe(true);
        expect(isRealResource("orders")).toBe(true);
    });
    it("accepts hyphenated resource names (not action verbs)", () => {
        expect(isRealResource("saved-searches")).toBe(true);
        expect(isRealResource("order-items")).toBe(true);
        expect(isRealResource("user-profiles")).toBe(true);
        expect(isRealResource("api-keys")).toBe(true);
    });
    it("rejects infra/monitoring tokens but accepts semantic endpoint names", () => {
        expect(isRealResource("health")).toBe(false);
        expect(isRealResource("ping")).toBe(false);
        expect(isRealResource("dashboard")).toBe(false);
        // login, logout, verify, webhook are real resources (phase 6a)
        expect(isRealResource("login")).toBe(true);
        expect(isRealResource("logout")).toBe(true);
        expect(isRealResource("verify")).toBe(true);
        expect(isRealResource("webhook")).toBe(true);
    });
    it("rejects action-verb-hyphenated patterns", () => {
        expect(isRealResource("forgot-password")).toBe(false);
        expect(isRealResource("reset-password")).toBe(false);
        expect(isRealResource("verify-email")).toBe(false);
        expect(isRealResource("confirm-order")).toBe(false);
        expect(isRealResource("send-notification")).toBe(false);
        expect(isRealResource("create-session")).toBe(false);
        expect(isRealResource("delete-cache")).toBe(false);
    });
    it("is case-insensitive", () => {
        expect(isRealResource("Login")).toBe(true); // login is now a real resource (phase 6a)
        expect(isRealResource("VERIFY")).toBe(true); // verify is now a real resource (phase 6a)
        expect(isRealResource("Forgot-Password")).toBe(false); // ACTION_VERB_HYPHEN still filters this
    });
});
describe("draftScenariosFromEndpoints — irregular plural singularization", () => {
    it("uses 'category' (not 'categorie') in FK field names for -ies plurals", () => {
        const endpoints = [
            { path: "/api/categories", methods: [{ method: "GET" }, { method: "POST" }] },
            { path: "/api/categories/{id}", methods: [{ method: "GET" }, { method: "PUT" }, { method: "DELETE" }] },
            { path: "/api/items", methods: [{ method: "GET" }, { method: "POST" }] },
            { path: "/api/items/{id}", methods: [{ method: "GET" }, { method: "DELETE" }] },
        ];
        const scenarios = draftScenariosFromEndpoints(endpoints, [{ method: "POST", path: "/api/categories" }]);
        const allText = JSON.stringify(scenarios);
        expect(allText).not.toContain("categorie_id");
        expect(allText).not.toContain("\"categorie\"");
        // The diff-direct integration scenario description should use "category" (singular)
        const integrationScenario = scenarios.find((s) => s.scenarioName === "categories-post-lifecycle");
        expect(integrationScenario).toBeDefined();
        expect(integrationScenario.steps[0].description).toContain("category");
    });
    it("uses 'status' (not 'statu') in FK field names for -ses plurals", () => {
        const endpoints = [
            { path: "/api/statuses", methods: [{ method: "GET" }, { method: "POST" }] },
            { path: "/api/statuses/{id}", methods: [{ method: "GET" }, { method: "DELETE" }] },
        ];
        const scenarios = draftScenariosFromEndpoints(endpoints, [{ method: "POST", path: "/api/statuses" }]);
        const allText = JSON.stringify(scenarios);
        expect(allText).not.toContain("statu_id");
        // The diff-direct integration scenario description should use "status" (singular)
        const integrationScenario = scenarios.find((s) => s.scenarioName === "statuses-post-lifecycle");
        expect(integrationScenario).toBeDefined();
        expect(integrationScenario.steps[0].description).toContain("status");
    });
});
describe("draftScenariosFromEndpoints", () => {
    it("returns empty array for no endpoints", () => {
        expect(draftScenariosFromEndpoints([])).toEqual([]);
    });
});
describe("draftScenariosFromEndpoints — attack-surface expansion", () => {
    const endpoints = [
        { path: "/{id:uuid}", methods: [{ method: "DELETE", sourceFile: "src/prefect/server/api/flows.py" }] },
        { path: "/bulk_delete", methods: [{ method: "POST", sourceFile: "src/prefect/server/api/flows.py" }] },
        { path: "/{id:uuid}", methods: [{ method: "DELETE", sourceFile: "src/prefect/server/api/deployments.py" }] },
        { path: "/bulk_delete", methods: [{ method: "POST", sourceFile: "src/prefect/server/api/deployments.py" }] },
    ];
    it("drafts auth-boundary scenarios for same-file destructive bulk siblings", () => {
        const scenarios = draftScenariosFromEndpoints(endpoints, [], WorkspaceAuthType.ApiKey, {
            securityRelevantDiff: true,
            changedEndpoints: [
                { method: "DELETE", path: "/{id:uuid}", sourceFile: "src/prefect/server/api/flows.py" },
                { method: "DELETE", path: "/{id:uuid}", sourceFile: "src/prefect/server/api/deployments.py" },
            ],
        });
        const flowBulkDelete = scenarios.find(s => s.scenarioName === "flows-bulk-delete-post-auth-boundary");
        const deploymentBulkDelete = scenarios.find(s => s.scenarioName === "deployments-bulk-delete-post-auth-boundary");
        expect(flowBulkDelete).toBeDefined();
        expect(flowBulkDelete.category).toBe("security_boundary");
        expect(flowBulkDelete.testType).toBe("contract");
        expect(flowBulkDelete.steps[0]).toMatchObject({
            method: "POST",
            path: "/flows/bulk_delete",
            expectedStatusCode: 401,
        });
        expect(flowBulkDelete.description).toContain("destructive sibling");
        expect(deploymentBulkDelete).toBeDefined();
        expect(deploymentBulkDelete.steps[0]).toMatchObject({
            method: "POST",
            path: "/deployments/bulk_delete",
            expectedStatusCode: 401,
        });
    });
    it("does not expand when the diff lacks security signals", () => {
        const scenarios = draftScenariosFromEndpoints(endpoints, [], WorkspaceAuthType.ApiKey, {
            securityRelevantDiff: false,
            changedEndpoints: [{ method: "DELETE", path: "/{id:uuid}", sourceFile: "src/prefect/server/api/flows.py" }],
        });
        expect(scenarios.some(s => s.scenarioName.includes("bulk-delete"))).toBe(false);
    });
    it("uses the source-file resource for router-relative nested destructive endpoints", () => {
        const scenarios = draftScenariosFromEndpoints([
            { path: "/{id:uuid}/schedules/{schedule_id:uuid}", methods: [{ method: "DELETE", sourceFile: "src/prefect/server/api/deployments.py" }] },
            { path: "/bulk_delete", methods: [{ method: "POST", sourceFile: "src/prefect/server/api/deployments.py" }] },
            { path: "/{id:uuid}/schedules/bulk_delete", methods: [{ method: "POST", sourceFile: "src/prefect/server/api/deployments.py" }] },
        ], [], WorkspaceAuthType.ApiKey, {
            securityRelevantDiff: true,
            changedEndpoints: [
                { method: "DELETE", path: "/{id:uuid}/schedules/{schedule_id:uuid}", sourceFile: "src/prefect/server/api/deployments.py" },
            ],
        });
        expect(scenarios.some(s => s.steps.some(step => step.path === "/deployments/bulk_delete"))).toBe(true);
        expect(scenarios.some(s => s.steps.some(step => step.path === "/{id:uuid}/schedules/bulk_delete"))).toBe(false);
    });
    it("preserves Express-style tenant params for attack-surface siblings", () => {
        const scenarios = draftScenariosFromEndpoints([
            { path: "/:tenant_id/deployments/:id", methods: [{ method: "DELETE", sourceFile: "src/routes/deployments.ts" }] },
            { path: "/:tenant_id/deployments/bulk_delete", methods: [{ method: "POST", sourceFile: "src/routes/deployments.ts" }] },
        ], [], WorkspaceAuthType.ApiKey, {
            securityRelevantDiff: true,
            changedEndpoints: [
                { method: "DELETE", path: "/:tenant_id/deployments/:id", sourceFile: "src/routes/deployments.ts" },
            ],
        });
        expect(scenarios.some(s => s.steps.some(step => step.path === "/:tenant_id/deployments/bulk_delete"))).toBe(true);
    });
    it("preserves Next-style tenant params for attack-surface siblings", () => {
        const scenarios = draftScenariosFromEndpoints([
            { path: "/[tenant_id]/deployments/[id]", methods: [{ method: "DELETE", sourceFile: "src/app/api/deployments/route.ts" }] },
            { path: "/[tenant_id]/deployments/bulk_delete", methods: [{ method: "POST", sourceFile: "src/app/api/deployments/route.ts" }] },
        ], [], WorkspaceAuthType.ApiKey, {
            securityRelevantDiff: true,
            changedEndpoints: [
                { method: "DELETE", path: "/[tenant_id]/deployments/[id]", sourceFile: "src/app/api/deployments/route.ts" },
            ],
        });
        expect(scenarios.some(s => s.steps.some(step => step.path === "/[tenant_id]/deployments/bulk_delete"))).toBe(true);
    });
    it("preserves full tenant-scoped paths when the resource segment is explicit", () => {
        const scenarios = draftScenariosFromEndpoints([
            { path: "/{tenant_id}/deployments/{id}", methods: [{ method: "DELETE", sourceFile: "src/prefect/server/api/deployments.py" }] },
            { path: "/{tenant_id}/deployments/bulk_delete", methods: [{ method: "POST", sourceFile: "src/prefect/server/api/deployments.py" }] },
        ], [], WorkspaceAuthType.ApiKey, {
            securityRelevantDiff: true,
            changedEndpoints: [
                { method: "DELETE", path: "/{tenant_id}/deployments/{id}", sourceFile: "src/prefect/server/api/deployments.py" },
            ],
        });
        expect(scenarios.some(s => s.steps.some(step => step.path === "/{tenant_id}/deployments/bulk_delete"))).toBe(true);
    });
    it("does not invent a bulk sibling when no destructive sibling endpoint exists", () => {
        const scenarios = draftScenariosFromEndpoints([{ path: "/{id:uuid}", methods: [{ method: "DELETE", sourceFile: "src/prefect/server/api/flows.py" }] }], [], WorkspaceAuthType.ApiKey, {
            securityRelevantDiff: true,
            changedEndpoints: [{ method: "DELETE", path: "/{id:uuid}", sourceFile: "src/prefect/server/api/flows.py" }],
        });
        expect(scenarios).toEqual([]);
    });
    it("does not expand ordinary changed GET endpoints", () => {
        const scenarios = draftScenariosFromEndpoints(endpoints, [], WorkspaceAuthType.ApiKey, {
            securityRelevantDiff: true,
            changedEndpoints: [{ method: "GET", path: "/{id:uuid}", sourceFile: "src/prefect/server/api/flows.py" }],
        });
        expect(scenarios.some(s => s.scenarioName.includes("bulk-delete"))).toBe(false);
    });
    it("does not promote unrelated destructive routes from the same source file", () => {
        const scenarios = draftScenariosFromEndpoints([
            { path: "/flows/{id:uuid}", methods: [{ method: "DELETE", sourceFile: "src/routes/api.py" }] },
            { path: "/admin/bulk_delete", methods: [{ method: "POST", sourceFile: "src/routes/api.py" }] },
        ], [], WorkspaceAuthType.ApiKey, {
            securityRelevantDiff: true,
            changedEndpoints: [{ method: "DELETE", path: "/flows/{id:uuid}", sourceFile: "src/routes/api.py" }],
        });
        expect(scenarios).toEqual([]);
    });
});
describe("inferResourceRelationships", () => {
    it("returns empty map for endpoints with no nesting or FK fields", () => {
        const endpoints = [
            { path: "/api/users", methods: [{ method: "GET" }] },
            { path: "/api/products", methods: [{ method: "GET" }] },
        ];
        expect(inferResourceRelationships(endpoints).size).toBe(0);
    });
    it("detects parent→child relationship from path nesting (/parent/{id}/child)", () => {
        const endpoints = [
            { path: "/api/collections/{id}/links", methods: [{ method: "GET" }] },
        ];
        const rel = inferResourceRelationships(endpoints);
        expect(rel.has("links")).toBe(true);
        expect(rel.get("links").has("collections")).toBe(true);
    });
    it("detects FK relationship from request body field ({resource}_id)", () => {
        const endpoints = [
            { path: "/api/orders", methods: [{ method: "POST", interactions: [{ request: { body: { product_id: "abc" } } }] }] },
            { path: "/api/products", methods: [{ method: "GET" }] },
        ];
        const rel = inferResourceRelationships(endpoints);
        expect(rel.has("orders")).toBe(true);
        expect(rel.get("orders").has("products")).toBe(true);
    });
    it("does not create self-reference", () => {
        const endpoints = [
            { path: "/api/orders", methods: [{ method: "POST", interactions: [{ request: { body: { order_id: "abc" } } }] }] },
        ];
        const rel = inferResourceRelationships(endpoints);
        // orders should not reference itself
        expect(rel.get("orders")?.has("orders")).toBeFalsy();
    });
    it("ignores FK fields that point to unknown resources", () => {
        const endpoints = [
            { path: "/api/orders", methods: [{ method: "POST", interactions: [{ request: { body: { nonexistent_resource_id: "abc" } } }] }] },
        ];
        const rel = inferResourceRelationships(endpoints);
        // nonexistent_resource is not a known endpoint resource, should be ignored
        expect(rel.get("orders")?.size ?? 0).toBe(0);
    });
});
describe("draftDiffDirectScenarios", () => {
    const makeGroups = (defs) => new Map(Object.entries(defs).map(([k, v]) => [k, { ...v, methods: new Set(v.methods) }]));
    it("returns empty array when newEndpoints is empty", () => {
        const groups = makeGroups({ orders: { basePath: "/api/orders", methods: ["GET", "POST"], paramPath: "/api/orders/{id}" } });
        expect(draftDiffDirectScenarios([], groups)).toEqual([]);
    });
    it("generates new_endpoint-category scenarios for a new PUT endpoint", () => {
        const groups = makeGroups({
            orders: { basePath: "/api/orders", methods: ["GET", "POST", "PUT"], paramPath: "/api/orders/{order_id}" },
        });
        const scenarios = draftDiffDirectScenarios([{ method: "PUT", path: "/api/orders/{order_id}" }], groups);
        // PUT generates: mutation-recalc, contract, not-found (3 scenarios)
        // Note: we intentionally skip the redundant "happy-path" integration for PUT/PATCH
        // because mutation-recalc already provides comprehensive happy-path coverage
        expect(scenarios.length).toBeGreaterThanOrEqual(3);
        const newEndpointScenarios = scenarios.filter(s => s.category === "new_endpoint");
        expect(newEndpointScenarios.length).toBeGreaterThanOrEqual(2);
        // Mutation-recalculation scenario is drafted first for PUT/PATCH
        const mutationRecalc = scenarios.find(s => s.testType === "integration" && s.scenarioName.includes("mutation-verify"));
        expect(mutationRecalc).toBeDefined();
        expect(mutationRecalc.steps.some(st => st.method === "PUT")).toBe(true);
        expect(mutationRecalc.description).toContain("Mutation test");
        // Note: happy-path integration removed for PUT/PATCH - mutation-recalc covers this
        // Contract test
        const contract = scenarios.find(s => s.testType === "contract" && s.steps[0].expectedStatusCode === 200);
        expect(contract).toBeDefined();
        // Not-found edge case
        const notFound = scenarios.find(s => s.steps.some(st => st.expectedStatusCode === 404));
        expect(notFound).toBeDefined();
    });
    it("generates mutation-recalc scenario for a new PATCH endpoint", () => {
        const groups = makeGroups({
            orders: { basePath: "/api/orders", methods: ["GET", "POST", "PATCH"], paramPath: "/api/orders/{order_id}" },
        });
        const scenarios = draftDiffDirectScenarios([{ method: "PATCH", path: "/api/orders/{order_id}" }], groups);
        // Mutation-recalculation scenario is drafted for PATCH
        const mutationRecalc = scenarios.find(s => s.scenarioName === "orders-patch-mutation-verify");
        expect(mutationRecalc).toBeDefined();
        expect(mutationRecalc.testType).toBe("integration");
        expect(mutationRecalc.category).toBe("new_endpoint");
        expect(mutationRecalc.priority).toBe("high");
        expect(mutationRecalc.description).toContain("Mutation test");
        expect(mutationRecalc.description).toContain("derived calculations");
        // Should have 3 steps: POST (create) → PATCH (add items) → GET (verify)
        expect(mutationRecalc.steps).toHaveLength(3);
        expect(mutationRecalc.steps[0].method).toBe("POST");
        expect(mutationRecalc.steps[1].method).toBe("PATCH");
        expect(mutationRecalc.steps[2].method).toBe("GET");
        // Note: happy-path integration removed for PUT/PATCH - mutation-recalc covers this
    });
    it("integration scenario minimum steps: POST resource then PUT — LLM discovers prereqs from source code", () => {
        const groups = makeGroups({
            orders: { basePath: "/api/orders", methods: ["GET", "POST", "PUT"], paramPath: "/api/orders/{order_id}" },
        });
        const scenarios = draftDiffDirectScenarios([{ method: "PUT", path: "/api/orders/{order_id}" }], groups);
        // Mutation-recalculation scenario comes first
        const mutationRecalc = scenarios.find(s => s.testType === "integration" && s.scenarioName.includes("mutation-verify"));
        expect(mutationRecalc).toBeDefined();
        expect(mutationRecalc.steps.some(st => st.method === "POST" && st.path === "/api/orders")).toBe(true);
        expect(mutationRecalc.steps.some(st => st.method === "PUT")).toBe(true);
        // Note: happy-path integration removed for PUT/PATCH
        // mutation-recalc already provides comprehensive happy-path coverage with POST → PUT → GET
        // The minimum steps (POST + PUT) are already verified in the mutationRecalc check above
    });
    it("generates 3 scenarios for a new POST endpoint", () => {
        const groups = makeGroups({
            products: { basePath: "/api/products", methods: ["GET", "POST"], paramPath: "/api/products/{id}" },
        });
        const scenarios = draftDiffDirectScenarios([{ method: "POST", path: "/api/products" }], groups);
        expect(scenarios.length).toBe(3); // integration, contract, validation
        const types = scenarios.map(s => s.testType);
        expect(types).toContain("integration");
        expect(types).toContain("contract");
        // validation error scenario (422)
        expect(scenarios.some(s => s.steps.some(st => st.expectedStatusCode === 422))).toBe(true);
    });
    it("ignores unknown paths and skips action-verb segments", () => {
        const groups = makeGroups({
            orders: { basePath: "/api/orders", methods: ["POST", "DELETE"], paramPath: "/api/orders/{id}" },
        });
        const scenarios = draftDiffDirectScenarios([{ method: "POST", path: "/api/health" }], // 'health' is not a real resource
        groups);
        expect(scenarios).toEqual([]);
    });
    it("draftScenariosFromEndpoints passes newEndpoints through to diff-direct scenarios", () => {
        const endpoints = [
            { path: "/api/orders", methods: ["GET", "POST", "PUT"] },
            { path: "/api/orders/{order_id}", methods: ["GET", "PUT", "DELETE"] },
        ];
        const newEndpoints = [{ method: "PUT", path: "/api/orders/{order_id}" }];
        const scenarios = draftScenariosFromEndpoints(endpoints, newEndpoints);
        const diffDirect = scenarios.filter(s => s.category === "new_endpoint");
        expect(diffDirect.length).toBeGreaterThan(0);
        expect(diffDirect.some(s => s.steps.some(st => st.method === "PUT"))).toBe(true);
    });
    it("resolves router-relative param paths (e.g. /{order_id}) against known resourceGroups", () => {
        const groups = makeGroups({
            orders: { basePath: "/api/v1/orders", methods: ["GET", "POST", "PATCH", "DELETE"], paramPath: "/api/v1/orders/{order_id}" },
            products: { basePath: "/api/v1/products", methods: ["GET", "POST"], paramPath: "/api/v1/products/{id}" },
        });
        // Diff scanner reports router-relative path, not the full API path
        const scenarios = draftDiffDirectScenarios([{ method: "PATCH", path: "/{order_id}" }], groups);
        expect(scenarios.length).toBeGreaterThanOrEqual(3);
        // Mutation-recalculation scenario is drafted
        const mutationRecalc = scenarios.find(s => s.scenarioName === "orders-patch-mutation-verify");
        expect(mutationRecalc).toBeDefined();
        expect(mutationRecalc.testType).toBe("integration");
        expect(mutationRecalc.category).toBe("new_endpoint");
        expect(mutationRecalc.steps[1].method).toBe("PATCH");
        // Contract and not-found scenarios use the resolved full path
        const contract = scenarios.find(s => s.scenarioName.includes("contract"));
        expect(contract).toBeDefined();
        expect(contract.steps[0].path).toBe("/api/v1/orders/{order_id}");
        const notFound = scenarios.find(s => s.scenarioName.includes("not-found"));
        expect(notFound).toBeDefined();
        expect(notFound.steps[0].path).toBe("/api/v1/orders/{order_id}");
    });
    it("resolves router-relative base paths (e.g. /orders) against known resourceGroups", () => {
        const groups = makeGroups({
            orders: { basePath: "/api/v1/orders", methods: ["GET", "POST"], paramPath: "/api/v1/orders/{order_id}" },
        });
        const scenarios = draftDiffDirectScenarios([{ method: "POST", path: "/orders" }], groups);
        expect(scenarios.length).toBeGreaterThanOrEqual(2);
        const integration = scenarios.find(s => s.testType === "integration");
        expect(integration).toBeDefined();
        expect(integration.category).toBe("new_endpoint");
    });
    it("skips unresolvable router-relative paths", () => {
        const groups = makeGroups({
            products: { basePath: "/api/v1/products", methods: ["GET", "POST"], paramPath: "/api/v1/products/{id}" },
        });
        // /{nonexistent_id} doesn't match any known resource
        const scenarios = draftDiffDirectScenarios([{ method: "PATCH", path: "/{nonexistent_id}" }], groups);
        expect(scenarios).toEqual([]);
    });
    it("PR #195 regression: PATCH /{order_id} produces mutation-recalculate scenario in GENERATE slots", () => {
        // Simulates the exact data from PR #195:
        // - Full endpoint list with orders, products, reviews
        // - newEndpoints has PATCH /{order_id} (router-relative)
        const endpoints = [
            { path: "/api/v1/orders", methods: ["GET", "POST"] },
            { path: "/api/v1/orders/{order_id}", methods: ["GET", "PATCH", "DELETE"] },
            { path: "/api/v1/products", methods: ["GET", "POST"] },
            { path: "/api/v1/products/{product_id}", methods: ["GET", "PUT", "DELETE"] },
            { path: "/api/v1/reviews", methods: ["GET", "POST"] },
            { path: "/api/v1/reset", methods: ["POST"] },
        ];
        const newEndpoints = [{ method: "PATCH", path: "/{order_id}" }];
        const allScenarios = draftScenariosFromEndpoints(endpoints, newEndpoints);
        // The diff-direct scenarios should include orders-patch-mutation-verify
        const mutationRecalc = allScenarios.find(s => s.scenarioName === "orders-patch-mutation-verify");
        expect(mutationRecalc).toBeDefined();
        expect(mutationRecalc.category).toBe("new_endpoint"); // → CRITICAL priority tier
        expect(mutationRecalc.steps).toHaveLength(3);
        expect(mutationRecalc.steps[0].method).toBe("POST");
        expect(mutationRecalc.steps[1].method).toBe("PATCH");
        expect(mutationRecalc.steps[2].method).toBe("GET");
        expect(mutationRecalc.description).toContain("derived calculations");
        // Verify it outranks all non-new_endpoint scenarios (cascade-delete, unique-constraint, etc.)
        const newEndpointScenarios = allScenarios.filter(s => s.category === "new_endpoint");
        const otherScenarios = allScenarios.filter(s => s.category !== "new_endpoint");
        expect(newEndpointScenarios.length).toBeGreaterThanOrEqual(3);
        // All new_endpoint scenarios should include our mutation-recalculate
        expect(newEndpointScenarios.some(s => s.scenarioName === "orders-patch-mutation-verify")).toBe(true);
        // Non-new_endpoint scenarios should NOT include mutation-recalculate
        expect(otherScenarios.every(s => s.scenarioName !== "orders-patch-mutation-verify")).toBe(true);
    });
});
describe("diffDirectMutationRecalc — diverse app types (no hardcoded field names)", () => {
    const makeGroups = (defs) => new Map(Object.entries(defs).map(([k, v]) => [k, { ...v, methods: new Set(v.methods) }]));
    const testCases = [
        {
            name: "E-commerce: PATCH /orders/{order_id}",
            resource: "orders",
            group: { basePath: "/api/v1/orders", methods: ["GET", "POST", "PATCH"], paramPath: "/api/v1/orders/{order_id}" },
            method: "PATCH",
            expectedParamName: "order_id",
        },
        {
            name: "Project management: PUT /projects/{id}",
            resource: "projects",
            group: { basePath: "/api/projects", methods: ["GET", "POST", "PUT"], paramPath: "/api/projects/{id}" },
            method: "PUT",
            expectedParamName: "id",
        },
        {
            name: "Invoicing: PATCH /invoices/{invoice_id}",
            resource: "invoices",
            group: { basePath: "/v2/invoices", methods: ["GET", "POST", "PATCH"], paramPath: "/v2/invoices/{invoice_id}" },
            method: "PATCH",
            expectedParamName: "invoice_id",
        },
        {
            name: "Education LMS: PUT /courses/{course_id}",
            resource: "courses",
            group: { basePath: "/api/courses", methods: ["GET", "POST", "PUT"], paramPath: "/api/courses/{course_id}" },
            method: "PUT",
            expectedParamName: "course_id",
        },
        {
            name: "Healthcare: PATCH /prescriptions/{prescription_id}",
            resource: "prescriptions",
            group: { basePath: "/api/prescriptions", methods: ["GET", "POST", "PATCH"], paramPath: "/api/prescriptions/{prescription_id}" },
            method: "PATCH",
            expectedParamName: "prescription_id",
        },
        {
            name: "Logistics: PUT /shipments/{shipment_id}",
            resource: "shipments",
            group: { basePath: "/api/shipments", methods: ["GET", "POST", "PUT"], paramPath: "/api/shipments/{shipment_id}" },
            method: "PUT",
            expectedParamName: "shipment_id",
        },
        {
            name: "Restaurant: PATCH /menus/{menuId}",
            resource: "menus",
            group: { basePath: "/api/menus", methods: ["GET", "POST", "PATCH"], paramPath: "/api/menus/{menuId}" },
            method: "PATCH",
            expectedParamName: "menuId",
        },
        {
            name: "Cloud billing: PUT /subscriptions/{subscription_id}",
            resource: "subscriptions",
            group: { basePath: "/billing/subscriptions", methods: ["GET", "POST", "PUT"], paramPath: "/billing/subscriptions/{subscription_id}" },
            method: "PUT",
            expectedParamName: "subscription_id",
        },
        {
            name: "Warehouse: PATCH /inventories/{uuid}",
            resource: "inventories",
            group: { basePath: "/api/inventories", methods: ["GET", "POST", "PATCH"], paramPath: "/api/inventories/{uuid}" },
            method: "PATCH",
            expectedParamName: "uuid",
        },
        {
            name: "Minimal: PATCH /items (no paramPath)",
            resource: "items",
            group: { basePath: "/items", methods: ["GET", "POST", "PATCH"] },
            method: "PATCH",
            expectedParamName: "item_id",
        },
    ];
    it.each(testCases)("$name — produces valid mutation-recalc scenario", ({ resource, group, method, expectedParamName }) => {
        const groups = makeGroups({ [resource]: group });
        const scenarios = draftDiffDirectScenarios([{ method, path: group.paramPath ?? group.basePath }], groups);
        const mutationRecalc = scenarios.find(s => s.scenarioName.endsWith("-mutation-verify"));
        expect(mutationRecalc).toBeDefined();
        expect(mutationRecalc.category).toBe("new_endpoint");
        expect(mutationRecalc.testType).toBe("integration");
        expect(mutationRecalc.priority).toBe("high");
        // Step 1: POST to create resource (all test cases have POST)
        expect(mutationRecalc.steps[0].method).toBe("POST");
        expect(mutationRecalc.steps[0].path).toBe(group.basePath);
        // Step 2: PUT/PATCH with mutation
        const mutationStep = mutationRecalc.steps[1];
        expect(mutationStep.method).toBe(method);
        // Note: bodyMustInclude removed in intent-based refactor - LLM discovers fields from source
        // Verify NO hardcoded domain-specific field names
        // Verify hints are descriptive (contain "e.g." or similar guidance)
        // Chaining uses the actual path param name (only when paramPath exists)
        if (group.paramPath) {
            expect(mutationStep.chainsFrom).toBeDefined();
            const chaining = mutationStep.chainsFrom;
            expect(chaining.targetParam).toBe(expectedParamName);
            expect(chaining.sourceStep).toBe(1);
            expect(chaining.sourceField).toBe("<id-field>");
        }
        else {
            expect(mutationStep.chainsFrom).toBeUndefined();
        }
        // Step 3: GET verification (if paramPath exists)
        if (group.paramPath) {
            expect(mutationRecalc.steps).toHaveLength(3);
            const getStep = mutationRecalc.steps[2];
            expect(getStep.method).toBe("GET");
            // Note: expectedResponseFields removed in intent-based refactor
        }
        // chainingKeys uses the actual path param name, not hardcoded singular_id
        expect(mutationRecalc.chainingKeys).toContain("id");
        expect(mutationRecalc.chainingKeys).toContain(expectedParamName);
        // Description is domain-agnostic
        expect(mutationRecalc.description).toContain("Mutation test");
        expect(mutationRecalc.description).toContain("derived calculations");
    });
    it("no paramPath: PATCH step targets basePath, no GET verification, fallback param name", () => {
        const groups = makeGroups({
            items: { basePath: "/items", methods: ["GET", "POST", "PATCH"] },
        });
        const scenarios = draftDiffDirectScenarios([{ method: "PATCH", path: "/items" }], groups);
        const mutationRecalc = scenarios.find(s => s.scenarioName.endsWith("-mutation-verify"));
        expect(mutationRecalc).toBeDefined();
        // No paramPath → PATCH targets basePath, no GET step
        expect(mutationRecalc.steps).toHaveLength(2);
        expect(mutationRecalc.steps[1].path).toBe("/items");
        // Fallback param name: singular + _id
        expect(mutationRecalc.chainingKeys).toContain("item_id");
    });
    it("resource without POST: no create step, no chaining", () => {
        const groups = makeGroups({
            configs: { basePath: "/api/configs", methods: ["GET", "PATCH"], paramPath: "/api/configs/{config_id}" },
        });
        const scenarios = draftDiffDirectScenarios([{ method: "PATCH", path: "/api/configs/{config_id}" }], groups);
        const mutationRecalc = scenarios.find(s => s.scenarioName.endsWith("-mutation-verify"));
        expect(mutationRecalc).toBeDefined();
        // No POST → starts with PATCH directly, no chaining
        expect(mutationRecalc.steps[0].method).toBe("PATCH");
        expect(mutationRecalc.steps[0].chainsFrom).toBeUndefined();
        // Still has GET verification
        expect(mutationRecalc.steps[1].method).toBe("GET");
        expect(mutationRecalc.steps[1].chainsFrom).toBeUndefined();
    });
    it("generates auth-boundary scenario for each new endpoint (security_boundary, not new_endpoint)", () => {
        const groups = makeGroups({
            orders: { basePath: "/api/orders", methods: ["GET", "POST", "PUT"], paramPath: "/api/orders/{order_id}" },
        });
        const scenarios = draftDiffDirectScenarios([{ method: "PUT", path: "/api/orders/{order_id}" }], groups);
        const authBoundary = scenarios.find(s => s.scenarioName === "orders-put-auth-boundary");
        expect(authBoundary).toBeDefined();
        expect(authBoundary.category).toBe("security_boundary");
        expect(authBoundary.testType).toBe("contract");
        expect(authBoundary.steps[0].method).toBe("PUT");
        expect(authBoundary.steps[0].path).toBe("/api/orders/{order_id}");
        expect(authBoundary.steps[0].expectedStatusCode).toBe(401);
        expect(authBoundary.requiresAuth).toBe(false);
    });
    it("auth-boundary scenario is not duplicated for same method/path", () => {
        const groups = makeGroups({
            orders: { basePath: "/api/orders", methods: ["GET", "POST", "PUT", "DELETE"], paramPath: "/api/orders/{order_id}" },
        });
        const scenarios = draftDiffDirectScenarios([{ method: "PUT", path: "/api/orders/{order_id}" }, { method: "DELETE", path: "/api/orders/{order_id}" }], groups);
        const authBoundaries = scenarios.filter(s => s.scenarioName.includes("auth-boundary"));
        // One for PUT, one for DELETE (not GET, not POST)
        expect(authBoundaries).toHaveLength(2);
        expect(authBoundaries.map(s => s.scenarioName).sort()).toEqual([
            "orders-delete-auth-boundary",
            "orders-put-auth-boundary",
        ]);
    });
    it("does NOT generate auth-boundary for GET endpoints (often public)", () => {
        const groups = makeGroups({
            orders: { basePath: "/api/orders", methods: ["GET", "POST"], paramPath: "/api/orders/{order_id}" },
        });
        const scenarios = draftDiffDirectScenarios([{ method: "GET", path: "/api/orders/{order_id}" }], groups);
        const authBoundary = scenarios.find(s => s.scenarioName.includes("auth-boundary"));
        expect(authBoundary).toBeUndefined();
    });
    it("does NOT generate auth-boundary for POST", () => {
        const groups = makeGroups({
            products: { basePath: "/api/products", methods: ["GET", "POST"], paramPath: "/api/products/{id}" },
        });
        const scenarios = draftDiffDirectScenarios([{ method: "POST", path: "/api/products" }], groups);
        const authBoundary = scenarios.find(s => s.scenarioName.includes("auth-boundary"));
        expect(authBoundary).toBeUndefined();
    });
});
describe("draftDiffDirectScenarios — GET without path param", () => {
    const resourceGroups = new Map([
        ["products", {
                basePath: "/api/v1/products",
                methods: new Set(["GET", "POST"]),
                paramPath: "/api/v1/products/{product_id}",
            }],
    ]);
    it("GET no-param: produces contract + collection scenario (LLM determines if query params apply)", () => {
        const result = draftDiffDirectScenarios([{ method: "GET", path: "/api/v1/products/search" }], resourceGroups);
        const names = result.map(s => s.scenarioName);
        expect(names).toContain("products-get-new-endpoint-contract");
        expect(names).toContain("products-search-list-query");
        const scenario = result.find(s => s.scenarioName === "products-search-list-query");
        expect(scenario.category).toBe("new_endpoint");
        // Description instructs LLM to read source for query param logic
        expect(scenario.description).toMatch(/read diff source|query param/i);
    });
    it("GET no-param: produces zero cascade-delete or unique-constraint scenarios", () => {
        const result = draftDiffDirectScenarios([{ method: "GET", path: "/api/v1/products/search" }], resourceGroups);
        const names = result.map(s => s.scenarioName);
        expect(names.some(n => n.includes("cascade"))).toBe(false);
        expect(names.some(n => n.includes("unique"))).toBe(false);
        expect(names.some(n => n.includes("delete-blocked"))).toBe(false);
    });
    it("collection GET with POST: produces integration scenario (POST then GET)", () => {
        const result = draftDiffDirectScenarios([{ method: "GET", path: "/api/v1/products" }], resourceGroups);
        const scenario = result.find(s => s.scenarioName === "products-list-query");
        expect(scenario).toBeDefined();
        expect(scenario.testType).toBe("integration");
        expect(scenario.steps[0].method).toBe("POST");
        expect(scenario.steps[1].method).toBe("GET");
    });
    it("collection GET without POST: produces contract scenario (single GET step)", () => {
        const readOnlyGroups = new Map([
            ["reports", {
                    basePath: "/api/v1/reports",
                    methods: new Set(["GET"]),
                    paramPath: undefined,
                }],
        ]);
        const result = draftDiffDirectScenarios([{ method: "GET", path: "/api/v1/reports" }], readOnlyGroups);
        const scenario = result.find(s => s.scenarioName === "reports-list-query");
        expect(scenario).toBeDefined();
        expect(scenario.testType).toBe("contract");
        expect(scenario.steps).toHaveLength(1);
    });
});
// ---------------------------------------------------------------------------
// capScenarios — enforced via draftScenariosFromEndpoints
// ---------------------------------------------------------------------------
describe("draftScenariosFromEndpoints — capScenarios hard limit", () => {
    // Each PUT/PATCH new endpoint produces ~5 scenarios (mutation-verify, boundary-values,
    // contract, not-found, auth-boundary). Seven resources × 2 methods each = 70 new
    // endpoints → well above the 30-scenario cap.
    function makeEndpointsAndNewEps(resourceNames) {
        const endpoints = resourceNames.flatMap(r => [
            { path: `/api/${r}`, methods: [{ method: "GET" }, { method: "POST" }] },
            { path: `/api/${r}/{id}`, methods: [{ method: "GET" }, { method: "PUT" }, { method: "PATCH" }, { method: "DELETE" }] },
        ]);
        const newEndpoints = resourceNames.flatMap(r => [
            { method: "PUT", path: `/api/${r}/{id}` },
            { method: "PATCH", path: `/api/${r}/{id}` },
        ]);
        return { endpoints, newEndpoints };
    }
    it("caps output at 30 scenarios when many new endpoints are present", () => {
        const resources = ["users", "products", "orders", "invoices", "shipments", "reviews", "payments", "discounts"];
        const { endpoints, newEndpoints } = makeEndpointsAndNewEps(resources);
        const result = draftScenariosFromEndpoints(endpoints, newEndpoints);
        expect(result.length).toBeLessThanOrEqual(30);
    });
    it("always keeps CRITICAL (new_endpoint) scenarios under the cap", () => {
        const resources = ["users", "products", "orders", "invoices", "shipments", "reviews", "payments", "discounts"];
        const { endpoints, newEndpoints } = makeEndpointsAndNewEps(resources);
        const result = draftScenariosFromEndpoints(endpoints, newEndpoints);
        // new_endpoint is CRITICAL — verify at least some survive
        const criticalOnes = result.filter((s) => s.category === "new_endpoint");
        expect(criticalOnes.length).toBeGreaterThan(0);
    });
    it("CRITICAL items (new_endpoint) fill the cap first; security_boundary may be dropped", () => {
        // With many new endpoints, diffDirectContract/NotFound/MutationVerify/BoundaryValues all
        // produce category "new_endpoint" (CRITICAL). When CRITICAL items alone exceed 30, the hard
        // cap slices them first — breadth picks for non-CRITICAL categories are not guaranteed.
        const resources = ["users", "products", "orders", "invoices", "shipments", "reviews", "payments", "discounts"];
        const { endpoints, newEndpoints } = makeEndpointsAndNewEps(resources);
        const result = draftScenariosFromEndpoints(endpoints, newEndpoints);
        // All 30 surviving scenarios should be CRITICAL (new_endpoint)
        const nonCritical = result.filter((s) => s.category !== "new_endpoint");
        // Non-critical (security_boundary) may be 0 — this is the documented cap behavior
        expect(nonCritical.length).toBeLessThanOrEqual(result.length);
        // And the cap is enforced
        expect(result.length).toBeLessThanOrEqual(30);
    });
    it("does not cap when total scenarios are within the limit", () => {
        const endpoints = [
            { path: "/api/users", methods: [{ method: "GET" }, { method: "POST" }] },
            { path: "/api/users/{id}", methods: [{ method: "GET" }, { method: "PUT" }, { method: "DELETE" }] },
        ];
        const newEndpoints = [
            { method: "PUT", path: "/api/users/{id}" },
        ];
        const result = draftScenariosFromEndpoints(endpoints, newEndpoints);
        // A single PUT endpoint produces 5 scenarios — well within the 30-scenario cap
        expect(result.length).toBeLessThanOrEqual(30);
        expect(result.length).toBeGreaterThanOrEqual(4);
    });
});
describe("path collision fix — resourceGroups disambiguation", () => {
    it("unrelated paths sharing a last segment do not overwrite each other", () => {
        // /api/v1/orders/search and /api/v1/products/search share 'search' as last segment.
        // Before the fix, the second overwrote the first in resourceGroups.
        // The scenario must reference the orders/search path specifically, not products/search.
        const endpoints = [
            { path: "/api/v1/orders/search", methods: ["GET"] },
            { path: "/api/v1/products/search", methods: ["GET"] },
        ];
        const newEndpoints = [{ method: "GET", path: "/api/v1/orders/search" }];
        const result = draftScenariosFromEndpoints(endpoints, newEndpoints);
        expect(result.length).toBeGreaterThanOrEqual(1);
        const allPaths = result.flatMap((s) => (s.steps ?? []).map((step) => step.path ?? ""));
        // Must contain orders/search
        expect(allPaths.some((p) => p.includes("orders/search"))).toBe(true);
        // Must NOT contain products/search — wrong path from a collision would appear here
        expect(allPaths.some((p) => p.includes("products/search"))).toBe(false);
    });
    it("related paths (nested resources) still merge correctly", () => {
        // /api/v1/users and /api/v1/users/{id} are related — should still merge
        const endpoints = [
            { path: "/api/v1/users", methods: ["GET", "POST"] },
            { path: "/api/v1/users/{id}", methods: ["GET", "PUT", "DELETE"] },
        ];
        const newEndpoints = [{ method: "POST", path: "/api/v1/users" }];
        const result = draftScenariosFromEndpoints(endpoints, newEndpoints);
        expect(result.length).toBeGreaterThanOrEqual(1);
    });
    it("new endpoint under a disambiguated group is not silently dropped", () => {
        // When two paths share a last segment, both are stored under disambiguated keys
        // (e.g. "search::/api/v1/orders/search"). The fallback in draftDiffDirectScenarios
        // must use the full key for resourceGroups.get(), not the stripped "search" name
        // (which has no entry), otherwise the scenario is silently skipped.
        const endpoints = [
            { path: "/api/v1/orders/search", methods: ["GET"] },
            { path: "/api/v1/products/search", methods: ["GET"] },
            { path: "/api/v1/orders", methods: ["GET", "POST"] },
            { path: "/api/v1/products", methods: ["GET", "POST"] },
        ];
        const newEndpoints = [
            { method: "GET", path: "/api/v1/orders/search" },
            { method: "GET", path: "/api/v1/products/search" },
        ];
        const result = draftScenariosFromEndpoints(endpoints, newEndpoints);
        // Both new endpoints must produce at least one scenario each — neither should be dropped
        const allPaths = result.flatMap((s) => (s.steps ?? []).map((step) => step.path ?? ""));
        expect(allPaths.some((p) => p.includes("orders/search") || p.includes("orders"))).toBe(true);
        expect(allPaths.some((p) => p.includes("products/search") || p.includes("products"))).toBe(true);
    });
    it("paths sharing a string prefix but different resource names are not merged", () => {
        // /api/v1/orders and /api/v1/orders-archive start with the same string
        // but are different resources. startsWith alone would incorrectly merge them.
        const endpoints = [
            { path: "/api/v1/orders", methods: ["GET", "POST"] },
            { path: "/api/v1/orders-archive", methods: ["GET"] },
        ];
        const newEndpoints = [{ method: "POST", path: "/api/v1/orders" }];
        const result = draftScenariosFromEndpoints(endpoints, newEndpoints);
        expect(result.length).toBeGreaterThanOrEqual(1);
        const allPaths = result.flatMap((s) => (s.steps ?? []).map((step) => step.path ?? ""));
        // orders scenario must be present
        expect(allPaths.some((p) => p.includes("/orders"))).toBe(true);
        // orders-archive must not appear in scenarios targeting /orders
        expect(allPaths.some((p) => p.includes("orders-archive"))).toBe(false);
    });
});
describe("draftDiffDirectScenarios — execution endpoints", () => {
    it("produces execution templates for /execute-style endpoints (non-GET)", () => {
        const newEndpoints = [{ method: "POST", path: "/api/jobs/execute" }];
        const resourceGroups = new Map([
            ["jobs", { basePath: "/api/jobs", methods: new Set(["GET", "POST"]), paramPath: "/api/jobs/{id}" }],
        ]);
        const result = draftDiffDirectScenarios(newEndpoints, resourceGroups);
        const names = result.map((s) => s.scenarioName);
        expect(names).toContain("jobs-post-execute-happy");
        expect(names).toContain("jobs-post-execute-invalid");
        expect(names).toContain("jobs-post-auth-boundary");
        // Should NOT produce CRUD lifecycle templates
        expect(names.some((n) => n.includes("integration") || n.includes("contract"))).toBe(false);
    });
    it("does not produce execution templates for GET on execution endpoints", () => {
        const newEndpoints = [{ method: "GET", path: "/api/jobs/execute" }];
        const resourceGroups = new Map([
            ["jobs", { basePath: "/api/jobs", methods: new Set(["GET"]), paramPath: "/api/jobs/{id}" }],
        ]);
        const result = draftDiffDirectScenarios(newEndpoints, resourceGroups);
        const names = result.map((s) => s.scenarioName);
        // GET should fall through to normal GET handling, not execution templates
        expect(names.some((n) => n.includes("execute-happy"))).toBe(false);
        expect(names.some((n) => n.includes("execute-invalid"))).toBe(false);
    });
});
describe("draftDiffDirectScenarios — auth boundary status codes", () => {
    const newEndpoints = [{ method: "DELETE", path: "/api/orders/{id}" }];
    const resourceGroups = new Map([
        ["orders", { basePath: "/api/orders", methods: new Set(["GET", "POST", "DELETE"]), paramPath: "/api/orders/{id}" }],
    ]);
    it("expects 403 for Bearer auth type", () => {
        const result = draftDiffDirectScenarios(newEndpoints, resourceGroups, WorkspaceAuthType.Bearer);
        const authScenario = result.find((s) => s.scenarioName.includes("auth-boundary"));
        expect(authScenario).toBeDefined();
        expect(authScenario.steps[0].expectedStatusCode).toBe(403);
    });
    it("expects 403 for OAuth auth type (uses Bearer scheme)", () => {
        const result = draftDiffDirectScenarios(newEndpoints, resourceGroups, WorkspaceAuthType.OAuth);
        const authScenario = result.find((s) => s.scenarioName.includes("auth-boundary"));
        expect(authScenario).toBeDefined();
        expect(authScenario.steps[0].expectedStatusCode).toBe(403);
    });
    it("expects 401 for other auth types", () => {
        const result = draftDiffDirectScenarios(newEndpoints, resourceGroups, WorkspaceAuthType.ApiKey);
        const authScenario = result.find((s) => s.scenarioName.includes("auth-boundary"));
        expect(authScenario).toBeDefined();
        expect(authScenario.steps[0].expectedStatusCode).toBe(401);
    });
    it("expects 401 when no auth type specified", () => {
        const result = draftDiffDirectScenarios(newEndpoints, resourceGroups);
        const authScenario = result.find((s) => s.scenarioName.includes("auth-boundary"));
        expect(authScenario).toBeDefined();
        expect(authScenario.steps[0].expectedStatusCode).toBe(401);
    });
});
