"""Tests for mcpaudit.plugins — Plugin system.

Covers: PluginRule, PluginRegistry, discover_plugins, load_plugin_file,
validate_plugin, dict-to-rule conversion, error handling.
"""

from __future__ import annotations

import ast
import textwrap
from pathlib import Path

import pytest

from mcpaudit.models import Finding, Severity
from mcpaudit.plugins import (
    PluginInfo,
    PluginRegistry,
    PluginRule,
    _dict_to_plugin_rule,
    discover_plugins,
    load_plugin_file,
    validate_plugin,
)
from mcpaudit.rules import Rule


# --------------- PluginRule ---------------

class TestPluginRule:
    """Tests for the PluginRule wrapper class."""

    def _make_check_fn(self):
        def check_fn(tree, source):
            return [
                Finding(
                    rule_id="PLUG-TEST",
                    severity=Severity.LOW,
                    title="Test finding",
                    description="test",
                    file="",
                    line=1,
                )
            ]
        return check_fn

    def test_plugin_rule_is_rule_subclass(self):
        rule = PluginRule(
            rule_id="PLUG-001",
            name="Test",
            severity=Severity.LOW,
            description="desc",
            check_fn=lambda t, s: [],
        )
        assert isinstance(rule, Rule)

    def test_plugin_rule_properties(self):
        rule = PluginRule(
            rule_id="PLUG-001",
            name="My Plugin",
            severity=Severity.HIGH,
            description="Detects stuff",
            check_fn=lambda t, s: [],
            cwe_id="CWE-79",
            detailed_desc="More detail",
        )
        assert rule.rule_id == "PLUG-001"
        assert rule.name == "My Plugin"
        assert rule.severity == Severity.HIGH
        assert rule.description == "Detects stuff"
        assert rule.cwe == "CWE-79"
        assert rule.detailed_description == "More detail"

    def test_plugin_rule_default_detailed_description(self):
        rule = PluginRule(
            rule_id="PLUG-001",
            name="T",
            severity=Severity.LOW,
            description="short desc",
            check_fn=lambda t, s: [],
        )
        assert rule.detailed_description == "short desc"

    def test_plugin_rule_check_delegates(self):
        fn = self._make_check_fn()
        rule = PluginRule(
            rule_id="PLUG-TEST",
            name="T",
            severity=Severity.LOW,
            description="d",
            check_fn=fn,
        )
        tree = ast.parse("x = 1")
        findings = rule.check(tree, "x = 1")
        assert len(findings) == 1
        assert findings[0].rule_id == "PLUG-TEST"

    def test_plugin_rule_check_empty(self):
        rule = PluginRule(
            rule_id="PLUG-E",
            name="Empty",
            severity=Severity.INFO,
            description="d",
            check_fn=lambda t, s: [],
        )
        assert rule.check(ast.parse(""), "") == []

    def test_plugin_rule_cwe_default_empty(self):
        rule = PluginRule(
            rule_id="PLUG-001",
            name="T",
            severity=Severity.LOW,
            description="d",
            check_fn=lambda t, s: [],
        )
        assert rule.cwe == ""


# --------------- validate_plugin ---------------

class TestValidatePlugin:
    """Tests for validate_plugin()."""

    def test_valid_plugin(self):
        rule = PluginRule(
            rule_id="PLUG-001",
            name="Valid",
            severity=Severity.LOW,
            description="desc",
            check_fn=lambda t, s: [],
        )
        errors = validate_plugin(rule)
        assert errors == []

    def test_empty_rule_id(self):
        rule = PluginRule(
            rule_id="",
            name="Bad",
            severity=Severity.LOW,
            description="d",
            check_fn=lambda t, s: [],
        )
        errors = validate_plugin(rule)
        assert any("rule_id is empty" in e for e in errors)

    def test_wrong_prefix(self):
        rule = PluginRule(
            rule_id="CUSTOM-001",
            name="Bad",
            severity=Severity.LOW,
            description="d",
            check_fn=lambda t, s: [],
        )
        errors = validate_plugin(rule)
        assert any("PLUG-" in e for e in errors)

    def test_empty_name(self):
        rule = PluginRule(
            rule_id="PLUG-001",
            name="",
            severity=Severity.LOW,
            description="d",
            check_fn=lambda t, s: [],
        )
        errors = validate_plugin(rule)
        assert any("name is empty" in e for e in errors)

    def test_empty_description(self):
        rule = PluginRule(
            rule_id="PLUG-001",
            name="T",
            severity=Severity.LOW,
            description="",
            check_fn=lambda t, s: [],
        )
        errors = validate_plugin(rule)
        assert any("description is empty" in e for e in errors)

    def test_multiple_errors(self):
        rule = PluginRule(
            rule_id="",
            name="",
            severity=Severity.LOW,
            description="",
            check_fn=lambda t, s: [],
        )
        errors = validate_plugin(rule)
        assert len(errors) >= 3


# --------------- _dict_to_plugin_rule ---------------

class TestDictToPluginRule:
    """Tests for _dict_to_plugin_rule()."""

    def test_valid_dict(self):
        d = {
            "rule_id": "PLUG-001",
            "name": "Dict Rule",
            "severity": Severity.MEDIUM,
            "description": "From dict",
            "check": lambda t, s: [],
        }
        rule = _dict_to_plugin_rule(d, "test.py")
        assert rule.rule_id == "PLUG-001"
        assert rule.name == "Dict Rule"
        assert rule.severity == Severity.MEDIUM

    def test_severity_from_string(self):
        d = {
            "rule_id": "PLUG-001",
            "name": "T",
            "severity": "high",
            "description": "d",
            "check": lambda t, s: [],
        }
        rule = _dict_to_plugin_rule(d, "test.py")
        assert rule.severity == Severity.HIGH

    def test_missing_keys_raises(self):
        d = {"rule_id": "PLUG-001"}
        with pytest.raises(ValueError, match="missing keys"):
            _dict_to_plugin_rule(d, "test.py")

    def test_optional_cwe(self):
        d = {
            "rule_id": "PLUG-001",
            "name": "T",
            "severity": Severity.LOW,
            "description": "d",
            "check": lambda t, s: [],
            "cwe": "CWE-200",
        }
        rule = _dict_to_plugin_rule(d, "test.py")
        assert rule.cwe == "CWE-200"

    def test_optional_detailed_description(self):
        d = {
            "rule_id": "PLUG-001",
            "name": "T",
            "severity": Severity.LOW,
            "description": "d",
            "check": lambda t, s: [],
            "detailed_description": "Longer text",
        }
        rule = _dict_to_plugin_rule(d, "test.py")
        assert rule.detailed_description == "Longer text"


# --------------- load_plugin_file ---------------

class TestLoadPluginFile:
    """Tests for load_plugin_file()."""

    def test_load_valid_plugin(self, tmp_path):
        plugin = tmp_path / "my_plugin.py"
        plugin.write_text(textwrap.dedent("""\
            from mcpaudit.plugins import PluginRule
            from mcpaudit.models import Severity

            def register_rules():
                return [
                    PluginRule(
                        rule_id="PLUG-001",
                        name="Test Plugin",
                        severity=Severity.LOW,
                        description="A test plugin",
                        check_fn=lambda t, s: [],
                    )
                ]
        """))
        rules = load_plugin_file(plugin)
        assert len(rules) == 1
        assert rules[0].rule_id == "PLUG-001"

    def test_load_dict_plugin(self, tmp_path):
        plugin = tmp_path / "dict_plugin.py"
        plugin.write_text(textwrap.dedent("""\
            from mcpaudit.models import Severity

            def register_rules():
                return [
                    {
                        "rule_id": "PLUG-D01",
                        "name": "Dict Plugin",
                        "severity": "medium",
                        "description": "From dict",
                        "check": lambda t, s: [],
                    }
                ]
        """))
        rules = load_plugin_file(plugin)
        assert len(rules) == 1
        assert rules[0].rule_id == "PLUG-D01"

    def test_file_not_found(self, tmp_path):
        with pytest.raises(FileNotFoundError):
            load_plugin_file(tmp_path / "nonexistent.py")

    def test_not_py_file(self, tmp_path):
        txt_file = tmp_path / "not_a_plugin.txt"
        txt_file.write_text("hello")
        with pytest.raises(ValueError, match="must be .py"):
            load_plugin_file(txt_file)

    def test_no_register_rules(self, tmp_path):
        plugin = tmp_path / "bad_plugin.py"
        plugin.write_text("x = 1\n")
        with pytest.raises(ValueError, match="no register_rules"):
            load_plugin_file(plugin)

    def test_register_rules_not_callable(self, tmp_path):
        plugin = tmp_path / "bad2.py"
        plugin.write_text("register_rules = 42\n")
        with pytest.raises(ValueError, match="not callable"):
            load_plugin_file(plugin)

    def test_register_rules_returns_non_list(self, tmp_path):
        plugin = tmp_path / "bad3.py"
        plugin.write_text(textwrap.dedent("""\
            def register_rules():
                return "not a list"
        """))
        with pytest.raises(ValueError, match="must return a list"):
            load_plugin_file(plugin)

    def test_unsupported_item_type(self, tmp_path):
        plugin = tmp_path / "bad4.py"
        plugin.write_text(textwrap.dedent("""\
            def register_rules():
                return [42]
        """))
        with pytest.raises(ValueError, match="unsupported type"):
            load_plugin_file(plugin)

    def test_plugin_with_syntax_error(self, tmp_path):
        plugin = tmp_path / "syntax_err.py"
        plugin.write_text("def register_rules(\n")
        with pytest.raises(ImportError, match="Error loading plugin"):
            load_plugin_file(plugin)

    def test_multiple_rules_from_one_file(self, tmp_path):
        plugin = tmp_path / "multi.py"
        plugin.write_text(textwrap.dedent("""\
            from mcpaudit.plugins import PluginRule
            from mcpaudit.models import Severity

            def register_rules():
                return [
                    PluginRule(
                        rule_id="PLUG-A01",
                        name="Rule A",
                        severity=Severity.LOW,
                        description="A",
                        check_fn=lambda t, s: [],
                    ),
                    PluginRule(
                        rule_id="PLUG-A02",
                        name="Rule B",
                        severity=Severity.HIGH,
                        description="B",
                        check_fn=lambda t, s: [],
                    ),
                ]
        """))
        rules = load_plugin_file(plugin)
        assert len(rules) == 2
        assert {r.rule_id for r in rules} == {"PLUG-A01", "PLUG-A02"}


# --------------- discover_plugins ---------------

class TestDiscoverPlugins:
    """Tests for discover_plugins()."""

    def test_discover_empty_directory(self, tmp_path):
        rules = discover_plugins(tmp_path)
        assert rules == []

    def test_discover_skips_underscored_files(self, tmp_path):
        (tmp_path / "_internal.py").write_text(textwrap.dedent("""\
            def register_rules():
                return []
        """))
        rules = discover_plugins(tmp_path)
        assert rules == []

    def test_discover_loads_multiple_files(self, tmp_path):
        for name, rid in [("a.py", "PLUG-A01"), ("b.py", "PLUG-B01")]:
            (tmp_path / name).write_text(textwrap.dedent(f"""\
                from mcpaudit.plugins import PluginRule
                from mcpaudit.models import Severity

                def register_rules():
                    return [
                        PluginRule(
                            rule_id="{rid}",
                            name="Rule from {name}",
                            severity=Severity.LOW,
                            description="d",
                            check_fn=lambda t, s: [],
                        )
                    ]
            """))
        rules = discover_plugins(tmp_path)
        assert len(rules) == 2
        ids = {r.rule_id for r in rules}
        assert "PLUG-A01" in ids
        assert "PLUG-B01" in ids

    def test_discover_nonexistent_directory(self):
        with pytest.raises(FileNotFoundError):
            discover_plugins(Path("/nonexistent/path"))

    def test_discover_not_a_directory(self, tmp_path):
        f = tmp_path / "file.txt"
        f.write_text("hi")
        with pytest.raises(NotADirectoryError):
            discover_plugins(f)

    def test_discover_ignores_non_py_files(self, tmp_path):
        (tmp_path / "readme.txt").write_text("not a plugin")
        (tmp_path / "data.json").write_text("{}")
        rules = discover_plugins(tmp_path)
        assert rules == []


# --------------- PluginRegistry ---------------

class TestPluginRegistry:
    """Tests for PluginRegistry."""

    def test_empty_registry(self):
        reg = PluginRegistry()
        assert reg.total_rules == 0
        assert reg.total_plugins == 0
        assert reg.plugins == []
        assert reg.rules == []

    def test_load_file(self, tmp_path):
        plugin = tmp_path / "p1.py"
        plugin.write_text(textwrap.dedent("""\
            from mcpaudit.plugins import PluginRule
            from mcpaudit.models import Severity

            def register_rules():
                return [
                    PluginRule(
                        rule_id="PLUG-001",
                        name="T",
                        severity=Severity.LOW,
                        description="d",
                        check_fn=lambda t, s: [],
                    )
                ]
        """))
        reg = PluginRegistry()
        info = reg.load_file(plugin)
        assert info.rules_count == 1
        assert info.errors == []
        assert reg.total_rules == 1
        assert reg.total_plugins == 1

    def test_load_file_with_errors(self, tmp_path):
        plugin = tmp_path / "bad.py"
        plugin.write_text("x = 1\n")
        reg = PluginRegistry()
        info = reg.load_file(plugin)
        assert info.rules_count == 0
        assert len(info.errors) > 0
        assert reg.total_rules == 0

    def test_load_directory(self, tmp_path):
        for i in range(3):
            (tmp_path / f"rule_{i}.py").write_text(textwrap.dedent(f"""\
                from mcpaudit.plugins import PluginRule
                from mcpaudit.models import Severity

                def register_rules():
                    return [
                        PluginRule(
                            rule_id="PLUG-{i:03d}",
                            name="Rule {i}",
                            severity=Severity.LOW,
                            description="d",
                            check_fn=lambda t, s: [],
                        )
                    ]
            """))
        reg = PluginRegistry()
        infos = reg.load_directory(tmp_path)
        assert len(infos) == 3
        assert reg.total_plugins == 3
        assert reg.total_rules == 3

    def test_load_directory_nonexistent(self):
        reg = PluginRegistry()
        with pytest.raises(FileNotFoundError):
            reg.load_directory(Path("/nonexistent"))

    def test_format_list_empty(self):
        reg = PluginRegistry()
        text = reg.format_list()
        assert "no plugins loaded" in text

    def test_format_list_with_plugins(self, tmp_path):
        plugin = tmp_path / "myplugin.py"
        plugin.write_text(textwrap.dedent("""\
            from mcpaudit.plugins import PluginRule
            from mcpaudit.models import Severity

            def register_rules():
                return [
                    PluginRule(
                        rule_id="PLUG-001",
                        name="T",
                        severity=Severity.LOW,
                        description="d",
                        check_fn=lambda t, s: [],
                    )
                ]
        """))
        reg = PluginRegistry()
        reg.load_file(plugin)
        text = reg.format_list()
        assert "myplugin" in text
        assert "1 rules" in text
        assert "OK" in text

    def test_format_list_with_errors(self, tmp_path):
        plugin = tmp_path / "broken.py"
        plugin.write_text("x = 1\n")
        reg = PluginRegistry()
        reg.load_file(plugin)
        text = reg.format_list()
        assert "ERRORS" in text
        assert "ERROR:" in text

    def test_clear(self, tmp_path):
        plugin = tmp_path / "p.py"
        plugin.write_text(textwrap.dedent("""\
            from mcpaudit.plugins import PluginRule
            from mcpaudit.models import Severity

            def register_rules():
                return [
                    PluginRule(
                        rule_id="PLUG-001",
                        name="T",
                        severity=Severity.LOW,
                        description="d",
                        check_fn=lambda t, s: [],
                    )
                ]
        """))
        reg = PluginRegistry()
        reg.load_file(plugin)
        assert reg.total_rules == 1
        reg.clear()
        assert reg.total_rules == 0
        assert reg.total_plugins == 0

    def test_load_file_validates_rules(self, tmp_path):
        """Rules without PLUG- prefix are rejected by validation."""
        plugin = tmp_path / "invalid_prefix.py"
        plugin.write_text(textwrap.dedent("""\
            from mcpaudit.plugins import PluginRule
            from mcpaudit.models import Severity

            def register_rules():
                return [
                    PluginRule(
                        rule_id="CUSTOM-001",
                        name="T",
                        severity=Severity.LOW,
                        description="d",
                        check_fn=lambda t, s: [],
                    )
                ]
        """))
        reg = PluginRegistry()
        info = reg.load_file(plugin)
        # Rule has wrong prefix, so it should be rejected
        assert info.rules_count == 0
        assert len(info.errors) > 0
        assert reg.total_rules == 0

    def test_plugins_returns_copy(self, tmp_path):
        reg = PluginRegistry()
        plugins = reg.plugins
        plugins.append(PluginInfo(filepath=Path("x"), name="x", rules_count=0))
        assert reg.total_plugins == 0  # Original not modified

    def test_rules_returns_copy(self):
        reg = PluginRegistry()
        rules = reg.rules
        rules.append(PluginRule(
            rule_id="PLUG-X",
            name="X",
            severity=Severity.LOW,
            description="d",
            check_fn=lambda t, s: [],
        ))
        assert reg.total_rules == 0


# --------------- Scanner integration ---------------

class TestScannerPluginIntegration:
    """Tests that plugin rules integrate with MCPSecurityScanner."""

    def test_scanner_with_plugin_rules(self, tmp_path):
        from mcpaudit.scanner import MCPSecurityScanner

        def check_fn(tree, source):
            return [
                Finding(
                    rule_id="PLUG-INT",
                    severity=Severity.LOW,
                    title="Integration test",
                    description="Plugin found something",
                    file="",
                    line=1,
                )
            ]

        plugin_rule = PluginRule(
            rule_id="PLUG-INT",
            name="Integration",
            severity=Severity.LOW,
            description="d",
            check_fn=check_fn,
        )

        scanner = MCPSecurityScanner(
            rules=[],
            plugin_rules=[plugin_rule],
        )
        assert len(scanner.rules) == 1

        target = tmp_path / "test_target.py"
        target.write_text("x = 1\n")
        result = scanner.scan_file(target)
        assert len(result.findings) == 1
        assert result.findings[0].rule_id == "PLUG-INT"

    def test_scanner_plugin_rules_none(self):
        from mcpaudit.scanner import MCPSecurityScanner
        scanner = MCPSecurityScanner(rules=[], plugin_rules=None)
        assert scanner.rules == []

    def test_scanner_plugin_rules_appended(self):
        from mcpaudit.rules import get_all_rules
        from mcpaudit.scanner import MCPSecurityScanner

        plugin_rule = PluginRule(
            rule_id="PLUG-APP",
            name="Appended",
            severity=Severity.LOW,
            description="d",
            check_fn=lambda t, s: [],
        )
        builtin_count = len(get_all_rules())
        scanner = MCPSecurityScanner(plugin_rules=[plugin_rule])
        assert len(scanner.rules) == builtin_count + 1


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

class TestPluginCLI:
    """Tests for plugin-related CLI arguments."""

    def test_plugin_dir_arg_registered(self):
        from mcpaudit.__main__ import build_parser
        parser = build_parser()
        args = parser.parse_args(["test.py", "--plugin-dir", "/tmp/plugins"])
        assert args.plugin_dir == "/tmp/plugins"

    def test_plugins_subcommand_registered(self):
        from mcpaudit.__main__ import _SUBCOMMANDS
        assert "plugins" in _SUBCOMMANDS
