"""Tests for Enhanced Auto-Fix Engine -- v1.3.0 expanded rule coverage.

Tests suggest_fix() on 18 rules (was 5), fix statistics, and CLI
integration for --fix-stats and new fixable rules.

NOTE: This test file contains intentionally vulnerable code STRINGS
for testing the scanner and fixer. These strings reference unsafe
functions (pickle, yaml.load, etc.) as detection signatures -- they
are NOT executed. This is a SECURITY SCANNER test suite.
"""

from __future__ import annotations

import ast
import tempfile
from pathlib import Path

import pytest

from mcpaudit.fixer import (
    FixResult,
    FixStats,
    FixSuggestion,
    apply_fixes,
    build_rule_map,
    compute_fix_stats,
    format_fix_stats,
    get_fixable_rule_ids,
    suggest_fixes,
)
from mcpaudit.models import Finding, Severity
from mcpaudit.rules import get_all_rules


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

def _check(rule, source: str) -> list[Finding]:
    """Run a single rule against source code and return findings."""
    tree = ast.parse(source)
    return rule.check(tree, source)


def _write_temp(content: str, suffix: str = ".py") -> Path:
    """Write content to a temp file and return its path."""
    f = tempfile.NamedTemporaryFile(
        suffix=suffix, mode="w", delete=False, encoding="utf-8",
    )
    f.write(content)
    f.flush()
    f.close()
    return Path(f.name)


# ===========================================================================
# get_fixable_rule_ids
# ===========================================================================

class TestGetFixableRuleIds:
    """Tests for the get_fixable_rule_ids() utility."""

    def test_returns_list(self):
        ids = get_fixable_rule_ids()
        assert isinstance(ids, list)

    def test_at_least_18_fixable(self):
        ids = get_fixable_rule_ids()
        assert len(ids) >= 18

    def test_sorted(self):
        ids = get_fixable_rule_ids()
        assert ids == sorted(ids)

    def test_contains_core_rules(self):
        ids = get_fixable_rule_ids()
        for rule_id in [
            "AUTH-001", "CMD-001", "CMD-002", "CRYPTO-001",
            "DESER-001", "ERR-001", "FILE-001", "INFO-001",
            "LOG-001", "MCP-005", "MCP-006", "PATH-001",
            "PERM-001", "RES-001", "SEC-001", "SQL-001",
            "SSRF-001", "VAL-001",
        ]:
            assert rule_id in ids, f"{rule_id} not found in fixable rules"


# ===========================================================================
# CMD-001: shell=True -> shell=False
# ===========================================================================

class TestFixCMD001:
    """Tests for CMD-001 suggest_fix (shell=True)."""

    def test_basic_shell_true_fix(self):
        from mcpaudit.rules.command_injection import CommandInjectionShellRule
        source = "subprocess.run('ls', shell=True)\n"
        rule = CommandInjectionShellRule()
        findings = _check(rule, source)
        assert len(findings) >= 1
        fix = rule.suggest_fix(source, findings[0])
        assert fix is not None
        assert "shell=False" in fix
        assert "shell=True" not in fix

    def test_shell_true_with_other_args(self):
        from mcpaudit.rules.command_injection import CommandInjectionShellRule
        source = "subprocess.run(cmd, shell=True, timeout=30)\n"
        rule = CommandInjectionShellRule()
        findings = _check(rule, source)
        assert len(findings) >= 1
        fix = rule.suggest_fix(source, findings[0])
        assert "shell=False" in fix
        assert "timeout=30" in fix

    def test_invalid_line_returns_none(self):
        from mcpaudit.rules.command_injection import CommandInjectionShellRule
        rule = CommandInjectionShellRule()
        finding = Finding(
            rule_id="CMD-001", severity=Severity.CRITICAL,
            title="test", description="test",
            file="test.py", line=999,
        )
        fix = rule.suggest_fix("x = 1\n", finding)
        assert fix is None

    def test_no_shell_true_returns_none(self):
        from mcpaudit.rules.command_injection import CommandInjectionShellRule
        rule = CommandInjectionShellRule()
        finding = Finding(
            rule_id="CMD-001", severity=Severity.CRITICAL,
            title="test", description="test",
            file="test.py", line=1,
        )
        fix = rule.suggest_fix("x = 1\n", finding)
        assert fix is None

    def test_integrated_fix(self):
        from mcpaudit.rules.command_injection import CommandInjectionShellRule
        source = "subprocess.run('ls', shell=True)\n"
        f = _write_temp(source)
        rule = CommandInjectionShellRule()
        findings = _check(rule, source)
        for fi in findings:
            fi.file = str(f)
        rm = build_rule_map([rule])
        result = suggest_fixes(str(f), findings, rm)
        assert result.fixable_count >= 1


# ===========================================================================
# CMD-002: dynamic args
# ===========================================================================

class TestFixCMD002:
    """Tests for CMD-002 suggest_fix (dynamic args)."""

    def test_dynamic_fstring_fix(self):
        from mcpaudit.rules.command_injection import CommandInjectionDynamicRule
        source = 'subprocess.run(f"ls {path}")\n'
        rule = CommandInjectionDynamicRule()
        findings = _check(rule, source)
        assert len(findings) >= 1
        fix = rule.suggest_fix(source, findings[0])
        assert fix is not None
        assert "TODO" in fix

    def test_invalid_line_returns_none(self):
        from mcpaudit.rules.command_injection import CommandInjectionDynamicRule
        rule = CommandInjectionDynamicRule()
        finding = Finding(
            rule_id="CMD-002", severity=Severity.HIGH,
            title="test", description="test",
            file="test.py", line=999,
        )
        fix = rule.suggest_fix("x = 1\n", finding)
        assert fix is None


# ===========================================================================
# SQL-001: parameterized query
# ===========================================================================

class TestFixSQL001:
    """Tests for SQL-001 suggest_fix (SQL injection)."""

    def test_basic_sql_injection_fix(self):
        from mcpaudit.rules.sql_injection import SqlInjectionRule
        source = 'cursor.execute(f"SELECT * FROM users WHERE id={uid}")\n'
        rule = SqlInjectionRule()
        findings = _check(rule, source)
        assert len(findings) >= 1
        fix = rule.suggest_fix(source, findings[0])
        assert fix is not None
        assert "parameterized" in fix.lower() or "TODO" in fix

    def test_preserves_indentation(self):
        from mcpaudit.rules.sql_injection import SqlInjectionRule
        source = 'def query():\n    cursor.execute(f"DELETE FROM t WHERE id={x}")\n'
        rule = SqlInjectionRule()
        findings = _check(rule, source)
        assert len(findings) >= 1
        fix = rule.suggest_fix(source, findings[0])
        assert fix is not None
        assert fix.startswith("    ")

    def test_invalid_line_returns_none(self):
        from mcpaudit.rules.sql_injection import SqlInjectionRule
        rule = SqlInjectionRule()
        finding = Finding(
            rule_id="SQL-001", severity=Severity.CRITICAL,
            title="test", description="test",
            file="test.py", line=999,
        )
        fix = rule.suggest_fix("x = 1\n", finding)
        assert fix is None


# ===========================================================================
# DESER-001: unsafe deserialization
# Detection signatures for security scanner -- not actual usage
# ===========================================================================

class TestFixDESER001:
    """Tests for DESER-001 suggest_fix (unsafe deserialization)."""

    def test_yaml_load_to_safe_load(self):
        from mcpaudit.rules.unsafe_deser import UnsafeDeserializationRule
        source = "data = yaml.load(f)\n"
        rule = UnsafeDeserializationRule()
        findings = _check(rule, source)
        assert len(findings) >= 1
        fix = rule.suggest_fix(source, findings[0])
        assert fix is not None
        assert "safe_load" in fix

    def test_invalid_line_returns_none(self):
        from mcpaudit.rules.unsafe_deser import UnsafeDeserializationRule
        rule = UnsafeDeserializationRule()
        finding = Finding(
            rule_id="DESER-001", severity=Severity.CRITICAL,
            title="test", description="test",
            file="test.py", line=999,
        )
        fix = rule.suggest_fix("x = 1\n", finding)
        assert fix is None


# ===========================================================================
# ERR-001: unhandled tool errors
# ===========================================================================

class TestFixERR001:
    """Tests for ERR-001 suggest_fix (error handling)."""

    def test_tool_handler_fix(self):
        from mcpaudit.rules.error_handling import UnhandledToolErrorRule
        source = "async def call_tool(name, args):\n    return process(args)\n"
        rule = UnhandledToolErrorRule()
        findings = _check(rule, source)
        assert len(findings) >= 1
        fix = rule.suggest_fix(source, findings[0])
        assert fix is not None
        assert "try/except" in fix or "TODO" in fix

    def test_preserves_indentation(self):
        from mcpaudit.rules.error_handling import UnhandledToolErrorRule
        source = "class S:\n    async def handle_tool(self, name, args):\n        return process(args)\n"
        rule = UnhandledToolErrorRule()
        findings = _check(rule, source)
        if findings:
            fix = rule.suggest_fix(source, findings[0])
            if fix is not None:
                assert fix.startswith("    ")

    def test_invalid_line_returns_none(self):
        from mcpaudit.rules.error_handling import UnhandledToolErrorRule
        rule = UnhandledToolErrorRule()
        finding = Finding(
            rule_id="ERR-001", severity=Severity.MEDIUM,
            title="test", description="test",
            file="test.py", line=999,
        )
        fix = rule.suggest_fix("x = 1\n", finding)
        assert fix is None


# ===========================================================================
# SSRF-001: URL allowlist
# ===========================================================================

class TestFixSSRF001:
    """Tests for SSRF-001 suggest_fix (SSRF)."""

    def test_requests_get_fix(self):
        from mcpaudit.rules.ssrf import SSRFRule
        source = "response = requests.get(user_url)\n"
        rule = SSRFRule()
        findings = _check(rule, source)
        assert len(findings) >= 1
        fix = rule.suggest_fix(source, findings[0])
        assert fix is not None
        assert "allowlist" in fix.lower() or "TODO" in fix

    def test_invalid_line_returns_none(self):
        from mcpaudit.rules.ssrf import SSRFRule
        rule = SSRFRule()
        finding = Finding(
            rule_id="SSRF-001", severity=Severity.HIGH,
            title="test", description="test",
            file="test.py", line=999,
        )
        fix = rule.suggest_fix("x = 1\n", finding)
        assert fix is None


# ===========================================================================
# LOG-001: sensitive data in logs
# ===========================================================================

class TestFixLOG001:
    """Tests for LOG-001 suggest_fix (sensitive logging)."""

    def test_logging_sensitive_fix(self):
        from mcpaudit.rules.sensitive_logging import SensitiveLoggingRule
        source = "logger.info(f'User: {password}')\n"
        rule = SensitiveLoggingRule()
        findings = _check(rule, source)
        assert len(findings) >= 1
        fix = rule.suggest_fix(source, findings[0])
        assert fix is not None
        assert "Redact" in fix or "TODO" in fix

    def test_invalid_line_returns_none(self):
        from mcpaudit.rules.sensitive_logging import SensitiveLoggingRule
        rule = SensitiveLoggingRule()
        finding = Finding(
            rule_id="LOG-001", severity=Severity.MEDIUM,
            title="test", description="test",
            file="test.py", line=999,
        )
        fix = rule.suggest_fix("x = 1\n", finding)
        assert fix is None


# ===========================================================================
# FILE-001: unsafe file write
# ===========================================================================

class TestFixFILE001:
    """Tests for FILE-001 suggest_fix (unsafe file write)."""

    def test_open_write_fix(self):
        from mcpaudit.rules.unsafe_file_write import UnsafeFileWriteRule
        source = "f = open(user_path, 'w')\n"
        rule = UnsafeFileWriteRule()
        findings = _check(rule, source)
        assert len(findings) >= 1
        fix = rule.suggest_fix(source, findings[0])
        assert fix is not None
        assert "Validate" in fix or "realpath" in fix or "TODO" in fix

    def test_invalid_line_returns_none(self):
        from mcpaudit.rules.unsafe_file_write import UnsafeFileWriteRule
        rule = UnsafeFileWriteRule()
        finding = Finding(
            rule_id="FILE-001", severity=Severity.HIGH,
            title="test", description="test",
            file="test.py", line=999,
        )
        fix = rule.suggest_fix("x = 1\n", finding)
        assert fix is None


# ===========================================================================
# RES-001: resource exhaustion (timeout)
# ===========================================================================

class TestFixRES001:
    """Tests for RES-001 suggest_fix (resource exhaustion)."""

    def test_requests_without_timeout_fix(self):
        from mcpaudit.rules.resource_exhaustion import ResourceExhaustionRule
        source = "r = requests.get(url)\n"
        rule = ResourceExhaustionRule()
        findings = _check(rule, source)
        assert len(findings) >= 1
        fix = rule.suggest_fix(source, findings[0])
        assert fix is not None
        assert "timeout" in fix.lower()

    def test_timeout_inserted_before_paren(self):
        from mcpaudit.rules.resource_exhaustion import ResourceExhaustionRule
        source = "r = requests.get(url)\n"
        rule = ResourceExhaustionRule()
        findings = _check(rule, source)
        assert len(findings) >= 1
        fix = rule.suggest_fix(source, findings[0])
        assert "timeout=30" in fix

    def test_empty_call_adds_timeout(self):
        from mcpaudit.rules.resource_exhaustion import ResourceExhaustionRule
        rule = ResourceExhaustionRule()
        finding = Finding(
            rule_id="RES-001", severity=Severity.MEDIUM,
            title="test", description="test",
            file="test.py", line=1,
        )
        source = "data = f.read()\n"
        fix = rule.suggest_fix(source, finding)
        assert fix is not None
        assert "timeout" in fix.lower()

    def test_invalid_line_returns_none(self):
        from mcpaudit.rules.resource_exhaustion import ResourceExhaustionRule
        rule = ResourceExhaustionRule()
        finding = Finding(
            rule_id="RES-001", severity=Severity.MEDIUM,
            title="test", description="test",
            file="test.py", line=999,
        )
        fix = rule.suggest_fix("x = 1\n", finding)
        assert fix is None


# ===========================================================================
# INFO-001: information disclosure
# ===========================================================================

class TestFixINFO001:
    """Tests for INFO-001 suggest_fix (information disclosure)."""

    def test_info_disclosure_fix(self):
        from mcpaudit.rules.info_disclosure import InformationDisclosureRule
        rule = InformationDisclosureRule()
        finding = Finding(
            rule_id="INFO-001", severity=Severity.LOW,
            title="test", description="test",
            file="test.py", line=1,
        )
        source = "except Exception as e:\n"
        fix = rule.suggest_fix(source, finding)
        assert fix is not None
        assert "generic" in fix.lower() or "TODO" in fix

    def test_invalid_line_returns_none(self):
        from mcpaudit.rules.info_disclosure import InformationDisclosureRule
        rule = InformationDisclosureRule()
        finding = Finding(
            rule_id="INFO-001", severity=Severity.LOW,
            title="test", description="test",
            file="test.py", line=999,
        )
        fix = rule.suggest_fix("x = 1\n", finding)
        assert fix is None


# ===========================================================================
# MCP-005: tool input validation
# ===========================================================================

class TestFixMCP005:
    """Tests for MCP-005 suggest_fix (input validation)."""

    def test_tool_handler_fix(self):
        from mcpaudit.rules.mcp_rules import MCPToolInputValidationRule
        source = (
            "async def call_tool(name, arguments):\n"
            "    result = process(arguments['query'])\n"
            "    return result\n"
        )
        rule = MCPToolInputValidationRule()
        findings = _check(rule, source)
        assert len(findings) >= 1
        fix = rule.suggest_fix(source, findings[0])
        assert fix is not None
        assert "isinstance" in fix or "TODO" in fix

    def test_invalid_line_returns_none(self):
        from mcpaudit.rules.mcp_rules import MCPToolInputValidationRule
        rule = MCPToolInputValidationRule()
        finding = Finding(
            rule_id="MCP-005", severity=Severity.HIGH,
            title="test", description="test",
            file="test.py", line=999,
        )
        fix = rule.suggest_fix("x = 1\n", finding)
        assert fix is None


# ===========================================================================
# MCP-006: response size limit
# ===========================================================================

class TestFixMCP006:
    """Tests for MCP-006 suggest_fix (response size)."""

    def test_response_size_fix(self):
        from mcpaudit.rules.mcp_rules import MCPToolResponseSizeRule
        source = (
            "async def call_tool(name, arguments):\n"
            "    data = f.read()\n"
            "    return data\n"
        )
        rule = MCPToolResponseSizeRule()
        findings = _check(rule, source)
        assert len(findings) >= 1
        fix = rule.suggest_fix(source, findings[0])
        assert fix is not None
        assert "MAX" in fix or "truncat" in fix.lower() or "TODO" in fix

    def test_invalid_line_returns_none(self):
        from mcpaudit.rules.mcp_rules import MCPToolResponseSizeRule
        rule = MCPToolResponseSizeRule()
        finding = Finding(
            rule_id="MCP-006", severity=Severity.MEDIUM,
            title="test", description="test",
            file="test.py", line=999,
        )
        fix = rule.suggest_fix("x = 1\n", finding)
        assert fix is None


# ===========================================================================
# CRYPTO-001: weak hash
# ===========================================================================

class TestFixCRYPTO001:
    """Tests for CRYPTO-001 rule and suggest_fix (weak hash)."""

    def test_detect_md5(self):
        from mcpaudit.rules.weak_crypto import WeakCryptoHashRule
        source = "h = hashlib.md5(data)\n"
        rule = WeakCryptoHashRule()
        findings = _check(rule, source)
        assert len(findings) == 1
        assert findings[0].rule_id == "CRYPTO-001"
        assert "MD5" in findings[0].title

    def test_detect_sha1(self):
        from mcpaudit.rules.weak_crypto import WeakCryptoHashRule
        source = "h = hashlib.sha1(data)\n"
        rule = WeakCryptoHashRule()
        findings = _check(rule, source)
        assert len(findings) == 1
        assert "SHA1" in findings[0].title

    def test_sha256_no_finding(self):
        from mcpaudit.rules.weak_crypto import WeakCryptoHashRule
        source = "h = hashlib.sha256(data)\n"
        rule = WeakCryptoHashRule()
        findings = _check(rule, source)
        assert len(findings) == 0

    def test_comment_skipped(self):
        from mcpaudit.rules.weak_crypto import WeakCryptoHashRule
        source = "# hashlib.md5 is bad\n"
        rule = WeakCryptoHashRule()
        findings = _check(rule, source)
        assert len(findings) == 0

    def test_fix_md5_to_sha256(self):
        from mcpaudit.rules.weak_crypto import WeakCryptoHashRule
        source = "h = hashlib.md5(data)\n"
        rule = WeakCryptoHashRule()
        findings = _check(rule, source)
        assert len(findings) >= 1
        fix = rule.suggest_fix(source, findings[0])
        assert fix is not None
        assert "sha256" in fix
        assert "md5" not in fix

    def test_fix_sha1_to_sha256(self):
        from mcpaudit.rules.weak_crypto import WeakCryptoHashRule
        source = "h = hashlib.sha1(data)\n"
        rule = WeakCryptoHashRule()
        findings = _check(rule, source)
        assert len(findings) >= 1
        fix = rule.suggest_fix(source, findings[0])
        assert fix is not None
        assert "sha256" in fix
        assert "sha1" not in fix

    def test_fix_invalid_line_returns_none(self):
        from mcpaudit.rules.weak_crypto import WeakCryptoHashRule
        rule = WeakCryptoHashRule()
        finding = Finding(
            rule_id="CRYPTO-001", severity=Severity.MEDIUM,
            title="test", description="test",
            file="test.py", line=999,
        )
        fix = rule.suggest_fix("x = 1\n", finding)
        assert fix is None

    def test_rule_properties(self):
        from mcpaudit.rules.weak_crypto import WeakCryptoHashRule
        rule = WeakCryptoHashRule()
        assert rule.rule_id == "CRYPTO-001"
        assert rule.name == "Weak Cryptographic Hash"
        assert rule.severity == Severity.MEDIUM
        assert rule.cwe == "CWE-328"

    def test_multiple_findings(self):
        from mcpaudit.rules.weak_crypto import WeakCryptoHashRule
        source = "a = hashlib.md5(x)\nb = hashlib.sha1(y)\n"
        rule = WeakCryptoHashRule()
        findings = _check(rule, source)
        assert len(findings) == 2

    def test_detailed_description(self):
        from mcpaudit.rules.weak_crypto import WeakCryptoHashRule
        rule = WeakCryptoHashRule()
        desc = rule.detailed_description
        assert "MD5" in desc
        assert "SHA1" in desc
        assert "sha256" in desc.lower()


# ===========================================================================
# FixStats
# ===========================================================================

class TestFixStats:
    """Tests for FixStats dataclass."""

    def test_empty_stats(self):
        stats = FixStats()
        assert stats.total_findings == 0
        assert stats.fixable_findings == 0
        assert stats.coverage_pct == 0.0
        assert stats.success_pct == 0.0

    def test_coverage_calculation(self):
        stats = FixStats(total_findings=100, fixable_findings=50)
        assert stats.coverage_pct == 50.0

    def test_success_calculation(self):
        stats = FixStats(fixable_findings=10, applied_fixes=8)
        assert stats.success_pct == 80.0

    def test_to_dict(self):
        stats = FixStats(
            total_findings=10,
            fixable_findings=5,
            applied_fixes=3,
            by_rule={"CMD-001": 2, "SQL-001": 1},
        )
        d = stats.to_dict()
        assert d["total_findings"] == 10
        assert d["fixable_findings"] == 5
        assert d["applied_fixes"] == 3
        assert d["coverage_pct"] == 50.0
        assert d["by_rule"]["CMD-001"] == 2


# ===========================================================================
# compute_fix_stats
# ===========================================================================

class TestComputeFixStats:
    """Tests for compute_fix_stats()."""

    def test_empty_results(self):
        stats = compute_fix_stats([], total_findings=0)
        assert stats.total_findings == 0
        assert stats.fixable_findings == 0

    def test_with_suggestions(self):
        finding = Finding(
            rule_id="CMD-001", severity=Severity.CRITICAL,
            title="test", description="test",
            file="test.py", line=1,
        )
        fr = FixResult(
            file="test.py",
            suggestions=[
                FixSuggestion(
                    finding=finding,
                    original_line="a",
                    fixed_line="b",
                    line_number=1,
                ),
            ],
        )
        stats = compute_fix_stats([fr], total_findings=5)
        assert stats.total_findings == 5
        assert stats.fixable_findings == 1
        assert stats.by_rule["CMD-001"] == 1

    def test_applied_fixes(self):
        finding = Finding(
            rule_id="SQL-001", severity=Severity.CRITICAL,
            title="test", description="test",
            file="test.py", line=1,
        )
        fr = FixResult(
            file="test.py",
            applied=True,
            suggestions=[
                FixSuggestion(
                    finding=finding,
                    original_line="a",
                    fixed_line="b",
                    line_number=1,
                ),
            ],
        )
        stats = compute_fix_stats([fr], total_findings=1)
        assert stats.applied_fixes == 1
        assert stats.files_modified == ["test.py"]
        assert stats.success_pct == 100.0

    def test_with_errors(self):
        fr = FixResult(file="broken.py", error="Cannot read file")
        stats = compute_fix_stats([fr], total_findings=0)
        assert stats.errors == 1

    def test_multiple_files(self):
        finding1 = Finding(
            rule_id="CMD-001", severity=Severity.CRITICAL,
            title="test", description="test",
            file="a.py", line=1,
        )
        finding2 = Finding(
            rule_id="SQL-001", severity=Severity.CRITICAL,
            title="test", description="test",
            file="b.py", line=1,
        )
        fr1 = FixResult(
            file="a.py",
            applied=True,
            suggestions=[FixSuggestion(
                finding=finding1, original_line="a", fixed_line="b", line_number=1,
            )],
        )
        fr2 = FixResult(
            file="b.py",
            applied=True,
            suggestions=[FixSuggestion(
                finding=finding2, original_line="c", fixed_line="d", line_number=1,
            )],
        )
        stats = compute_fix_stats([fr1, fr2], total_findings=10)
        assert stats.fixable_findings == 2
        assert stats.applied_fixes == 2
        assert len(stats.files_modified) == 2
        assert stats.by_rule == {"CMD-001": 1, "SQL-001": 1}


# ===========================================================================
# format_fix_stats
# ===========================================================================

class TestFormatFixStats:
    """Tests for format_fix_stats()."""

    def test_empty_stats(self):
        stats = FixStats()
        output = format_fix_stats(stats)
        assert "Auto-Fix Statistics" in output
        assert "0" in output

    def test_with_data(self):
        stats = FixStats(
            total_findings=20,
            fixable_findings=10,
            applied_fixes=8,
            by_rule={"CMD-001": 5, "SQL-001": 3},
        )
        output = format_fix_stats(stats)
        assert "Total findings:    20" in output
        assert "Fixable findings:  10" in output
        assert "CMD-001" in output
        assert "SQL-001" in output

    def test_shows_errors(self):
        stats = FixStats(errors=2)
        output = format_fix_stats(stats)
        assert "Errors:            2" in output


# ===========================================================================
# End-to-end fix integration
# ===========================================================================

class TestEndToEndFix:
    """End-to-end tests for the enhanced fix engine."""

    def test_fix_crypto_md5_file(self):
        source = "import hashlib\nh = hashlib.md5(data)\n"
        f = _write_temp(source)
        from mcpaudit.rules.weak_crypto import WeakCryptoHashRule
        rule = WeakCryptoHashRule()
        findings = _check(rule, source)
        for fi in findings:
            fi.file = str(f)
        rm = build_rule_map([rule])
        result = suggest_fixes(str(f), findings, rm)
        assert result.fixable_count >= 1
        ok = apply_fixes(result)
        assert ok is True
        content = Path(f).read_text()
        assert "sha256" in content

    def test_fix_yaml_load_file(self):
        source = "import yaml\ndata = yaml.load(f)\n"
        f = _write_temp(source)
        from mcpaudit.rules.unsafe_deser import UnsafeDeserializationRule
        rule = UnsafeDeserializationRule()
        findings = _check(rule, source)
        for fi in findings:
            fi.file = str(f)
        rm = build_rule_map([rule])
        result = suggest_fixes(str(f), findings, rm)
        assert result.fixable_count >= 1
        ok = apply_fixes(result)
        assert ok is True
        content = Path(f).read_text()
        assert "safe_load" in content

    def test_fix_shell_true_file(self):
        source = "import subprocess\nsubprocess.run('ls', shell=True)\n"
        f = _write_temp(source)
        from mcpaudit.rules.command_injection import CommandInjectionShellRule
        rule = CommandInjectionShellRule()
        findings = _check(rule, source)
        for fi in findings:
            fi.file = str(f)
        rm = build_rule_map([rule])
        result = suggest_fixes(str(f), findings, rm)
        assert result.fixable_count >= 1
        ok = apply_fixes(result)
        assert ok is True
        content = Path(f).read_text()
        assert "shell=False" in content

    def test_stats_from_multi_rule_scan(self):
        """Test statistics from scanning with multiple rules."""
        source = (
            "import hashlib\n"
            "h = hashlib.md5(data)\n"
            "import subprocess\n"
            "subprocess.run('ls', shell=True)\n"
        )
        f = _write_temp(source)
        rules = get_all_rules()
        rm = build_rule_map(rules)

        from mcpaudit.scanner import MCPSecurityScanner
        scanner = MCPSecurityScanner(rules=rules)
        scan_result = scanner.scan_file(Path(f))

        for fi in scan_result.findings:
            fi.file = str(f)

        result = suggest_fixes(str(f), scan_result.findings, rm)
        stats = compute_fix_stats([result], total_findings=len(scan_result.findings))
        assert stats.fixable_findings >= 2
        assert stats.coverage_pct > 0


# ===========================================================================
# CLI integration
# ===========================================================================

class TestFixCLIEnhanced:
    """Tests for enhanced CLI fix flags."""

    def test_fix_stats_flag_registered(self):
        from mcpaudit.__main__ import build_parser
        parser = build_parser()
        args = parser.parse_args(["test.py", "--fix-stats"])
        assert args.fix_stats is True

    def test_fix_stats_output(self, tmp_path: Path, capsys):
        f = tmp_path / "test.py"
        f.write_text("import hashlib\nh = hashlib.md5(data)\n")
        from mcpaudit.__main__ import main
        exit_code = main([str(f), "--fix-stats"])
        captured = capsys.readouterr()
        assert "Auto-Fix Statistics" in captured.out

    def test_fix_dry_run_crypto(self, tmp_path: Path, capsys):
        f = tmp_path / "test.py"
        f.write_text("import hashlib\nh = hashlib.md5(data)\n")
        from mcpaudit.__main__ import main
        exit_code = main([str(f), "--fix-dry-run"])
        captured = capsys.readouterr()
        assert "Auto-Fix Preview" in captured.out

    def test_fix_applies_crypto(self, tmp_path: Path, capsys):
        f = tmp_path / "test.py"
        f.write_text("import hashlib\nh = hashlib.md5(data)\n")
        from mcpaudit.__main__ import main
        exit_code = main([str(f), "--fix"])
        captured = capsys.readouterr()
        assert "Auto-Fix Applied" in captured.out
        content = f.read_text()
        assert "sha256" in content
