/**
 * Detection Engine Tests
 */
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { mkdtemp, rm, writeFile, mkdir } from "fs/promises";
import { join } from "path";
import { tmpdir } from "os";
import { runDetection, getBuiltinRules, getBuiltinRuleById, getBuiltinRulesByCategory, listAvailableRules, getDetectionCategories, BUILTIN_RULES, } from "../index.js";
import { queryAST } from "../engines/ast-query.js";
import { analyzeDataFlow } from "../engines/data-flow.js";
import { createRule, loadRuleFromYAML, RuleValidationError } from "../rules/loader.js";
describe("Detection Engine", () => {
    let tempDir;
    beforeEach(async () => {
        tempDir = await mkdtemp(join(tmpdir(), `detection-test-${Math.random().toString(36).slice(2, 8)}-`));
    });
    afterEach(async () => {
        await rm(tempDir, { recursive: true, force: true });
    });
    describe("Built-in Rules", () => {
        it("has all expected built-in rules", () => {
            const rules = getBuiltinRules();
            expect(rules.length).toBeGreaterThan(0);
            const categories = new Set(rules.map((r) => r.category));
            expect(categories.has("idor")).toBe(true);
            expect(categories.has("ssrf")).toBe(true);
            expect(categories.has("sql-injection")).toBe(true);
        });
        it("can retrieve rule by ID", () => {
            const rule = getBuiltinRuleById("vaspera:idor:user-controlled-id");
            expect(rule).toBeDefined();
            expect(rule?.name).toContain("User-Controlled ID");
        });
        it("can retrieve rules by category", () => {
            const idorRules = getBuiltinRulesByCategory("idor");
            expect(idorRules.length).toBeGreaterThan(0);
            expect(idorRules.every((r) => r.category === "idor")).toBe(true);
        });
        it("lists available rules with metadata", () => {
            const list = listAvailableRules();
            expect(list.length).toBe(BUILTIN_RULES.length);
            expect(list[0]).toHaveProperty("id");
            expect(list[0]).toHaveProperty("name");
            expect(list[0]).toHaveProperty("category");
            expect(list[0]).toHaveProperty("severity");
        });
        it("returns detection categories", () => {
            const categories = getDetectionCategories();
            expect(categories).toContain("idor");
            expect(categories).toContain("ssrf");
            expect(categories).toContain("xss");
        });
    });
    describe("Rule Loader", () => {
        it("creates a valid rule programmatically", () => {
            const rule = createRule({
                id: "test:custom:rule",
                name: "Custom Test Rule",
                description: "A test rule",
                category: "test",
                severity: "medium",
                confidence: 75,
                engines: {
                    astQuery: { pattern: "test($arg)" },
                },
            });
            expect(rule.id).toBe("test:custom:rule");
            expect(rule.enabled).toBe(true);
        });
        it("loads rule from YAML", async () => {
            const yamlContent = `
id: test:yaml:rule
name: YAML Test Rule
description: A rule loaded from YAML
category: test
severity: high
confidence: 80
engines:
  astQuery:
    pattern: "dangerousFunction($input)"
cweIds:
  - CWE-123
`;
            const yamlPath = join(tempDir, "test-rule.yaml");
            await writeFile(yamlPath, yamlContent, "utf-8");
            const rule = await loadRuleFromYAML(yamlPath);
            expect(rule.id).toBe("test:yaml:rule");
            expect(rule.severity).toBe("high");
            expect(rule.cweIds).toContain("CWE-123");
        });
        it("throws on invalid rule", () => {
            expect(() => createRule({
                id: "test:invalid",
                name: "Invalid",
                description: "Missing engines",
                category: "test",
                severity: "high",
                confidence: 80,
                engines: {},
            })).toThrow(RuleValidationError);
        });
    });
    describe("AST Query Engine", () => {
        it("matches function calls", async () => {
            const code = `
function handler(req: Request) {
  const user = findById(req.params.id);
  return user;
}
`;
            const filePath = join(tempDir, "query-test.ts");
            await writeFile(filePath, code, "utf-8");
            const matches = await queryAST(filePath, {
                pattern: "findById($id)",
            });
            expect(matches.length).toBeGreaterThan(0);
            expect(matches[0].text).toContain("findById");
        });
        it("captures pattern variables", async () => {
            const code = `
fetch(userProvidedUrl);
axios.get(req.body.callback);
`;
            const filePath = join(tempDir, "capture-test.ts");
            await writeFile(filePath, code, "utf-8");
            const matches = await queryAST(filePath, {
                pattern: "fetch($url)",
            });
            expect(matches.length).toBeGreaterThan(0);
        });
    });
    describe("Data Flow Engine", () => {
        it("runs data flow analysis without error", async () => {
            const code = `
function handler(req: Request, res: Response) {
  const userId = req.params.id;
  const user = db.users.findById(userId);
  return res.json(user);
}
`;
            const filePath = join(tempDir, "dataflow-test.ts");
            await writeFile(filePath, code, "utf-8");
            const paths = await analyzeDataFlow(filePath, {
                sources: [{ pattern: "req.params.$id" }],
                sinks: [{ pattern: "findById($source)" }],
            });
            expect(Array.isArray(paths)).toBe(true);
        });
        it("respects sanitizers", async () => {
            const code = `
function handler(req: Request) {
  const id = req.params.id;
  const safeId = validateId(id);
  return db.findById(safeId);
}
`;
            const filePath = join(tempDir, "sanitized-test.ts");
            await writeFile(filePath, code, "utf-8");
            const paths = await analyzeDataFlow(filePath, {
                sources: [{ pattern: "req.params.$id" }],
                sinks: [{ pattern: "findById($source)" }],
                sanitizers: [{ pattern: "validateId($input)" }],
            });
            expect(paths.filter((p) => !p.sanitized).length).toBe(0);
        });
    });
    describe("Full Detection Run", () => {
        it("runs detection on project", async () => {
            const srcDir = join(tempDir, "src");
            await mkdir(srcDir, { recursive: true });
            const code = `
export function getResource(req: Request) {
  const id = req.params.id;
  return db.resources.findById(id);
}
`;
            await writeFile(join(srcDir, "handler.ts"), code, "utf-8");
            const result = await runDetection({
                projectPath: tempDir,
                files: [join(srcDir, "handler.ts")],
            });
            expect(result.success).toBe(true);
            expect(result.filesAnalyzed).toBe(1);
            expect(result.rulesEvaluated).toBeGreaterThan(0);
        });
        it("returns empty for no matches", async () => {
            const code = `
export function safeFunction() {
  return "hello world";
}
`;
            const filePath = join(tempDir, "safe.ts");
            await writeFile(filePath, code, "utf-8");
            const result = await runDetection({
                projectPath: tempDir,
                files: [filePath],
            });
            expect(result.success).toBe(true);
            expect(result.matches.length).toBe(0);
        });
        it("handles SSRF detection", async () => {
            const code = `
async function fetchWebhook(req: Request) {
  const url = req.body.webhookUrl;
  const response = await fetch(url);
  return response.json();
}
`;
            const filePath = join(tempDir, "ssrf.ts");
            await writeFile(filePath, code, "utf-8");
            const ssrfRules = getBuiltinRulesByCategory("ssrf");
            const result = await runDetection({
                projectPath: tempDir,
                files: [filePath],
                rules: ssrfRules,
            });
            expect(result.rulesEvaluated).toBe(ssrfRules.length);
        });
        it("handles SQL injection detection", async () => {
            const code = `
async function getUsers(req: Request) {
  const name = req.query.name;
  const result = await db.query(\`SELECT * FROM users WHERE name = '\${name}'\`);
  return result;
}
`;
            const filePath = join(tempDir, "sqli.ts");
            await writeFile(filePath, code, "utf-8");
            const sqliRules = getBuiltinRulesByCategory("sql-injection");
            const result = await runDetection({
                projectPath: tempDir,
                files: [filePath],
                rules: sqliRules,
            });
            expect(result.rulesEvaluated).toBe(sqliRules.length);
        });
    });
    describe("Detection Match Format", () => {
        it("includes required fields in matches", async () => {
            const code = `
function handler(req: Request) {
  const url = req.body.url;
  fetch(url);
}
`;
            const filePath = join(tempDir, "match-format.ts");
            await writeFile(filePath, code, "utf-8");
            const result = await runDetection({
                projectPath: tempDir,
                files: [filePath],
            });
            if (result.matches.length > 0) {
                const match = result.matches[0];
                expect(match).toHaveProperty("ruleId");
                expect(match).toHaveProperty("file");
                expect(match).toHaveProperty("line");
                expect(match).toHaveProperty("message");
                expect(match).toHaveProperty("severity");
                expect(match).toHaveProperty("confidence");
                expect(match).toHaveProperty("category");
            }
        });
    });
});
//# sourceMappingURL=detection.test.js.map