import fs from "fs";
import os from "os";
import path from "path";
import { ScenarioGenerationService } from "./ScenarioGenerationService.js";
import { AUTH_PLACEHOLDER_TOKEN } from "../types/TestTypes.js";
const BASE_PARAMS = {
    scenarioName: "test-scenario",
    destination: "api.example.com",
    method: "GET",
    path: "/api/v1/items",
    outputDir: "/tmp/skyramp-test",
    baseURL: "http://localhost:8000",
};
function generateTrace(overrides = {}) {
    const service = new ScenarioGenerationService();
    return service["generateTraceRequestFromInput"]({
        ...BASE_PARAMS,
        ...overrides,
    });
}
function getAuthHeaders(trace) {
    const { "Content-Type": _, ...rest } = trace.RequestHeaders;
    return rest;
}
describe("ScenarioGenerationService — auth header flavors", () => {
    // 1. Public API — no auth (explicit empty string)
    it("skips auth header when authHeader is empty string", () => {
        const trace = generateTrace({ authHeader: "" });
        expect(trace).not.toBeNull();
        const auth = getAuthHeaders(trace);
        expect(auth).toEqual({});
        expect(trace.RequestHeaders["Authorization"]).toBeUndefined();
    });
    // 2. Public API — no auth params at all (undefined)
    it("skips auth header when authHeader is undefined", () => {
        const trace = generateTrace({ authHeader: undefined });
        expect(trace).not.toBeNull();
        const auth = getAuthHeaders(trace);
        expect(auth).toEqual({});
    });
    // 3. Bearer token — Authorization with scheme embedded in trace for CLI compatibility
    it("generates Authorization: Bearer <token> header in trace", () => {
        const trace = generateTrace({
            authHeader: "Authorization",
            authScheme: "Bearer",
        });
        expect(trace).not.toBeNull();
        expect(trace.RequestHeaders["Authorization"]).toEqual([
            `Bearer ${AUTH_PLACEHOLDER_TOKEN}`,
        ]);
    });
    // 4. DRF Token — Authorization with Token scheme embedded in trace
    it("generates Authorization: Token <token> header in trace", () => {
        const trace = generateTrace({
            authHeader: "Authorization",
            authScheme: "Token",
        });
        expect(trace).not.toBeNull();
        expect(trace.RequestHeaders["Authorization"]).toEqual([
            `Token ${AUTH_PLACEHOLDER_TOKEN}`,
        ]);
    });
    // 5. Basic auth — Authorization with Basic scheme embedded in trace
    it("generates Authorization: Basic <token> header in trace", () => {
        const trace = generateTrace({
            authHeader: "Authorization",
            authScheme: "Basic",
        });
        expect(trace).not.toBeNull();
        expect(trace.RequestHeaders["Authorization"]).toEqual([
            `Basic ${AUTH_PLACEHOLDER_TOKEN}`,
        ]);
    });
    // 6. Raw token — Authorization with no scheme, placeholder only
    it("generates Authorization with raw placeholder when authScheme is empty", () => {
        const trace = generateTrace({
            authHeader: "Authorization",
            authScheme: "",
        });
        expect(trace).not.toBeNull();
        expect(trace.RequestHeaders["Authorization"]).toEqual([
            AUTH_PLACEHOLDER_TOKEN,
        ]);
    });
    // 7. X-API-Key header with explicit token
    it("includes X-API-Key with explicit token in trace", () => {
        const trace = generateTrace({
            authHeader: "X-API-Key",
            authToken: AUTH_PLACEHOLDER_TOKEN,
        });
        expect(trace).not.toBeNull();
        expect(trace.RequestHeaders["X-API-Key"]).toEqual([
            AUTH_PLACEHOLDER_TOKEN,
        ]);
        expect(trace.RequestHeaders["Authorization"]).toBeUndefined();
    });
    // 8. Cookie-based auth (e.g. NextAuth)
    it("includes Cookie with session token in trace", () => {
        const trace = generateTrace({
            authHeader: "Cookie",
            authToken: `session=${AUTH_PLACEHOLDER_TOKEN}`,
        });
        expect(trace).not.toBeNull();
        expect(trace.RequestHeaders["Cookie"]).toEqual([
            `session=${AUTH_PLACEHOLDER_TOKEN}`,
        ]);
        expect(trace.RequestHeaders["Authorization"]).toBeUndefined();
    });
    // 9. Custom header (e.g. internal microservice)
    it("includes custom auth header with token in trace", () => {
        const trace = generateTrace({
            authHeader: "X-Internal-Service-Key",
            authToken: AUTH_PLACEHOLDER_TOKEN,
        });
        expect(trace).not.toBeNull();
        expect(trace.RequestHeaders["X-Internal-Service-Key"]).toEqual([
            AUTH_PLACEHOLDER_TOKEN,
        ]);
        expect(trace.RequestHeaders["Authorization"]).toBeUndefined();
    });
    // 10. OAuth — Authorization with OAuth scheme embedded in trace
    it("generates Authorization: OAuth <token> header in trace", () => {
        const trace = generateTrace({
            authHeader: "Authorization",
            authScheme: "OAuth",
        });
        expect(trace).not.toBeNull();
        expect(trace.RequestHeaders["Authorization"]).toEqual([
            `OAuth ${AUTH_PLACEHOLDER_TOKEN}`,
        ]);
    });
    // 11. Explicit token overrides scheme auto-generation
    it("uses explicit authToken and ignores authScheme when both provided", () => {
        const trace = generateTrace({
            authHeader: "Authorization",
            authScheme: "Bearer",
            authToken: "my-hardcoded-token-123",
        });
        expect(trace).not.toBeNull();
        expect(trace.RequestHeaders["Authorization"]).toEqual([
            "my-hardcoded-token-123",
        ]);
    });
    // 12. Non-Authorization header without token — placeholder in trace (CLI parameterizes it)
    it("includes X-API-Key with placeholder when authToken is missing", () => {
        const trace = generateTrace({
            authHeader: "X-API-Key",
        });
        expect(trace).not.toBeNull();
        expect(trace.RequestHeaders["X-API-Key"]).toEqual([AUTH_PLACEHOLDER_TOKEN]);
    });
    // 16. Cookie header without explicit token — placeholder in trace (CLI parameterizes it)
    it("includes Cookie with placeholder when authToken is missing", () => {
        const trace = generateTrace({
            authHeader: "Cookie",
        });
        expect(trace).not.toBeNull();
        expect(trace.RequestHeaders["Cookie"]).toEqual([AUTH_PLACEHOLDER_TOKEN]);
    });
    // 17. Custom header without explicit token — placeholder in trace (CLI parameterizes it)
    it("includes custom auth header with placeholder when authToken is missing", () => {
        const trace = generateTrace({
            authHeader: "X-Internal-Service-Key",
        });
        expect(trace).not.toBeNull();
        expect(trace.RequestHeaders["X-Internal-Service-Key"]).toEqual([AUTH_PLACEHOLDER_TOKEN]);
    });
    // 13. Content-Type always present regardless of auth
    it("always includes Content-Type regardless of auth config", () => {
        const noAuth = generateTrace({ authHeader: "" });
        const bearer = generateTrace({ authHeader: "Authorization", authScheme: "Bearer" });
        const apiKey = generateTrace({ authHeader: "X-API-Key", authToken: "key123" });
        expect(noAuth.RequestHeaders["Content-Type"]).toEqual(["application/json"]);
        expect(bearer.RequestHeaders["Content-Type"]).toEqual(["application/json"]);
        expect(apiKey.RequestHeaders["Content-Type"]).toEqual(["application/json"]);
    });
    // 14. Firebase AppCheck custom header
    it("includes X-Firebase-AppCheck with token in trace", () => {
        const trace = generateTrace({
            authHeader: "X-Firebase-AppCheck",
            authToken: AUTH_PLACEHOLDER_TOKEN,
        });
        expect(trace).not.toBeNull();
        expect(trace.RequestHeaders["X-Firebase-AppCheck"]).toEqual([
            AUTH_PLACEHOLDER_TOKEN,
        ]);
        expect(trace.RequestHeaders["Authorization"]).toBeUndefined();
    });
    // 15. Proxy-Authorization header with explicit token
    it("includes Proxy-Authorization with explicit token in trace", () => {
        const trace = generateTrace({
            authHeader: "Proxy-Authorization",
            authToken: "Bearer proxy-token-abc",
        });
        expect(trace).not.toBeNull();
        expect(trace.RequestHeaders["Proxy-Authorization"]).toEqual([
            "Bearer proxy-token-abc",
        ]);
        expect(trace.RequestHeaders["Authorization"]).toBeUndefined();
    });
});
describe("ScenarioGenerationService — queryParams handling", () => {
    // --- Primitive values (strings, numbers, booleans) ---
    it("coerces string values into single-element arrays", () => {
        const trace = generateTrace({ queryParams: '{"status":"active"}' });
        expect(trace.QueryParams).toEqual({ status: ["active"] });
    });
    it("coerces numeric values to string", () => {
        const trace = generateTrace({ queryParams: '{"limit":10,"offset":0}' });
        expect(trace.QueryParams).toEqual({ limit: ["10"], offset: ["0"] });
    });
    it("coerces boolean values to string", () => {
        const trace = generateTrace({ queryParams: '{"active":true,"deleted":false}' });
        expect(trace.QueryParams).toEqual({ active: ["true"], deleted: ["false"] });
    });
    it("coerces empty string to single-element array", () => {
        const trace = generateTrace({ queryParams: '{"q":""}' });
        expect(trace.QueryParams).toEqual({ q: [""] });
    });
    it("passes bracket-notation keys through verbatim (Directus regression)", () => {
        // Root cause of the original P0: filter[title][_neq] must reach the Go backend
        // as the literal key string, not as a nested object or [object Object].
        const trace = generateTrace({
            queryParams: '{"filter[status][_eq]":"published","filter[date_created][_gte]":"2024-01-01","limit":25}',
        });
        expect(trace.QueryParams["filter[status][_eq]"]).toEqual(["published"]);
        expect(trace.QueryParams["filter[date_created][_gte]"]).toEqual(["2024-01-01"]);
        expect(trace.QueryParams["limit"]).toEqual(["25"]);
    });
    // --- Null omission ---
    it("omits null values entirely (prevents ?status=null in URL)", () => {
        // null means "omit this param" — same convention as axios/fetch omitting undefined params
        const trace = generateTrace({ queryParams: '{"cursor":null,"limit":20}' });
        expect(trace.QueryParams["cursor"]).toBeUndefined();
        expect(trace.QueryParams["limit"]).toEqual(["20"]);
    });
    it("omits all-null object leaving only non-null keys", () => {
        const trace = generateTrace({ queryParams: '{"a":null,"b":null,"c":"keep"}' });
        expect(trace.QueryParams).toEqual({ c: ["keep"] });
    });
    // --- Array values: filter nulls, comma-join into single element ---
    it("array of strings joins into single comma-separated value", () => {
        const trace = generateTrace({ queryParams: '{"tags":["sale","new","featured"]}' });
        expect(trace.QueryParams["tags"]).toEqual(["sale,new,featured"]);
    });
    it("array of mixed primitives coerces each to string then joins", () => {
        const trace = generateTrace({ queryParams: '{"ids":[1,2,"three",true,0,false]}' });
        expect(trace.QueryParams["ids"]).toEqual(["1,2,three,true,0,false"]);
    });
    it("array with null items filters nulls before joining", () => {
        const trace = generateTrace({ queryParams: '{"tags":[null,"active",null,"pending"]}' });
        expect(trace.QueryParams["tags"]).toEqual(["active,pending"]);
    });
    it("array with all-null items omits the key entirely", () => {
        const trace = generateTrace({ queryParams: '{"tags":[null,null],"limit":5}' });
        expect(trace.QueryParams["tags"]).toBeUndefined();
        expect(trace.QueryParams["limit"]).toEqual(["5"]);
    });
    it("empty array omits the key (no items after filtering)", () => {
        const trace = generateTrace({ queryParams: '{"ids":[],"limit":10}' });
        expect(trace.QueryParams["ids"]).toBeUndefined();
        expect(trace.QueryParams["limit"]).toEqual(["10"]);
    });
    it("array containing objects JSON-stringifies each then joins", () => {
        const trace = generateTrace({
            queryParams: '{"filters":[{"field":"status","value":"active"},{"field":"price","op":"gte","value":"10"}]}',
        });
        expect(trace.QueryParams["filters"]).toEqual([
            '{"field":"status","value":"active"},{"field":"price","op":"gte","value":"10"}',
        ]);
    });
    it("array mixing objects, primitives, and nulls: filters nulls, stringifies objects, joins all", () => {
        const trace = generateTrace({
            queryParams: '{"mixed":[null,{"nested":true},"plain",42,null]}',
        });
        expect(trace.QueryParams["mixed"]).toEqual(['{"nested":true},plain,42']);
    });
    // --- Nested object fallback: JSON.stringify ---
    it("nested object is JSON-stringified as fallback", () => {
        const trace = generateTrace({
            queryParams: '{"filter":{"status":"active","min_price":10}}',
        });
        expect(trace.QueryParams["filter"]).toEqual(['{"status":"active","min_price":10}']);
    });
    it("deeply nested object is fully JSON-stringified", () => {
        const trace = generateTrace({
            queryParams: '{"filter":{"title":{"_neq":"not-a-number"}}}',
        });
        expect(trace.QueryParams["filter"]).toEqual(['{"title":{"_neq":"not-a-number"}}']);
    });
    it("empty nested object is JSON-stringified to '{}'", () => {
        const trace = generateTrace({ queryParams: '{"filter":{}}' });
        expect(trace.QueryParams["filter"]).toEqual(["{}"]);
    });
    // --- JSON parsing and object validation ---
    it("omitted queryParams produces empty QueryParams", () => {
        const trace = generateTrace({});
        expect(trace.QueryParams).toEqual({});
    });
    it("invalid JSON produces empty QueryParams without throwing", () => {
        const trace = generateTrace({ queryParams: "not-valid-json{" });
        expect(trace).not.toBeNull();
        expect(trace.QueryParams).toEqual({});
    });
    it("JSON array (not object) produces empty QueryParams", () => {
        const trace = generateTrace({ queryParams: '["limit","10"]' });
        expect(trace.QueryParams).toEqual({});
    });
    it("JSON null produces empty QueryParams", () => {
        const trace = generateTrace({ queryParams: "null" });
        expect(trace.QueryParams).toEqual({});
    });
    it("JSON number produces empty QueryParams", () => {
        const trace = generateTrace({ queryParams: "42" });
        expect(trace.QueryParams).toEqual({});
    });
    it("JSON string produces empty QueryParams", () => {
        const trace = generateTrace({ queryParams: '"just a string"' });
        expect(trace.QueryParams).toEqual({});
    });
    it("empty object produces empty QueryParams", () => {
        const trace = generateTrace({ queryParams: '{}' });
        expect(trace.QueryParams).toEqual({});
    });
    // --- Security: PROTO_KEYS guard ---
    it("skips __proto__ key to prevent prototype pollution via setter", () => {
        const trace = generateTrace({
            queryParams: '{"__proto__":{"polluted":true},"limit":10}',
        });
        expect(Object.hasOwn(trace.QueryParams, "__proto__")).toBe(false);
        expect(Object.getPrototypeOf(trace.QueryParams)).toBe(Object.prototype);
        expect(trace.QueryParams["limit"]).toEqual(["10"]);
    });
    it("skips constructor and prototype keys", () => {
        const trace = generateTrace({
            queryParams: '{"constructor":"pwned","prototype":"evil","valid":"yes"}',
        });
        expect(Object.hasOwn(trace.QueryParams, "constructor")).toBe(false);
        expect(Object.hasOwn(trace.QueryParams, "prototype")).toBe(false);
        expect(trace.QueryParams["valid"]).toEqual(["yes"]);
    });
});
describe("ScenarioGenerationService — baseURL parsing", () => {
    it("parses http baseURL correctly", () => {
        const trace = generateTrace({
            baseURL: "http://localhost:3000",
            authHeader: "",
        });
        expect(trace).not.toBeNull();
        expect(trace.Scheme).toBe("http");
        expect(trace.Destination).toBe("localhost");
        expect(trace.Port).toBe(3000);
    });
    it("parses https baseURL with default port", () => {
        const trace = generateTrace({
            baseURL: "https://api.example.com",
            authHeader: "",
        });
        expect(trace).not.toBeNull();
        expect(trace.Scheme).toBe("https");
        expect(trace.Destination).toBe("api.example.com");
        expect(trace.Port).toBe(443);
    });
    it("parses http baseURL with default port", () => {
        const trace = generateTrace({
            baseURL: "http://api.example.com",
            authHeader: "",
        });
        expect(trace).not.toBeNull();
        expect(trace.Scheme).toBe("http");
        expect(trace.Port).toBe(80);
    });
});
describe("ScenarioGenerationService — timestamp chaining", () => {
    it("assigns strictly increasing timestamps across multiple parseScenario calls", async () => {
        const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-ts-test-"));
        const service = new ScenarioGenerationService();
        const baseParams = {
            scenarioName: "timestamp-chain-test",
            destination: "api.example.com",
            method: "GET",
            path: "/api/v1/items",
            outputDir: tmpDir,
            baseURL: "http://localhost:8000",
        };
        await service.parseScenario({ ...baseParams, method: "POST", path: "/api/v1/items" });
        await service.parseScenario({ ...baseParams, method: "GET", path: "/api/v1/items/1" });
        await service.parseScenario({ ...baseParams, method: "DELETE", path: "/api/v1/items/1" });
        const fileName = "scenario_timestamp-chain-test.json";
        const filePath = path.join(tmpDir, fileName);
        const requests = JSON.parse(fs.readFileSync(filePath, "utf8"));
        expect(requests).toHaveLength(3);
        for (let i = 1; i < requests.length; i++) {
            const prev = new Date(requests[i - 1].Timestamp).getTime();
            const curr = new Date(requests[i].Timestamp).getTime();
            expect(curr).toBeGreaterThan(prev);
        }
        fs.rmSync(tmpDir, { recursive: true, force: true });
    });
});
