jest.mock("../../services/ScenarioGenerationService.js", () => ({
    ScenarioGenerationService: jest.fn().mockImplementation(() => ({
        generateTraceRequestFromInput: jest.fn().mockReturnValue(null),
    })),
}));
jest.mock("../../services/AnalyticsService.js", () => ({ AnalyticsService: { pushMCPToolEvent: jest.fn().mockResolvedValue(undefined) } }));
jest.mock("../../utils/workspaceAuth.js", () => ({
    getWorkspaceAuthConfig: jest.fn().mockResolvedValue({ authHeader: undefined, authType: "none" }),
    WorkspaceAuthType: { None: "none" },
    getDefaultAuthHeader: jest.fn().mockReturnValue(""),
    isAuthorizationHeaderName: jest.fn().mockReturnValue(false),
    getAuthScheme: jest.fn().mockReturnValue(""),
    readWorkspaceConfigRaw: jest.fn().mockResolvedValue(null),
}));
jest.mock("../../utils/logger.js", () => ({ logger: { info: jest.fn(), warning: jest.fn(), error: jest.fn() } }));
jest.mock("../../prompts/personas.js", () => ({ getPersonaPrefix: () => "" }));
import fs from "fs";
import { stepSchema, registerBatchScenarioTestTool } from "./generateBatchScenarioRestTool.js";
/** Build a minimal mock McpServer, register the tool, and return the captured handler. */
function captureHandler() {
    let capturedHandler;
    const mockServer = {
        registerTool: jest.fn((_name, _meta, handler) => {
            capturedHandler = handler;
        }),
    };
    registerBatchScenarioTestTool(mockServer);
    return capturedHandler;
}
// ─────────────────────────────────────────────────────────────────────────────
// stepSchema — requestBody validation
// ─────────────────────────────────────────────────────────────────────────────
describe("stepSchema — requestBody validation", () => {
    it("accepts a POST step with a non-empty requestBody", () => {
        const result = stepSchema.safeParse({
            method: "POST",
            path: "/api/v1/products",
            requestBody: JSON.stringify({ name: "Widget-123", price: 9.99 }),
        });
        expect(result.success).toBe(true);
    });
    it("rejects a POST step with an empty requestBody {}", () => {
        const result = stepSchema.safeParse({
            method: "POST",
            path: "/api/v1/products",
            requestBody: "{}",
        });
        expect(result.success).toBe(false);
        const issue = result.error?.issues.find(i => i.path.includes("requestBody"));
        expect(issue?.message).toContain("empty object {}");
        expect(issue?.message).toContain("POST");
    });
    it("rejects a PATCH step with an empty requestBody {}", () => {
        const result = stepSchema.safeParse({
            method: "PATCH",
            path: "/api/v1/orders/1",
            requestBody: "{}",
        });
        expect(result.success).toBe(false);
        expect(result.error?.issues.some(i => i.message.includes("empty object {}"))).toBe(true);
    });
    it("rejects a PUT step with an empty requestBody {}", () => {
        const result = stepSchema.safeParse({
            method: "PUT",
            path: "/api/v1/products/1",
            requestBody: "{}",
        });
        expect(result.success).toBe(false);
        const issue = result.error?.issues.find(i => i.path.includes("requestBody"));
        expect(issue?.message).toContain("empty object {}");
        expect(issue?.message).toContain("PUT");
    });
    it("accepts a POST step with requestBody omitted entirely", () => {
        const result = stepSchema.safeParse({
            method: "POST",
            path: "/api/v1/products",
        });
        expect(result.success).toBe(true);
    });
    it("does not throw on requestBody: 'null' (valid JSON null)", () => {
        const result = stepSchema.safeParse({
            method: "POST",
            path: "/api/v1/products",
            requestBody: "null",
        });
        expect(result.success).toBe(true);
    });
    it("accepts a GET step with no requestBody", () => {
        const result = stepSchema.safeParse({
            method: "GET",
            path: "/api/v1/products/1",
        });
        expect(result.success).toBe(true);
    });
    it("accepts a DELETE step with no requestBody", () => {
        const result = stepSchema.safeParse({
            method: "DELETE",
            path: "/api/v1/products/1",
        });
        expect(result.success).toBe(true);
    });
    it("does not reject a GET step that happens to have an empty requestBody", () => {
        const result = stepSchema.safeParse({
            method: "GET",
            path: "/api/v1/products",
            requestBody: "{}",
        });
        expect(result.success).toBe(true);
    });
});
// ─────────────────────────────────────────────────────────────────────────────
// GraphQL step rejection (Change 10b)
// ─────────────────────────────────────────────────────────────────────────────
describe("generateBatchScenarioRestTool — GraphQL step rejection (Change 10b)", () => {
    let handler;
    const baseParams = {
        scenarioName: "test_scenario",
        destination: "localhost",
        outputDir: "/tmp/skyramp-test",
    };
    beforeEach(() => {
        jest.clearAllMocks();
        handler = captureHandler();
    });
    it("rejects a step list containing /graphql", async () => {
        const result = await handler({
            ...baseParams,
            steps: [{ method: "POST", path: "/graphql", requestBody: '{"query":"{ users { id } }"}' }],
        });
        expect(result.isError).toBe(true);
        expect(result.content[0].text).toContain("GraphQL endpoints are not supported");
        expect(result.content[0].text).toContain("POST /graphql");
    });
    it("rejects a step list containing /api/graphql (nested segment)", async () => {
        const result = await handler({
            ...baseParams,
            steps: [
                { method: "GET", path: "/api/v1/users" },
                { method: "POST", path: "/api/graphql", requestBody: '{"query":"{ id }"}' },
            ],
        });
        expect(result.isError).toBe(true);
        expect(result.content[0].text).toContain("GraphQL endpoints are not supported");
        expect(result.content[0].text).toContain("POST /api/graphql");
    });
    it("does not reject a path whose segment is 'graphql-config' (not an exact segment match)", async () => {
        // 'graphql-config' !== 'graphql' — the guard uses exact segment comparison.
        // The GraphQL rejection message must never appear, regardless of whether the
        // handler succeeds or fails for some other reason.
        const result = await handler({
            ...baseParams,
            steps: [{ method: "GET", path: "/api/v1/graphql-config" }],
        });
        expect(result.content[0].text).not.toContain("GraphQL endpoints are not supported");
    });
    it("does not throw on a non-string path (typeof guard)", async () => {
        // typeof guard returns false for non-string — no crash from .replace()
        const result = await handler({
            ...baseParams,
            steps: [{ method: "GET", path: undefined }],
        });
        expect(result).toBeDefined();
        if (result.isError) {
            expect(result.content[0].text).not.toContain("Cannot read");
        }
    });
});
// ─────────────────────────────────────────────────────────────────────────────
// Spec path validation (Change 3)
// ─────────────────────────────────────────────────────────────────────────────
describe("generateBatchScenarioRestTool — spec path validation (Change 3)", () => {
    let handler;
    let readFileSyncSpy;
    const fakeSpec = JSON.stringify({
        openapi: "3.0.0",
        paths: {
            "/api/v1/users": {},
            "/api/v1/users/{id}": {},
        },
    });
    const baseParams = {
        scenarioName: "test_scenario",
        destination: "localhost",
        outputDir: "/tmp/skyramp-test",
        apiSchema: "/tmp/openapi.json",
    };
    let writeFileSyncSpy;
    beforeEach(() => {
        jest.clearAllMocks();
        handler = captureHandler();
        readFileSyncSpy = jest.spyOn(fs, "readFileSync").mockReturnValue(fakeSpec);
        writeFileSyncSpy = jest.spyOn(fs, "writeFileSync").mockImplementation(() => { });
        // Return a valid trace so tests reach the success path (where the warning is appended)
        const ScenarioSvc = require("../../services/ScenarioGenerationService.js").ScenarioGenerationService;
        ScenarioSvc.mockImplementation(() => ({
            generateTraceRequestFromInput: jest.fn().mockReturnValue({ method: "GET", path: "/mock" }),
        }));
    });
    afterEach(() => {
        readFileSyncSpy.mockRestore();
        writeFileSyncSpy.mockRestore();
    });
    it("warns (does not hard-reject) when step paths are not in the OpenAPI spec", async () => {
        // Spec may lag code — missing path != phantom path, so we warn and proceed.
        const result = await handler({
            ...baseParams,
            steps: [{ method: "GET", path: "/api/v1/phantom-endpoint" }],
        });
        expect(result.isError).toBeFalsy();
        expect(result.content[0].text).toContain("Spec warning");
        expect(result.content[0].text).toContain("/api/v1/phantom-endpoint");
        expect(result.content[0].text).toContain("Known spec paths");
    });
    it("includes no spec warning when step path is present in the OpenAPI spec", async () => {
        const result = await handler({
            ...baseParams,
            steps: [{ method: "GET", path: "/api/v1/users" }],
        });
        expect(result.content[0].text).not.toContain("Spec warning");
    });
    it("does not warn for Express-style :param paths that normalise to a spec path", async () => {
        const result = await handler({
            ...baseParams,
            steps: [{ method: "GET", path: "/api/v1/users/:id" }],
        });
        expect(result.content[0].text).not.toContain("Spec warning");
    });
    it("does not throw on a non-string path (typeof guard)", async () => {
        const result = await handler({
            ...baseParams,
            steps: [{ method: "GET", path: undefined }],
        });
        expect(result).toBeDefined();
        expect(result.content[0].text).not.toContain("TypeError");
        expect(result.content[0].text).not.toContain("Cannot read");
    });
    it("includes known spec paths hint in the warning message", async () => {
        const result = await handler({
            ...baseParams,
            steps: [{ method: "POST", path: "/does/not/exist" }],
        });
        expect(result.isError).toBeFalsy();
        expect(result.content[0].text).toContain("/api/v1/users");
    });
});
describe("generateBatchScenarioRestTool — spec URL fetch safety (comment 3203006665)", () => {
    let handler;
    const baseParams = {
        scenarioName: "test_scenario",
        destination: "localhost",
        outputDir: "/tmp/skyramp-test",
        apiSchema: "https://example.com/openapi.json",
        steps: [{ method: "GET", path: "/api/v1/users" }],
    };
    beforeEach(() => {
        jest.clearAllMocks();
        handler = captureHandler();
    });
    it("skips spec check (no warning emitted) when URL returns non-2xx", async () => {
        global.fetch = jest.fn().mockResolvedValue({
            ok: false,
            status: 404,
            statusText: "Not Found",
            text: async () => "<html>Not Found</html>",
        });
        const result = await handler({ ...baseParams });
        // Non-2xx throws → caught → spec check skipped entirely, no warning in output
        expect(result.content[0].text).not.toContain("Spec warning");
    });
    it("skips spec check (no warning emitted) when spec has no paths", async () => {
        global.fetch = jest.fn().mockResolvedValue({
            ok: true,
            status: 200,
            statusText: "OK",
            text: async () => JSON.stringify({ openapi: "3.0.0", info: { title: "Test", version: "1.0" } }),
        });
        const result = await handler({ ...baseParams });
        expect(result.content[0].text).not.toContain("Spec warning");
    });
    afterEach(() => {
        // Restore fetch
        delete global.fetch;
    });
});
