"""Tests for --explain flag and extended DESER-001 patterns.

NOTE: This test file contains intentionally vulnerable code STRINGS
that are passed to the scanner for detection testing. These are not
executed -- they are static analysis targets.
"""

from __future__ import annotations

import ast

from mcpaudit.__main__ import main
from mcpaudit.models import Finding
from mcpaudit.rules import Rule, get_all_rules
from mcpaudit.rules.unsafe_deser import UnsafeDeserializationRule


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)


# ===========================================================================
# --explain flag
# ===========================================================================

class TestExplainFlag:
    """Tests for the --explain CLI option."""

    def test_explain_cmd001_shows_info(self, capsys):
        exit_code = main(["--explain", "CMD-001"])
        captured = capsys.readouterr()
        assert exit_code == 0
        assert "CMD-001" in captured.out
        assert "Command Injection" in captured.out
        assert "CRITICAL" in captured.out
        assert "CWE-78" in captured.out

    def test_explain_cmd001_shows_detailed_description(self, capsys):
        exit_code = main(["--explain", "CMD-001"])
        captured = capsys.readouterr()
        assert exit_code == 0
        assert "shell=True" in captured.out
        assert "How to fix" in captured.out

    def test_explain_ssrf001(self, capsys):
        exit_code = main(["--explain", "SSRF-001"])
        captured = capsys.readouterr()
        assert exit_code == 0
        assert "SSRF-001" in captured.out
        assert "CWE-918" in captured.out
        assert "How to fix" in captured.out

    def test_explain_sql001(self, capsys):
        exit_code = main(["--explain", "SQL-001"])
        captured = capsys.readouterr()
        assert exit_code == 0
        assert "SQL-001" in captured.out
        assert "CWE-89" in captured.out
        assert "parameterized" in captured.out

    def test_explain_invalid_rule_returns_error(self, capsys):
        exit_code = main(["--explain", "INVALID"])
        captured = capsys.readouterr()
        assert exit_code == 1
        assert "unknown rule" in captured.err

    def test_explain_case_insensitive(self, capsys):
        exit_code = main(["--explain", "cmd-001"])
        captured = capsys.readouterr()
        assert exit_code == 0
        assert "CMD-001" in captured.out

    def test_explain_rule_without_detailed_desc_falls_back(self, capsys):
        """Rules without a detailed_description override return self.description."""
        exit_code = main(["--explain", "PATH-001"])
        captured = capsys.readouterr()
        assert exit_code == 0
        assert "PATH-001" in captured.out

    def test_all_rules_have_detailed_description(self):
        """Every rule must return a non-empty detailed_description."""
        for rule in get_all_rules():
            assert rule.detailed_description, (
                f"{rule.rule_id} has empty detailed_description"
            )


# ===========================================================================
# DESER-001: pickle / marshal detection
# ===========================================================================

class TestDeserPickleMarshal:
    """Tests for pickle and marshal detection in DESER-001."""

    def test_pickle_loads_detected(self):
        # Scanner detection target -- not executed
        source = 'import pickle\ndata = pickle.loads(payload)\n'
        findings = _check(UnsafeDeserializationRule(), source)
        assert any(f.rule_id == "DESER-001" for f in findings)
        pkl_findings = [f for f in findings if "pickle" in f.description.lower()]
        assert len(pkl_findings) == 1
        assert pkl_findings[0].severity.value == "CRITICAL"

    def test_pickle_load_detected(self):
        source = 'import pickle\nobj = pickle.load(open("data.bin", "rb"))\n'
        findings = _check(UnsafeDeserializationRule(), source)
        assert any(
            f.rule_id == "DESER-001" and "pickle" in f.description.lower()
            for f in findings
        )

    def test_marshal_loads_detected(self):
        source = 'import marshal\ncode = marshal.loads(raw)\n'
        findings = _check(UnsafeDeserializationRule(), source)
        assert any(f.rule_id == "DESER-001" for f in findings)
        m_findings = [f for f in findings if "marshal" in f.description.lower()]
        assert len(m_findings) == 1
        assert m_findings[0].severity.value == "HIGH"

    def test_marshal_load_detected(self):
        source = 'import marshal\nobj = marshal.load(f)\n'
        findings = _check(UnsafeDeserializationRule(), source)
        assert any(
            f.rule_id == "DESER-001" and "marshal" in f.description.lower()
            for f in findings
        )

    def test_pickle_comment_not_flagged(self):
        source = '# pickle.loads(payload)\n'
        findings = _check(UnsafeDeserializationRule(), source)
        assert len(findings) == 0

    def test_json_loads_not_flagged(self):
        source = 'import json\ndata = json.loads(payload)\n'
        findings = _check(UnsafeDeserializationRule(), source)
        assert len(findings) == 0
