"""Tests for MCP Security Audit — Auto-Fix Engine (mcpaudit.fixer).

Tests fix suggestion generation, diff formatting, fix application,
and integration with rules that implement suggest_fix().

NOTE: This test file contains intentionally vulnerable code STRINGS
for testing the fixer. These are not executed.
"""

from __future__ import annotations

import ast
import tempfile
from pathlib import Path

import pytest

from mcpaudit.fixer import (
    FixResult,
    FixSuggestion,
    apply_fixes,
    build_rule_map,
    format_fix_applied,
    format_fix_dry_run,
    suggest_fixes,
)
from mcpaudit.models import Finding, ScanResult, Severity
from mcpaudit.rules import Rule, get_all_rules
from mcpaudit.rules.excessive_perms import ExcessivePermissionsRule
from mcpaudit.rules.input_validation import MissingInputValidationRule
from mcpaudit.rules.missing_auth import MissingAuthRule
from mcpaudit.rules.path_traversal import PathTraversalRule


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

def _check(rule: 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)


# ===========================================================================
# FixSuggestion
# ===========================================================================

class TestFixSuggestion:
    """Tests for the FixSuggestion dataclass."""

    def test_basic_suggestion(self):
        finding = Finding(
            rule_id="PERM-001", severity=Severity.HIGH,
            title="Excessive permissions", description="777",
            file="test.py", line=1,
        )
        s = FixSuggestion(
            finding=finding,
            original_line="os.chmod(path, 0o777)",
            fixed_line="os.chmod(path, 0o755)",
            line_number=1,
        )
        assert s.line_number == 1
        assert s.original_line == "os.chmod(path, 0o777)"
        assert s.fixed_line == "os.chmod(path, 0o755)"

    def test_diff_output(self):
        finding = Finding(
            rule_id="PERM-001", severity=Severity.HIGH,
            title="test", description="test",
            file="test.py", line=1,
        )
        s = FixSuggestion(
            finding=finding,
            original_line="os.chmod(path, 0o777)",
            fixed_line="os.chmod(path, 0o755)",
            line_number=1,
        )
        diff = s.diff
        assert "---" in diff
        assert "+++" in diff
        assert "0o777" in diff
        assert "0o755" in diff


# ===========================================================================
# FixResult
# ===========================================================================

class TestFixResult:
    """Tests for the FixResult dataclass."""

    def test_empty_result(self):
        fr = FixResult(file="test.py")
        assert fr.fixable_count == 0
        assert fr.applied is False
        assert fr.error == ""

    def test_fixable_count(self):
        finding = Finding(
            rule_id="PERM-001", severity=Severity.HIGH,
            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),
                FixSuggestion(finding=finding, original_line="c", fixed_line="d", line_number=2),
            ],
        )
        assert fr.fixable_count == 2

    def test_diff_empty_suggestions(self):
        fr = FixResult(file="test.py")
        assert fr.diff == ""


# ===========================================================================
# build_rule_map
# ===========================================================================

class TestBuildRuleMap:
    """Tests for build_rule_map utility."""

    def test_map_contains_all_rules(self):
        rules = get_all_rules()
        rm = build_rule_map(rules)
        assert len(rm) == len(rules)

    def test_map_keys_are_rule_ids(self):
        rules = get_all_rules()
        rm = build_rule_map(rules)
        for rule in rules:
            assert rule.rule_id in rm
            assert rm[rule.rule_id] is rule

    def test_empty_rules(self):
        rm = build_rule_map([])
        assert len(rm) == 0


# ===========================================================================
# suggest_fixes — ExcessivePermissionsRule
# ===========================================================================

class TestSuggestFixExcessivePerms:
    """Tests for suggest_fix on PERM-001."""

    def test_chmod_777_fixed_to_755(self):
        source = "os.chmod(path, 0o777)\n"
        rule = ExcessivePermissionsRule()
        findings = _check(rule, source)
        assert len(findings) >= 1
        fix = rule.suggest_fix(source, findings[0])
        assert fix is not None
        assert "0o755" in fix
        assert "0o777" not in fix

    def test_chmod_644_no_fix(self):
        source = "os.chmod(path, 0o644)\n"
        rule = ExcessivePermissionsRule()
        findings = _check(rule, source)
        assert len(findings) == 0

    def test_suggest_fix_invalid_line(self):
        rule = ExcessivePermissionsRule()
        finding = Finding(
            rule_id="PERM-001", severity=Severity.HIGH,
            title="test", description="test",
            file="test.py", line=999,
        )
        fix = rule.suggest_fix("os.chmod(path, 0o777)\n", finding)
        assert fix is None

    def test_integrated_suggest_fixes(self, tmp_path: Path):
        source = "os.chmod(path, 0o777)\n"
        f = _write_temp(source)
        rule = ExcessivePermissionsRule()
        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


# ===========================================================================
# suggest_fixes — MissingAuthRule
# ===========================================================================

class TestSuggestFixMissingAuth:
    """Tests for suggest_fix on AUTH-001."""

    def test_server_without_auth_gets_todo(self):
        source = 'app = Server("test")\n'
        rule = MissingAuthRule()
        findings = _check(rule, source)
        assert len(findings) == 1
        fix = rule.suggest_fix(source, findings[0])
        assert fix is not None
        assert "TODO" in fix
        assert "auth" in fix.lower()

    def test_invalid_line_returns_none(self):
        rule = MissingAuthRule()
        finding = Finding(
            rule_id="AUTH-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


# ===========================================================================
# suggest_fixes — MissingInputValidationRule
# ===========================================================================

class TestSuggestFixInputValidation:
    """Tests for suggest_fix on VAL-001."""

    def test_handler_without_validation_gets_todo(self):
        source = 'async def call_tool(name, arguments):\n    result = process(arguments["query"])\n    return result\n'
        rule = MissingInputValidationRule()
        findings = _check(rule, source)
        assert len(findings) == 1
        fix = rule.suggest_fix(source, findings[0])
        assert fix is not None
        assert "TODO" in fix
        assert "isinstance" in fix

    def test_preserves_indentation(self):
        source = 'class S:\n    async def call_tool(self, name, arguments):\n        result = process(arguments["query"])\n        return result\n'
        rule = MissingInputValidationRule()
        findings = _check(rule, source)
        if findings:
            fix = rule.suggest_fix(source, findings[0])
            if fix is not None:
                # The fix line should preserve the indentation of the original line
                assert fix.startswith("    ")


# ===========================================================================
# suggest_fixes — PathTraversalRule
# ===========================================================================

class TestSuggestFixPathTraversal:
    """Tests for suggest_fix on PATH-001."""

    def test_open_with_user_input_gets_todo(self):
        source = 'def handle_read(path):\n    data = open(path).read()\n    return data\n'
        rule = PathTraversalRule()
        findings = _check(rule, source)
        assert len(findings) >= 1
        fix = rule.suggest_fix(source, findings[0])
        assert fix is not None
        assert "resolve" in fix.lower() or "TODO" in fix

    def test_invalid_line_returns_none(self):
        rule = PathTraversalRule()
        finding = Finding(
            rule_id="PATH-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


# ===========================================================================
# suggest_fixes — Integration
# ===========================================================================

class TestSuggestFixesIntegration:
    """Tests for the suggest_fixes() function."""

    def test_file_not_readable(self):
        finding = Finding(
            rule_id="PERM-001", severity=Severity.HIGH,
            title="test", description="test",
            file="/nonexistent/path.py", line=1,
        )
        rm = build_rule_map([ExcessivePermissionsRule()])
        result = suggest_fixes("/nonexistent/path.py", [finding], rm)
        assert result.error != ""
        assert result.fixable_count == 0

    def test_unknown_rule_id_skipped(self):
        source = "x = 1\n"
        f = _write_temp(source)
        finding = Finding(
            rule_id="UNKNOWN-999", severity=Severity.LOW,
            title="test", description="test",
            file=str(f), line=1,
        )
        rm = build_rule_map([ExcessivePermissionsRule()])
        result = suggest_fixes(str(f), [finding], rm)
        assert result.fixable_count == 0

    def test_rule_without_suggest_fix_skipped(self):
        """Rules that don't implement suggest_fix are skipped."""
        from mcpaudit.rules.redos import ReDosRule
        source = 'import re\nre.compile(r"(a+)+")\n'
        f = _write_temp(source)
        rule = ReDosRule()
        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 == 0

    def test_multiple_findings_multiple_fixes(self):
        source = "os.chmod(a, 0o777)\nos.chmod(b, 0o777)\n"
        f = _write_temp(source)
        rule = ExcessivePermissionsRule()
        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 == 2


# ===========================================================================
# apply_fixes
# ===========================================================================

class TestApplyFixes:
    """Tests for the apply_fixes() function."""

    def test_apply_single_fix(self):
        source = "os.chmod(path, 0o777)\n"
        f = _write_temp(source)
        finding = Finding(
            rule_id="PERM-001", severity=Severity.HIGH,
            title="test", description="test",
            file=str(f), line=1,
        )
        fr = FixResult(
            file=str(f),
            suggestions=[FixSuggestion(
                finding=finding,
                original_line="os.chmod(path, 0o777)",
                fixed_line="os.chmod(path, 0o755)",
                line_number=1,
            )],
        )
        ok = apply_fixes(fr)
        assert ok is True
        assert fr.applied is True
        content = Path(f).read_text()
        assert "0o755" in content
        assert "0o777" not in content

    def test_apply_multiple_fixes(self):
        source = "os.chmod(a, 0o777)\nx = 1\nos.chmod(b, 0o777)\n"
        f = _write_temp(source)
        finding1 = Finding(
            rule_id="PERM-001", severity=Severity.HIGH,
            title="test", description="test",
            file=str(f), line=1,
        )
        finding2 = Finding(
            rule_id="PERM-001", severity=Severity.HIGH,
            title="test", description="test",
            file=str(f), line=3,
        )
        fr = FixResult(
            file=str(f),
            suggestions=[
                FixSuggestion(finding=finding1, original_line="os.chmod(a, 0o777)", fixed_line="os.chmod(a, 0o755)", line_number=1),
                FixSuggestion(finding=finding2, original_line="os.chmod(b, 0o777)", fixed_line="os.chmod(b, 0o755)", line_number=3),
            ],
        )
        ok = apply_fixes(fr)
        assert ok is True
        content = Path(f).read_text()
        assert content.count("0o755") == 2

    def test_apply_no_suggestions(self):
        fr = FixResult(file="test.py")
        ok = apply_fixes(fr)
        assert ok is False

    def test_apply_nonexistent_file(self):
        finding = Finding(
            rule_id="PERM-001", severity=Severity.HIGH,
            title="test", description="test",
            file="/nonexistent.py", line=1,
        )
        fr = FixResult(
            file="/nonexistent.py",
            suggestions=[FixSuggestion(
                finding=finding,
                original_line="a",
                fixed_line="b",
                line_number=1,
            )],
        )
        ok = apply_fixes(fr)
        assert ok is False
        assert fr.error != ""


# ===========================================================================
# format_fix_dry_run
# ===========================================================================

class TestFormatFixDryRun:
    """Tests for the dry-run output formatter."""

    def test_empty_results(self):
        output = format_fix_dry_run([])
        assert "Auto-Fix Preview" in output
        assert "Fixable findings: 0" in output

    def test_with_suggestions(self):
        finding = Finding(
            rule_id="PERM-001", severity=Severity.HIGH,
            title="Excessive permissions", description="777",
            file="test.py", line=1,
        )
        fr = FixResult(
            file="test.py",
            suggestions=[FixSuggestion(
                finding=finding,
                original_line="os.chmod(path, 0o777)",
                fixed_line="os.chmod(path, 0o755)",
                line_number=1,
            )],
        )
        output = format_fix_dry_run([fr])
        assert "Fixable findings: 1" in output
        assert "PERM-001" in output
        assert "0o777" in output
        assert "0o755" in output

    def test_no_fixable_message(self):
        fr = FixResult(file="test.py")
        output = format_fix_dry_run([fr])
        assert "No auto-fixable findings" in output

    def test_multiple_files(self):
        finding1 = Finding(
            rule_id="PERM-001", severity=Severity.HIGH,
            title="test", description="test",
            file="a.py", line=1,
        )
        finding2 = Finding(
            rule_id="AUTH-001", severity=Severity.MEDIUM,
            title="test", description="test",
            file="b.py", line=1,
        )
        fr1 = FixResult(file="a.py", suggestions=[
            FixSuggestion(finding=finding1, original_line="a", fixed_line="b", line_number=1),
        ])
        fr2 = FixResult(file="b.py", suggestions=[
            FixSuggestion(finding=finding2, original_line="c", fixed_line="d", line_number=1),
        ])
        output = format_fix_dry_run([fr1, fr2])
        assert "Fixable findings: 2" in output
        assert "a.py" in output
        assert "b.py" in output


# ===========================================================================
# format_fix_applied
# ===========================================================================

class TestFormatFixApplied:
    """Tests for the applied-fix output formatter."""

    def test_empty_applied(self):
        output = format_fix_applied([])
        assert "Auto-Fix Applied" in output
        assert "Fixes applied: 0" in output

    def test_with_applied_fixes(self):
        finding = Finding(
            rule_id="PERM-001", severity=Severity.HIGH,
            title="Excessive permissions", description="777",
            file="test.py", line=1,
        )
        fr = FixResult(
            file="test.py",
            applied=True,
            suggestions=[FixSuggestion(
                finding=finding,
                original_line="os.chmod(path, 0o777)",
                fixed_line="os.chmod(path, 0o755)",
                line_number=1,
            )],
        )
        output = format_fix_applied([fr])
        assert "FIXED" in output
        assert "test.py" in output
        assert "PERM-001" in output

    def test_with_error(self):
        fr = FixResult(file="broken.py", error="Cannot read file")
        output = format_fix_applied([fr])
        assert "ERROR" in output
        assert "broken.py" in output


# ===========================================================================
# CLI Integration
# ===========================================================================

class TestFixCLI:
    """Tests for --fix and --fix-dry-run CLI flags."""

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

    def test_fix_dry_run_flag_registered(self):
        from mcpaudit.__main__ import build_parser
        parser = build_parser()
        args = parser.parse_args(["test.py", "--fix-dry-run"])
        assert args.fix_dry_run is True

    def test_fix_dry_run_output(self, tmp_path: Path, capsys):
        f = tmp_path / "test.py"
        f.write_text("os.chmod(path, 0o777)\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_changes(self, tmp_path: Path, capsys):
        f = tmp_path / "test.py"
        f.write_text("os.chmod(path, 0o777)\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 "0o755" in content

    def test_fix_dry_run_does_not_modify_file(self, tmp_path: Path, capsys):
        f = tmp_path / "test.py"
        original = "os.chmod(path, 0o777)\n"
        f.write_text(original)
        from mcpaudit.__main__ import main
        main([str(f), "--fix-dry-run"])
        content = f.read_text()
        assert content == original

    def test_fix_clean_file(self, tmp_path: Path, capsys):
        f = tmp_path / "test.py"
        f.write_text("x = 1\n")
        from mcpaudit.__main__ import main
        exit_code = main([str(f), "--fix-dry-run"])
        assert exit_code == 0
