"""Tests for SARIF CI Integration — enhanced SARIF output for CI/CD platforms."""

from __future__ import annotations

import json

import pytest

from mcpaudit.models import Finding, ScanResult, ScanSummary, Severity
from mcpaudit.sarif_ci import (
    SARIF_VERSION,
    SARIF_SCHEMA,
    TOOL_NAME,
    export_github_annotations,
    export_gitlab_sast_json,
    export_sarif_json,
    format_sarif_summary,
    generate_github_annotation,
    generate_gitlab_sast,
    generate_sarif_upload,
    _compute_fingerprint,
    _encode_uri,
    _severity_to_sarif_level,
    _severity_to_sarif_rank,
    _confidence_to_precision,
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_finding(**kwargs) -> Finding:
    """Create a test Finding with defaults."""
    defaults = {
        "rule_id": "CMD-001",
        "severity": Severity.HIGH,
        "title": "Command Injection",
        "description": "Potential OS command injection via subprocess",
        "file": "server.py",
        "line": 42,
        "code_snippet": "subprocess.call(user_input)",
        "recommendation": "Use subprocess.run with shell=False",
        "cwe": "CWE-78",
        "confidence": "HIGH",
    }
    defaults.update(kwargs)
    return Finding(**defaults)


def _make_scan_result(findings: list[Finding] | None = None, **kwargs) -> ScanResult:
    """Create a test ScanResult."""
    defaults = {
        "file": "server.py",
        "lines_scanned": 100,
        "scan_time_ms": 12.5,
    }
    defaults.update(kwargs)
    sr = ScanResult(**defaults)
    if findings:
        sr.findings = findings
    return sr


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------

class TestSeverityMapping:
    """Tests for severity mapping functions."""

    def test_critical_to_error(self):
        assert _severity_to_sarif_level(Severity.CRITICAL) == "error"

    def test_high_to_error(self):
        assert _severity_to_sarif_level(Severity.HIGH) == "error"

    def test_medium_to_warning(self):
        assert _severity_to_sarif_level(Severity.MEDIUM) == "warning"

    def test_low_to_note(self):
        assert _severity_to_sarif_level(Severity.LOW) == "note"

    def test_info_to_note(self):
        assert _severity_to_sarif_level(Severity.INFO) == "note"


class TestSeverityRank:
    """Tests for SARIF security-severity rank."""

    def test_critical_rank(self):
        assert _severity_to_sarif_rank(Severity.CRITICAL) == 9.5

    def test_high_rank(self):
        assert _severity_to_sarif_rank(Severity.HIGH) == 7.5

    def test_medium_rank(self):
        assert _severity_to_sarif_rank(Severity.MEDIUM) == 5.0

    def test_low_rank(self):
        assert _severity_to_sarif_rank(Severity.LOW) == 2.5

    def test_info_rank(self):
        assert _severity_to_sarif_rank(Severity.INFO) == 1.0


class TestConfidencePrecision:
    """Tests for confidence to precision mapping."""

    def test_high(self):
        assert _confidence_to_precision("HIGH") == "high"

    def test_medium(self):
        assert _confidence_to_precision("MEDIUM") == "medium"

    def test_low(self):
        assert _confidence_to_precision("LOW") == "low"

    def test_unknown(self):
        assert _confidence_to_precision("UNKNOWN") == "medium"


class TestComputeFingerprint:
    """Tests for fingerprint computation."""

    def test_deterministic(self):
        f = _make_finding()
        fp1 = _compute_fingerprint(f)
        fp2 = _compute_fingerprint(f)
        assert fp1 == fp2

    def test_different_findings_different_fingerprints(self):
        f1 = _make_finding(rule_id="CMD-001")
        f2 = _make_finding(rule_id="CMD-002")
        assert _compute_fingerprint(f1) != _compute_fingerprint(f2)

    def test_fingerprint_length(self):
        f = _make_finding()
        fp = _compute_fingerprint(f)
        assert len(fp) == 32  # SHA-256 truncated to 32 hex chars


class TestEncodeUri:
    """Tests for URI encoding."""

    def test_simple_path(self):
        assert _encode_uri("server.py") == "server.py"

    def test_path_with_spaces(self):
        result = _encode_uri("my server.py")
        assert "%20" in result

    def test_path_with_backslashes(self):
        result = _encode_uri("src\\server.py")
        assert "\\" not in result
        assert "src/server.py" == result

    def test_preserves_slashes(self):
        result = _encode_uri("src/main/server.py")
        assert result == "src/main/server.py"


# ---------------------------------------------------------------------------
# generate_sarif_upload
# ---------------------------------------------------------------------------

class TestGenerateSarifUpload:
    """Tests for full SARIF generation."""

    def test_basic_structure(self):
        findings = [_make_finding()]
        results = [_make_scan_result(findings)]
        sarif = generate_sarif_upload(results)

        assert sarif["$schema"] == SARIF_SCHEMA
        assert sarif["version"] == SARIF_VERSION
        assert len(sarif["runs"]) == 1

    def test_tool_driver(self):
        findings = [_make_finding()]
        results = [_make_scan_result(findings)]
        sarif = generate_sarif_upload(results)

        driver = sarif["runs"][0]["tool"]["driver"]
        assert driver["name"] == TOOL_NAME
        assert "version" in driver
        assert "semanticVersion" in driver
        assert "informationUri" in driver

    def test_rules_as_reporting_descriptors(self):
        findings = [
            _make_finding(rule_id="CMD-001"),
            _make_finding(rule_id="PATH-001", title="Path Traversal"),
        ]
        results = [_make_scan_result(findings)]
        sarif = generate_sarif_upload(results)

        rules = sarif["runs"][0]["tool"]["driver"]["rules"]
        assert len(rules) == 2
        rule_ids = {r["id"] for r in rules}
        assert "CMD-001" in rule_ids
        assert "PATH-001" in rule_ids

    def test_deduplicate_rules(self):
        findings = [
            _make_finding(rule_id="CMD-001", line=1),
            _make_finding(rule_id="CMD-001", line=5),
        ]
        results = [_make_scan_result(findings)]
        sarif = generate_sarif_upload(results)

        rules = sarif["runs"][0]["tool"]["driver"]["rules"]
        assert len(rules) == 1

    def test_result_fields(self):
        findings = [_make_finding()]
        results = [_make_scan_result(findings)]
        sarif = generate_sarif_upload(results)

        result = sarif["runs"][0]["results"][0]
        assert result["ruleId"] == "CMD-001"
        assert result["level"] == "error"
        assert "text" in result["message"]
        assert len(result["locations"]) == 1

    def test_artifact_locations(self):
        findings = [_make_finding(file="src/server.py")]
        results = [_make_scan_result(findings, file="src/server.py")]
        sarif = generate_sarif_upload(results)

        loc = sarif["runs"][0]["results"][0]["locations"][0]["physicalLocation"]
        assert loc["artifactLocation"]["uri"] == "src/server.py"
        assert loc["artifactLocation"]["uriBaseId"] == "%SRCROOT%"

    def test_fingerprints_present(self):
        findings = [_make_finding()]
        results = [_make_scan_result(findings)]
        sarif = generate_sarif_upload(results)

        result = sarif["runs"][0]["results"][0]
        assert "fingerprints" in result
        assert "partialFingerprints" in result

    def test_code_flow_with_snippet(self):
        findings = [_make_finding(code_snippet="subprocess.call(input)")]
        results = [_make_scan_result(findings)]
        sarif = generate_sarif_upload(results)

        result = sarif["runs"][0]["results"][0]
        assert "codeFlows" in result
        assert len(result["codeFlows"]) == 1

    def test_no_code_flow_without_snippet(self):
        findings = [_make_finding(code_snippet="")]
        results = [_make_scan_result(findings)]
        sarif = generate_sarif_upload(results)

        result = sarif["runs"][0]["results"][0]
        assert "codeFlows" not in result

    def test_cwe_relationships(self):
        findings = [_make_finding(cwe="CWE-78")]
        results = [_make_scan_result(findings)]
        sarif = generate_sarif_upload(results)

        rule = sarif["runs"][0]["tool"]["driver"]["rules"][0]
        assert "relationships" in rule
        assert rule["relationships"][0]["target"]["id"] == "CWE-78"

    def test_artifacts_list(self):
        findings = [_make_finding(file="a.py"), _make_finding(file="b.py")]
        results = [
            _make_scan_result([findings[0]], file="a.py"),
            _make_scan_result([findings[1]], file="b.py"),
        ]
        sarif = generate_sarif_upload(results)

        artifacts = sarif["runs"][0]["artifacts"]
        assert len(artifacts) == 2

    def test_invocations(self):
        findings = [_make_finding()]
        results = [_make_scan_result(findings)]
        sarif = generate_sarif_upload(results)

        invocations = sarif["runs"][0]["invocations"]
        assert len(invocations) == 1
        assert invocations[0]["executionSuccessful"] is True
        props = invocations[0]["properties"]
        assert "filesScanned" in props
        assert "linesScanned" in props

    def test_custom_tool_name(self):
        findings = [_make_finding()]
        results = [_make_scan_result(findings)]
        sarif = generate_sarif_upload(results, tool_name="custom-scanner")

        driver = sarif["runs"][0]["tool"]["driver"]
        assert driver["name"] == "custom-scanner"

    def test_empty_results(self):
        sarif = generate_sarif_upload([])
        assert sarif["runs"][0]["results"] == []
        assert sarif["runs"][0]["tool"]["driver"]["rules"] == []

    def test_rule_help_present(self):
        findings = [_make_finding(recommendation="Use subprocess.run")]
        results = [_make_scan_result(findings)]
        sarif = generate_sarif_upload(results)

        rule = sarif["runs"][0]["tool"]["driver"]["rules"][0]
        assert "help" in rule
        assert "text" in rule["help"]
        assert "markdown" in rule["help"]

    def test_fixes_present(self):
        findings = [_make_finding(recommendation="Fix this")]
        results = [_make_scan_result(findings)]
        sarif = generate_sarif_upload(results)

        result = sarif["runs"][0]["results"][0]
        assert "fixes" in result

    def test_column_kind(self):
        findings = [_make_finding()]
        results = [_make_scan_result(findings)]
        sarif = generate_sarif_upload(results)
        assert sarif["runs"][0]["columnKind"] == "utf16CodeUnits"


# ---------------------------------------------------------------------------
# generate_github_annotation
# ---------------------------------------------------------------------------

class TestGitHubAnnotation:
    """Tests for GitHub Actions annotation format."""

    def test_critical_is_failure(self):
        f = _make_finding(severity=Severity.CRITICAL)
        ann = generate_github_annotation(f)
        assert ann["annotation_level"] == "failure"

    def test_high_is_failure(self):
        f = _make_finding(severity=Severity.HIGH)
        ann = generate_github_annotation(f)
        assert ann["annotation_level"] == "failure"

    def test_medium_is_warning(self):
        f = _make_finding(severity=Severity.MEDIUM)
        ann = generate_github_annotation(f)
        assert ann["annotation_level"] == "warning"

    def test_low_is_notice(self):
        f = _make_finding(severity=Severity.LOW)
        ann = generate_github_annotation(f)
        assert ann["annotation_level"] == "notice"

    def test_annotation_fields(self):
        f = _make_finding()
        ann = generate_github_annotation(f)
        assert ann["path"] == "server.py"
        assert ann["start_line"] == 42
        assert ann["end_line"] == 42
        assert "CMD-001" in ann["title"]
        assert ann["message"] == f.description

    def test_annotation_zero_line(self):
        f = _make_finding(line=0)
        ann = generate_github_annotation(f)
        assert ann["start_line"] == 1  # min 1


# ---------------------------------------------------------------------------
# generate_gitlab_sast
# ---------------------------------------------------------------------------

class TestGitLabSAST:
    """Tests for GitLab SAST format."""

    def test_basic_structure(self):
        findings = [_make_finding()]
        results = [_make_scan_result(findings)]
        report = generate_gitlab_sast(results)

        assert report["version"] == "15.0.0"
        assert "vulnerabilities" in report
        assert "scan" in report

    def test_vulnerability_fields(self):
        findings = [_make_finding()]
        results = [_make_scan_result(findings)]
        report = generate_gitlab_sast(results)

        vuln = report["vulnerabilities"][0]
        assert vuln["category"] == "sast"
        assert vuln["severity"] == "High"
        assert vuln["name"] == "Command Injection"
        assert vuln["location"]["file"] == "server.py"
        assert vuln["location"]["start_line"] == 42

    def test_cwe_identifier(self):
        findings = [_make_finding(cwe="CWE-78")]
        results = [_make_scan_result(findings)]
        report = generate_gitlab_sast(results)

        vuln = report["vulnerabilities"][0]
        cwe_ids = [i for i in vuln["identifiers"] if i["type"] == "cwe"]
        assert len(cwe_ids) == 1
        assert cwe_ids[0]["value"] == "78"

    def test_no_cwe(self):
        findings = [_make_finding(cwe="")]
        results = [_make_scan_result(findings)]
        report = generate_gitlab_sast(results)

        vuln = report["vulnerabilities"][0]
        cwe_ids = [i for i in vuln["identifiers"] if i["type"] == "cwe"]
        assert len(cwe_ids) == 0

    def test_scanner_info(self):
        findings = [_make_finding()]
        results = [_make_scan_result(findings)]
        report = generate_gitlab_sast(results)

        scanner = report["scan"]["scanner"]
        assert scanner["id"] == "mcpaudit"
        assert scanner["name"] == "mcpaudit"

    def test_solution_from_recommendation(self):
        findings = [_make_finding(recommendation="Use safe API")]
        results = [_make_scan_result(findings)]
        report = generate_gitlab_sast(results)

        vuln = report["vulnerabilities"][0]
        assert vuln["solution"] == "Use safe API"

    def test_severity_mapping(self):
        for sev, expected in [
            (Severity.CRITICAL, "Critical"),
            (Severity.HIGH, "High"),
            (Severity.MEDIUM, "Medium"),
            (Severity.LOW, "Low"),
            (Severity.INFO, "Info"),
        ]:
            findings = [_make_finding(severity=sev)]
            results = [_make_scan_result(findings)]
            report = generate_gitlab_sast(results)
            assert report["vulnerabilities"][0]["severity"] == expected

    def test_empty_results(self):
        report = generate_gitlab_sast([])
        assert report["vulnerabilities"] == []


# ---------------------------------------------------------------------------
# format_sarif_summary
# ---------------------------------------------------------------------------

class TestFormatSarifSummary:
    """Tests for SARIF summary formatting."""

    def test_basic_summary(self):
        findings = [_make_finding()]
        results = [_make_scan_result(findings)]
        sarif = generate_sarif_upload(results)
        summary = format_sarif_summary(sarif)

        assert "SARIF Report Summary" in summary
        assert "mcpaudit" in summary
        assert "1" in summary  # 1 finding

    def test_empty_summary(self):
        sarif = generate_sarif_upload([])
        summary = format_sarif_summary(sarif)
        assert "Findings: 0" in summary

    def test_level_counts(self):
        findings = [
            _make_finding(severity=Severity.HIGH, rule_id="A"),
            _make_finding(severity=Severity.MEDIUM, rule_id="B"),
        ]
        results = [_make_scan_result(findings)]
        sarif = generate_sarif_upload(results)
        summary = format_sarif_summary(sarif)
        assert "error" in summary
        assert "warning" in summary


# ---------------------------------------------------------------------------
# Export functions
# ---------------------------------------------------------------------------

class TestExportFunctions:
    """Tests for export functions."""

    def test_export_sarif_json(self):
        findings = [_make_finding()]
        results = [_make_scan_result(findings)]
        output = export_sarif_json(results)

        data = json.loads(output)
        assert data["version"] == SARIF_VERSION

    def test_export_github_annotations(self):
        findings = [_make_finding()]
        results = [_make_scan_result(findings)]
        output = export_github_annotations(results)

        data = json.loads(output)
        assert isinstance(data, list)
        assert len(data) == 1
        assert data[0]["path"] == "server.py"

    def test_export_gitlab_sast_json(self):
        findings = [_make_finding()]
        results = [_make_scan_result(findings)]
        output = export_gitlab_sast_json(results)

        data = json.loads(output)
        assert "vulnerabilities" in data


# ---------------------------------------------------------------------------
# CLI integration
# ---------------------------------------------------------------------------

class TestSarifCLI:
    """Tests for SARIF CLI subcommand."""

    def test_cli_github_format(self, tmp_path, capsys):
        f = tmp_path / "test.py"
        f.write_text('import subprocess\nsubprocess.call(input(), shell=True)\n')

        from mcpaudit.__main__ import main
        main(["sarif", str(f), "--format", "github"])
        captured = capsys.readouterr()
        data = json.loads(captured.out)
        assert data["version"] == SARIF_VERSION

    def test_cli_gitlab_format(self, tmp_path, capsys):
        f = tmp_path / "test.py"
        f.write_text('import subprocess\nsubprocess.call(input(), shell=True)\n')

        from mcpaudit.__main__ import main
        main(["sarif", str(f), "--format", "gitlab"])
        captured = capsys.readouterr()
        data = json.loads(captured.out)
        assert "vulnerabilities" in data

    def test_cli_output_file(self, tmp_path):
        f = tmp_path / "test.py"
        f.write_text('print("hello")\n')
        out = tmp_path / "report.sarif"

        from mcpaudit.__main__ import main
        main(["sarif", str(f), "--output", str(out)])
        assert out.exists()

    def test_cli_nonexistent_path(self):
        from mcpaudit.__main__ import main
        exit_code = main(["sarif", "/nonexistent/path"])
        assert exit_code == 1



# ---------------------------------------------------------------------------
# Additional SARIF structure tests
# ---------------------------------------------------------------------------

class TestSarifStructureAdditional:
    """Additional structural tests for SARIF generation."""

    def test_security_severity_in_result_properties(self):
        findings = [_make_finding(severity=Severity.CRITICAL)]
        results = [_make_scan_result(findings)]
        sarif = generate_sarif_upload(results)
        result = sarif["runs"][0]["results"][0]
        assert "security-severity" in result["properties"]
        assert result["properties"]["security-severity"] == "9.5"

    def test_confidence_in_result_properties(self):
        findings = [_make_finding(confidence="MEDIUM")]
        results = [_make_scan_result(findings)]
        sarif = generate_sarif_upload(results)
        result = sarif["runs"][0]["results"][0]
        assert result["properties"]["confidence"] == "MEDIUM"

    def test_rule_default_configuration_level(self):
        findings = [_make_finding(severity=Severity.MEDIUM)]
        results = [_make_scan_result(findings)]
        sarif = generate_sarif_upload(results)
        rule = sarif["runs"][0]["tool"]["driver"]["rules"][0]
        assert rule["defaultConfiguration"]["level"] == "warning"

    def test_rule_short_description(self):
        findings = [_make_finding(title="My Rule Title")]
        results = [_make_scan_result(findings)]
        sarif = generate_sarif_upload(results)
        rule = sarif["runs"][0]["tool"]["driver"]["rules"][0]
        assert rule["shortDescription"]["text"] == "My Rule Title"

    def test_rule_full_description(self):
        findings = [_make_finding(description="Detailed description of the issue")]
        results = [_make_scan_result(findings)]
        sarif = generate_sarif_upload(results)
        rule = sarif["runs"][0]["tool"]["driver"]["rules"][0]
        assert rule["fullDescription"]["text"] == "Detailed description of the issue"

    def test_rule_name_no_spaces(self):
        findings = [_make_finding(title="Command Injection Via Shell")]
        results = [_make_scan_result(findings)]
        sarif = generate_sarif_upload(results)
        rule = sarif["runs"][0]["tool"]["driver"]["rules"][0]
        assert " " not in rule["name"]
        assert rule["name"] == "CommandInjectionViaShell"

    def test_rule_security_tags(self):
        findings = [_make_finding()]
        results = [_make_scan_result(findings)]
        sarif = generate_sarif_upload(results)
        rule = sarif["runs"][0]["tool"]["driver"]["rules"][0]
        assert "security" in rule["properties"]["tags"]
        assert "mcp" in rule["properties"]["tags"]

    def test_rule_cwe_in_tags(self):
        findings = [_make_finding(cwe="CWE-78")]
        results = [_make_scan_result(findings)]
        sarif = generate_sarif_upload(results)
        rule = sarif["runs"][0]["tool"]["driver"]["rules"][0]
        assert "CWE-78" in rule["properties"]["tags"]

    def test_rule_no_cwe_no_relationships(self):
        findings = [_make_finding(cwe="")]
        results = [_make_scan_result(findings)]
        sarif = generate_sarif_upload(results)
        rule = sarif["runs"][0]["tool"]["driver"]["rules"][0]
        assert "relationships" not in rule

    def test_rule_no_recommendation_no_help(self):
        findings = [_make_finding(recommendation="")]
        results = [_make_scan_result(findings)]
        sarif = generate_sarif_upload(results)
        rule = sarif["runs"][0]["tool"]["driver"]["rules"][0]
        assert "help" not in rule

    def test_region_start_line_min_1(self):
        findings = [_make_finding(line=-5)]
        results = [_make_scan_result(findings)]
        sarif = generate_sarif_upload(results)
        loc = sarif["runs"][0]["results"][0]["locations"][0]["physicalLocation"]
        assert loc["region"]["startLine"] == 1

    def test_multiple_findings_multiple_rules(self):
        findings = [
            _make_finding(rule_id="CMD-001", severity=Severity.HIGH),
            _make_finding(rule_id="PATH-001", title="Path Traversal", severity=Severity.MEDIUM),
            _make_finding(rule_id="SSRF-001", title="SSRF", severity=Severity.CRITICAL),
        ]
        results = [_make_scan_result(findings)]
        sarif = generate_sarif_upload(results)
        rules = sarif["runs"][0]["tool"]["driver"]["rules"]
        assert len(rules) == 3
        results_list = sarif["runs"][0]["results"]
        assert len(results_list) == 3

    def test_result_rule_index_correct(self):
        findings = [
            _make_finding(rule_id="CMD-001", line=1),
            _make_finding(rule_id="PATH-001", title="Path Traversal", line=2),
            _make_finding(rule_id="CMD-001", line=3),  # Same rule, different line
        ]
        results = [_make_scan_result(findings)]
        sarif = generate_sarif_upload(results)
        sarif_results = sarif["runs"][0]["results"]
        # First CMD-001 should have ruleIndex 0
        assert sarif_results[0]["ruleIndex"] == 0
        # PATH-001 should have ruleIndex 1
        assert sarif_results[1]["ruleIndex"] == 1
        # Second CMD-001 should also have ruleIndex 0
        assert sarif_results[2]["ruleIndex"] == 0

    def test_driver_properties_tags(self):
        findings = [_make_finding()]
        results = [_make_scan_result(findings)]
        sarif = generate_sarif_upload(results)
        tags = sarif["runs"][0]["tool"]["driver"]["properties"]["tags"]
        assert "owasp-agentic" in tags

    def test_sarif_version_string(self):
        assert SARIF_VERSION == "2.1.0"

    def test_tool_name_constant(self):
        assert TOOL_NAME == "mcpaudit"


# ---------------------------------------------------------------------------
# Additional GitHub annotation tests
# ---------------------------------------------------------------------------

class TestGitHubAnnotationAdditional:
    """Additional tests for GitHub annotations."""

    def test_info_is_notice(self):
        f = _make_finding(severity=Severity.INFO)
        ann = generate_github_annotation(f)
        assert ann["annotation_level"] == "notice"

    def test_annotation_includes_rule_id_in_title(self):
        f = _make_finding(rule_id="SSRF-001", title="Server-Side Request Forgery")
        ann = generate_github_annotation(f)
        assert "SSRF-001" in ann["title"]
        assert "Server-Side Request Forgery" in ann["title"]

    def test_annotation_raw_details_empty_when_no_snippet(self):
        f = _make_finding(code_snippet="")
        ann = generate_github_annotation(f)
        assert ann["raw_details"] == ""

    def test_annotation_raw_details_has_snippet(self):
        f = _make_finding(code_snippet="subprocess.call(input)")
        ann = generate_github_annotation(f)
        assert ann["raw_details"] == "subprocess.call(input)"


# ---------------------------------------------------------------------------
# Additional GitLab SAST tests
# ---------------------------------------------------------------------------

class TestGitLabSASTAdditional:
    """Additional tests for GitLab SAST format."""

    def test_confidence_mapping(self):
        for conf, expected in [("HIGH", "High"), ("MEDIUM", "Medium"), ("LOW", "Low")]:
            findings = [_make_finding(confidence=conf)]
            results = [_make_scan_result(findings)]
            report = generate_gitlab_sast(results)
            assert report["vulnerabilities"][0]["confidence"] == expected

    def test_multiple_vulnerabilities(self):
        findings = [
            _make_finding(rule_id="CMD-001", line=1),
            _make_finding(rule_id="PATH-001", title="Path Traversal", line=10),
        ]
        results = [_make_scan_result(findings)]
        report = generate_gitlab_sast(results)
        assert len(report["vulnerabilities"]) == 2

    def test_vulnerability_id_is_fingerprint(self):
        findings = [_make_finding()]
        results = [_make_scan_result(findings)]
        report = generate_gitlab_sast(results)
        vuln_id = report["vulnerabilities"][0]["id"]
        expected_fp = _compute_fingerprint(findings[0])
        assert vuln_id == expected_fp

    def test_cwe_url_format(self):
        findings = [_make_finding(cwe="CWE-78")]
        results = [_make_scan_result(findings)]
        report = generate_gitlab_sast(results)
        vuln = report["vulnerabilities"][0]
        cwe_ids = [i for i in vuln["identifiers"] if i["type"] == "cwe"]
        assert cwe_ids[0]["url"] == "https://cwe.mitre.org/data/definitions/78.html"

    def test_scan_type_is_sast(self):
        report = generate_gitlab_sast([])
        assert report["scan"]["type"] == "sast"
        assert report["scan"]["status"] == "success"

    def test_no_solution_without_recommendation(self):
        findings = [_make_finding(recommendation="")]
        results = [_make_scan_result(findings)]
        report = generate_gitlab_sast(results)
        assert "solution" not in report["vulnerabilities"][0]

    def test_scanner_version_present(self):
        from mcpaudit import __version__
        findings = [_make_finding()]
        results = [_make_scan_result(findings)]
        report = generate_gitlab_sast(results)
        assert report["scan"]["scanner"]["version"] == __version__


# ---------------------------------------------------------------------------
# Additional export tests
# ---------------------------------------------------------------------------

class TestExportAdditional:
    """Additional tests for export functions."""

    def test_export_sarif_json_valid_json(self):
        findings = [_make_finding(), _make_finding(rule_id="PATH-001", title="Path", line=20)]
        results = [_make_scan_result(findings)]
        output = export_sarif_json(results)
        data = json.loads(output)
        assert len(data["runs"][0]["results"]) == 2

    def test_export_sarif_custom_tool_name(self):
        findings = [_make_finding()]
        results = [_make_scan_result(findings)]
        output = export_sarif_json(results, tool_name="my-tool")
        data = json.loads(output)
        assert data["runs"][0]["tool"]["driver"]["name"] == "my-tool"

    def test_export_github_annotations_multiple(self):
        findings = [_make_finding(line=1), _make_finding(line=5)]
        results = [_make_scan_result(findings)]
        output = export_github_annotations(results)
        data = json.loads(output)
        assert len(data) == 2

    def test_export_gitlab_multiple_files(self):
        results = [
            _make_scan_result([_make_finding(file="a.py")], file="a.py"),
            _make_scan_result([_make_finding(file="b.py")], file="b.py"),
        ]
        output = export_gitlab_sast_json(results)
        data = json.loads(output)
        assert len(data["vulnerabilities"]) == 2


# ---------------------------------------------------------------------------
# Additional SARIF summary tests
# ---------------------------------------------------------------------------

class TestSarifSummaryAdditional:
    """Additional tests for SARIF summary formatting."""

    def test_summary_contains_grade(self):
        findings = [_make_finding()]
        results = [_make_scan_result(findings)]
        sarif = generate_sarif_upload(results)
        summary = format_sarif_summary(sarif)
        assert "Grade:" in summary

    def test_summary_contains_files_scanned(self):
        findings = [_make_finding()]
        results = [_make_scan_result(findings)]
        sarif = generate_sarif_upload(results)
        summary = format_sarif_summary(sarif)
        assert "Files scanned:" in summary

    def test_summary_no_runs(self):
        sarif = {"runs": []}
        summary = format_sarif_summary(sarif)
        assert "SARIF Report Summary" in summary

    def test_summary_separator_lines(self):
        findings = [_make_finding()]
        results = [_make_scan_result(findings)]
        sarif = generate_sarif_upload(results)
        summary = format_sarif_summary(sarif)
        assert "=" * 60 in summary


# ---------------------------------------------------------------------------
# Additional CLI tests
# ---------------------------------------------------------------------------

class TestSarifCLIAdditional:
    """Additional SARIF CLI tests."""

    def test_cli_azure_format(self, tmp_path, capsys):
        f = tmp_path / "test.py"
        f.write_text('import subprocess\nsubprocess.call(input(), shell=True)\n')

        from mcpaudit.__main__ import main
        main(["sarif", str(f), "--format", "azure"])
        captured = capsys.readouterr()
        data = json.loads(captured.out)
        assert data["version"] == SARIF_VERSION

    def test_cli_clean_file_exit_code(self, tmp_path):
        f = tmp_path / "clean.py"
        f.write_text('print("hello")\n')

        from mcpaudit.__main__ import main
        exit_code = main(["sarif", str(f)])
        assert exit_code == 0

    def test_cli_output_file_content(self, tmp_path):
        f = tmp_path / "test.py"
        f.write_text('import subprocess\nsubprocess.call(input(), shell=True)\n')
        out = tmp_path / "report.json"

        from mcpaudit.__main__ import main
        main(["sarif", str(f), "--output", str(out)])
        content = out.read_text()
        data = json.loads(content)
        assert data["version"] == SARIF_VERSION
