/**
 * Tests for evaluation metrics
 */
import { describe, it, expect } from "vitest";
import { calculatePrecision, calculateRecall, calculateF1Score, calculateMetrics, calculateStabilityMetrics, meetsTargets, formatPercent, getGrade, } from "./metrics.js";
describe("metrics", () => {
    describe("calculatePrecision", () => {
        it("returns 1 when no findings (perfect precision)", () => {
            expect(calculatePrecision(0, 0)).toBe(1);
        });
        it("calculates precision correctly", () => {
            // 8 TP, 2 FP = 8/10 = 0.8
            expect(calculatePrecision(8, 2)).toBe(0.8);
        });
        it("returns 1 when all findings are true positives", () => {
            expect(calculatePrecision(10, 0)).toBe(1);
        });
        it("returns 0 when all findings are false positives", () => {
            expect(calculatePrecision(0, 10)).toBe(0);
        });
    });
    describe("calculateRecall", () => {
        it("returns 1 when no expected findings", () => {
            expect(calculateRecall(0, 0)).toBe(1);
        });
        it("calculates recall correctly", () => {
            // 8 TP, 2 FN = 8/10 = 0.8
            expect(calculateRecall(8, 2)).toBe(0.8);
        });
        it("returns 1 when all expected findings are found", () => {
            expect(calculateRecall(10, 0)).toBe(1);
        });
        it("returns 0 when no expected findings are found", () => {
            expect(calculateRecall(0, 10)).toBe(0);
        });
    });
    describe("calculateF1Score", () => {
        it("returns 0 when precision and recall are both 0", () => {
            expect(calculateF1Score(0, 0)).toBe(0);
        });
        it("calculates F1 score correctly", () => {
            // F1 = 2 * (0.8 * 0.8) / (0.8 + 0.8) = 0.8
            expect(calculateF1Score(0.8, 0.8)).toBeCloseTo(0.8);
        });
        it("returns correct F1 for different precision/recall", () => {
            // F1 = 2 * (0.9 * 0.7) / (0.9 + 0.7) = 2 * 0.63 / 1.6 = 0.7875
            expect(calculateF1Score(0.9, 0.7)).toBeCloseTo(0.7875);
        });
        it("returns 1 for perfect precision and recall", () => {
            expect(calculateF1Score(1, 1)).toBe(1);
        });
    });
    describe("calculateMetrics", () => {
        it("calculates aggregate metrics from fixture results", () => {
            const mockExpected = {
                file: "test.ts",
                line: 10,
                severity: "high",
                category: "sql-injection",
                description: "SQL injection",
                isVulnerable: true,
            };
            const mockActual = {
                scanner: "semgrep",
                ruleId: "sql-injection",
                file: "test.ts",
                line: 10,
                severity: "high",
                message: "SQL injection found",
                confidence: 100,
            };
            const mockMatched = {
                expected: mockExpected,
                actual: mockActual,
            };
            const results = [
                {
                    fixtureId: "test-001",
                    success: true,
                    actualFindings: [mockActual],
                    expectedFindings: [mockExpected],
                    truePositives: [mockMatched],
                    falsePositives: [],
                    falseNegatives: [],
                    duration: 1000,
                },
            ];
            const metrics = calculateMetrics(results);
            expect(metrics.totalFixtures).toBe(1);
            expect(metrics.successfulFixtures).toBe(1);
            expect(metrics.failedFixtures).toBe(0);
            expect(metrics.totalExpected).toBe(1);
            expect(metrics.totalActual).toBe(1);
            expect(metrics.totalTruePositives).toBe(1);
            expect(metrics.totalFalsePositives).toBe(0);
            expect(metrics.totalFalseNegatives).toBe(0);
            expect(metrics.precision).toBe(1);
            expect(metrics.recall).toBe(1);
            expect(metrics.f1Score).toBe(1);
        });
        it("handles multiple fixtures with false positives and negatives", () => {
            const expected1 = {
                file: "a.ts",
                line: 5,
                severity: "critical",
                category: "sql-injection",
                description: "SQLi",
                isVulnerable: true,
            };
            const expected2 = {
                file: "b.ts",
                line: 10,
                severity: "high",
                category: "xss",
                description: "XSS",
                isVulnerable: true,
            };
            const actual1 = {
                scanner: "semgrep",
                ruleId: "sql-injection",
                file: "a.ts",
                line: 5,
                severity: "critical",
                message: "SQLi",
                confidence: 100,
            };
            const actualFP = {
                scanner: "semgrep",
                ruleId: "false-positive",
                file: "c.ts",
                line: 20,
                severity: "medium",
                message: "FP",
                confidence: 100,
            };
            const results = [
                {
                    fixtureId: "test-001",
                    success: true,
                    actualFindings: [actual1, actualFP],
                    expectedFindings: [expected1],
                    truePositives: [{ expected: expected1, actual: actual1 }],
                    falsePositives: [actualFP],
                    falseNegatives: [],
                    duration: 1000,
                },
                {
                    fixtureId: "test-002",
                    success: true,
                    actualFindings: [],
                    expectedFindings: [expected2],
                    truePositives: [],
                    falsePositives: [],
                    falseNegatives: [expected2],
                    duration: 500,
                },
            ];
            const metrics = calculateMetrics(results);
            expect(metrics.totalExpected).toBe(2);
            expect(metrics.totalActual).toBe(2);
            expect(metrics.totalTruePositives).toBe(1);
            expect(metrics.totalFalsePositives).toBe(1);
            expect(metrics.totalFalseNegatives).toBe(1);
            expect(metrics.precision).toBe(0.5); // 1 / (1 + 1)
            expect(metrics.recall).toBe(0.5); // 1 / (1 + 1)
            expect(metrics.f1Score).toBe(0.5); // 2 * (0.5 * 0.5) / (0.5 + 0.5)
        });
        it("handles failed fixtures", () => {
            const results = [
                {
                    fixtureId: "test-001",
                    success: false,
                    error: "Scanner timeout",
                    actualFindings: [],
                    expectedFindings: [],
                    truePositives: [],
                    falsePositives: [],
                    falseNegatives: [],
                    duration: 5000,
                },
            ];
            const metrics = calculateMetrics(results);
            expect(metrics.totalFixtures).toBe(1);
            expect(metrics.successfulFixtures).toBe(0);
            expect(metrics.failedFixtures).toBe(1);
        });
        it("calculates by-category metrics", () => {
            const sqlExpected = {
                file: "db.ts",
                line: 5,
                severity: "critical",
                category: "sql-injection",
                description: "SQLi",
                isVulnerable: true,
            };
            const sqlActual = {
                scanner: "semgrep",
                ruleId: "sqli",
                file: "db.ts",
                line: 5,
                severity: "critical",
                message: "SQLi",
                confidence: 100,
            };
            const results = [
                {
                    fixtureId: "test-001",
                    success: true,
                    actualFindings: [sqlActual],
                    expectedFindings: [sqlExpected],
                    truePositives: [{ expected: sqlExpected, actual: sqlActual }],
                    falsePositives: [],
                    falseNegatives: [],
                    duration: 1000,
                },
            ];
            const metrics = calculateMetrics(results);
            expect(metrics.byCategory["sql-injection"]).toBeDefined();
            expect(metrics.byCategory["sql-injection"].expected).toBe(1);
            expect(metrics.byCategory["sql-injection"].truePositives).toBe(1);
            expect(metrics.byCategory["sql-injection"].precision).toBe(1);
            expect(metrics.byCategory["sql-injection"].recall).toBe(1);
        });
        it("calculates by-scanner metrics", () => {
            const actual1 = {
                scanner: "semgrep",
                ruleId: "rule1",
                file: "a.ts",
                line: 5,
                severity: "high",
                message: "Issue",
                confidence: 100,
            };
            const actual2 = {
                scanner: "gitleaks",
                ruleId: "rule2",
                file: "b.ts",
                line: 10,
                severity: "critical",
                message: "Secret",
                confidence: 100,
            };
            const results = [
                {
                    fixtureId: "test-001",
                    success: true,
                    actualFindings: [actual1, actual2],
                    expectedFindings: [],
                    truePositives: [],
                    falsePositives: [actual1, actual2],
                    falseNegatives: [],
                    duration: 1000,
                },
            ];
            const metrics = calculateMetrics(results);
            expect(metrics.byScanner["semgrep"]).toBeDefined();
            expect(metrics.byScanner["semgrep"].findings).toBe(1);
            expect(metrics.byScanner["gitleaks"]).toBeDefined();
            expect(metrics.byScanner["gitleaks"].findings).toBe(1);
        });
    });
    describe("calculateStabilityMetrics", () => {
        it("returns 100% stability for empty runs", () => {
            const metrics = calculateStabilityMetrics([]);
            expect(metrics.runs).toBe(0);
            expect(metrics.stabilityPercent).toBe(100);
        });
        it("calculates stability for consistent findings", () => {
            const actual = {
                scanner: "semgrep",
                ruleId: "rule1",
                file: "a.ts",
                line: 5,
                severity: "high",
                message: "Issue",
                confidence: 100,
            };
            const result = {
                fixtureId: "test-001",
                success: true,
                actualFindings: [actual],
                expectedFindings: [],
                truePositives: [],
                falsePositives: [],
                falseNegatives: [],
                duration: 1000,
            };
            // Same result in 3 runs = 100% stable
            const allRuns = [[result], [result], [result]];
            const metrics = calculateStabilityMetrics(allRuns);
            expect(metrics.runs).toBe(3);
            expect(metrics.consistentFindings).toBe(1);
            expect(metrics.inconsistentFindings).toBe(0);
            expect(metrics.stabilityPercent).toBe(100);
        });
        it("calculates stability for inconsistent findings", () => {
            const actual1 = {
                scanner: "semgrep",
                ruleId: "rule1",
                file: "a.ts",
                line: 5,
                severity: "high",
                message: "Issue",
                confidence: 100,
            };
            const actual2 = {
                scanner: "semgrep",
                ruleId: "rule2",
                file: "b.ts",
                line: 10,
                severity: "high",
                message: "Another issue",
                confidence: 100,
            };
            const run1 = {
                fixtureId: "test-001",
                success: true,
                actualFindings: [actual1],
                expectedFindings: [],
                truePositives: [],
                falsePositives: [],
                falseNegatives: [],
                duration: 1000,
            };
            const run2 = {
                fixtureId: "test-001",
                success: true,
                actualFindings: [actual1, actual2],
                expectedFindings: [],
                truePositives: [],
                falsePositives: [],
                falseNegatives: [],
                duration: 1000,
            };
            // actual1 appears in both runs (consistent)
            // actual2 appears in only run2 (inconsistent)
            const allRuns = [[run1], [run2]];
            const metrics = calculateStabilityMetrics(allRuns);
            expect(metrics.runs).toBe(2);
            expect(metrics.consistentFindings).toBe(1);
            expect(metrics.inconsistentFindings).toBe(1);
            expect(metrics.stabilityPercent).toBe(50);
        });
    });
    describe("meetsTargets", () => {
        it("returns true when all targets are met", () => {
            const metrics = {
                precision: 0.95,
                recall: 0.90,
                f1Score: 0.92,
            };
            const result = meetsTargets(metrics);
            expect(result.meetsAll).toBe(true);
            expect(result.results.precision.met).toBe(true);
            expect(result.results.recall.met).toBe(true);
            expect(result.results.f1Score.met).toBe(true);
        });
        it("returns false when precision target is not met", () => {
            const metrics = {
                precision: 0.80,
                recall: 0.90,
                f1Score: 0.85,
            };
            const result = meetsTargets(metrics);
            expect(result.meetsAll).toBe(false);
            expect(result.results.precision.met).toBe(false);
        });
        it("uses custom targets when provided", () => {
            const metrics = {
                precision: 0.80,
                recall: 0.75,
                f1Score: 0.77,
            };
            const result = meetsTargets(metrics, {
                precision: 0.75,
                recall: 0.70,
                f1Score: 0.70,
            });
            expect(result.meetsAll).toBe(true);
        });
    });
    describe("formatPercent", () => {
        it("formats decimal as percentage", () => {
            expect(formatPercent(0.95)).toBe("95.0%");
        });
        it("respects decimal places parameter", () => {
            expect(formatPercent(0.9567, 2)).toBe("95.67%");
        });
        it("handles 100%", () => {
            expect(formatPercent(1)).toBe("100.0%");
        });
        it("handles 0%", () => {
            expect(formatPercent(0)).toBe("0.0%");
        });
    });
    describe("getGrade", () => {
        it("returns A+ for scores >= 0.95", () => {
            expect(getGrade(0.95)).toBe("A+");
            expect(getGrade(1.0)).toBe("A+");
        });
        it("returns A for scores >= 0.9", () => {
            expect(getGrade(0.90)).toBe("A");
            expect(getGrade(0.94)).toBe("A");
        });
        it("returns B+ for scores >= 0.85", () => {
            expect(getGrade(0.85)).toBe("B+");
        });
        it("returns B for scores >= 0.8", () => {
            expect(getGrade(0.80)).toBe("B");
        });
        it("returns C+ for scores >= 0.75", () => {
            expect(getGrade(0.75)).toBe("C+");
        });
        it("returns C for scores >= 0.7", () => {
            expect(getGrade(0.70)).toBe("C");
        });
        it("returns D for scores >= 0.6", () => {
            expect(getGrade(0.60)).toBe("D");
        });
        it("returns F for scores < 0.6", () => {
            expect(getGrade(0.50)).toBe("F");
            expect(getGrade(0)).toBe("F");
        });
    });
});
//# sourceMappingURL=metrics.test.js.map