"""Tests for Plugin System."""

from __future__ import annotations

import json
from pathlib import Path

import pytest

from mcpaudit.plugin_system import (
    PluginLoader,
    PluginManifest,
    PluginMatch,
    PluginRegistry,
    PluginRule,
    PluginRunner,
    format_matches_json,
    format_plugins_text,
)


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

def _rule(
    rule_id="CUSTOM-001", pattern=r"eval\(",
    severity="HIGH", title="Eval usage",
) -> PluginRule:
    return PluginRule(
        rule_id=rule_id,
        pattern=pattern,
        severity=severity,
        title=title,
        description=f"Desc: {title}",
        fix_suggestion="Remove eval",
    )


def _manifest(
    name="test-plugin", version="1.0.0",
    rules=None,
) -> PluginManifest:
    return PluginManifest(
        name=name,
        version=version,
        author="Test",
        description="Test plugin",
        rules=rules or [_rule()],
    )


def _write_manifest(tmp_path: Path, data: dict) -> Path:
    manifest_file = tmp_path / "plugin.json"
    manifest_file.write_text(
        json.dumps(data, indent=2),
        encoding="utf-8",
    )
    return manifest_file


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


class TestPluginRule:
    def test_defaults(self):
        r = PluginRule(rule_id="X")
        assert r.pattern == ""
        assert r.severity == "MEDIUM"
        assert r.file_pattern == "*"
        assert r.enabled is True

    def test_to_dict(self):
        r = _rule()
        d = r.to_dict()
        assert d["rule_id"] == "CUSTOM-001"
        assert d["pattern"] == r"eval\("
        assert d["severity"] == "HIGH"

    def test_from_dict(self):
        data = {
            "rule_id": "X", "pattern": "test",
            "severity": "LOW",
        }
        r = PluginRule.from_dict(data)
        assert r.rule_id == "X"
        assert r.severity == "LOW"

    def test_from_dict_defaults(self):
        r = PluginRule.from_dict({})
        assert r.rule_id == ""
        assert r.severity == "MEDIUM"
        assert r.enabled is True

    def test_validate_valid(self):
        r = _rule()
        assert r.validate() == []

    def test_validate_no_id(self):
        r = PluginRule(rule_id="", pattern="x")
        errors = r.validate()
        assert any("rule_id" in e for e in errors)

    def test_validate_no_pattern(self):
        r = PluginRule(rule_id="X", pattern="")
        errors = r.validate()
        assert any("pattern" in e for e in errors)

    def test_validate_invalid_regex(self):
        r = PluginRule(rule_id="X", pattern="[invalid")
        errors = r.validate()
        assert any("regex" in e for e in errors)

    def test_validate_invalid_severity(self):
        r = PluginRule(
            rule_id="X", pattern="x", severity="ULTRA",
        )
        errors = r.validate()
        assert any("severity" in e for e in errors)

    def test_roundtrip(self):
        r = _rule()
        rebuilt = PluginRule.from_dict(r.to_dict())
        assert rebuilt.rule_id == r.rule_id
        assert rebuilt.pattern == r.pattern


# ---------------------------------------------------------------------------
# PluginManifest
# ---------------------------------------------------------------------------


class TestPluginManifest:
    def test_defaults(self):
        m = PluginManifest()
        assert m.name == ""
        assert m.version == "0.1.0"
        assert m.rules == []

    def test_to_dict(self):
        m = _manifest()
        d = m.to_dict()
        assert d["name"] == "test-plugin"
        assert d["version"] == "1.0.0"
        assert len(d["rules"]) == 1

    def test_from_dict(self):
        data = {
            "name": "my-plugin",
            "version": "2.0",
            "author": "Author",
            "rules": [
                {"rule_id": "R1", "pattern": "x", "severity": "LOW"},
            ],
        }
        m = PluginManifest.from_dict(data)
        assert m.name == "my-plugin"
        assert len(m.rules) == 1

    def test_validate_valid(self):
        m = _manifest()
        assert m.validate() == []

    def test_validate_no_name(self):
        m = PluginManifest(
            name="", version="1.0", rules=[_rule()],
        )
        errors = m.validate()
        assert any("name" in e for e in errors)

    def test_validate_no_version(self):
        m = PluginManifest(
            name="x", version="", rules=[_rule()],
        )
        errors = m.validate()
        assert any("version" in e for e in errors)

    def test_validate_no_rules(self):
        m = PluginManifest(name="x", version="1.0", rules=[])
        errors = m.validate()
        assert any("at least one rule" in e for e in errors)

    def test_validate_propagates_rule_errors(self):
        bad_rule = PluginRule(rule_id="", pattern="x")
        m = PluginManifest(
            name="x", version="1.0", rules=[bad_rule],
        )
        errors = m.validate()
        assert any("rule_id" in e for e in errors)

    def test_source_path(self):
        m = PluginManifest.from_dict(
            {"name": "x", "version": "1", "rules": []},
            source_path="/a/b/plugin.json",
        )
        assert m.source_path == "/a/b/plugin.json"


# ---------------------------------------------------------------------------
# PluginMatch
# ---------------------------------------------------------------------------


class TestPluginMatch:
    def test_to_dict(self):
        m = PluginMatch(
            rule_id="R1", severity="HIGH",
            title="Found", description="D",
            file="test.py", line=5,
            code_snippet="eval(x)",
            fix_suggestion="Remove eval",
            plugin_name="my-plugin",
        )
        d = m.to_dict()
        assert d["rule_id"] == "R1"
        assert d["plugin_name"] == "my-plugin"
        assert d["line"] == 5


# ---------------------------------------------------------------------------
# PluginLoader
# ---------------------------------------------------------------------------


class TestPluginLoader:
    def test_load_manifest(self, tmp_path):
        data = {
            "name": "test", "version": "1.0",
            "rules": [
                {"rule_id": "R1", "pattern": "eval", "severity": "HIGH"},
            ],
        }
        path = _write_manifest(tmp_path, data)
        manifest, errors = PluginLoader.load_manifest(path)
        assert errors == []
        assert manifest is not None
        assert manifest.name == "test"

    def test_load_manifest_missing(self):
        manifest, errors = PluginLoader.load_manifest("/no/such/file")
        assert manifest is None
        assert any("not found" in e for e in errors)

    def test_load_manifest_invalid_json(self, tmp_path):
        f = tmp_path / "plugin.json"
        f.write_text("{bad", encoding="utf-8")
        manifest, errors = PluginLoader.load_manifest(f)
        assert manifest is None
        assert any("JSON" in e for e in errors)

    def test_load_manifest_not_object(self, tmp_path):
        f = tmp_path / "plugin.json"
        f.write_text('"just a string"', encoding="utf-8")
        manifest, errors = PluginLoader.load_manifest(f)
        assert manifest is None
        assert any("object" in e for e in errors)

    def test_load_manifest_validation_errors(self, tmp_path):
        data = {"name": "", "version": "1.0", "rules": []}
        path = _write_manifest(tmp_path, data)
        manifest, errors = PluginLoader.load_manifest(path)
        assert len(errors) > 0

    def test_load_from_directory(self, tmp_path):
        data = {
            "name": "dir-plugin", "version": "1.0",
            "rules": [
                {"rule_id": "R1", "pattern": "x", "severity": "LOW"},
            ],
        }
        _write_manifest(tmp_path, data)
        manifest, errors = PluginLoader.load_from_directory(tmp_path)
        assert errors == []
        assert manifest is not None
        assert manifest.name == "dir-plugin"

    def test_load_from_directory_not_dir(self):
        manifest, errors = PluginLoader.load_from_directory(
            "/no/such/directory"
        )
        assert manifest is None
        assert any("Not a directory" in e for e in errors)


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


class TestPluginRegistry:
    def test_register(self):
        reg = PluginRegistry()
        errors = reg.register(_manifest())
        assert errors == []
        assert reg.count == 1

    def test_register_invalid(self):
        reg = PluginRegistry()
        m = PluginManifest(name="", version="1.0", rules=[])
        errors = reg.register(m)
        assert len(errors) > 0
        assert reg.count == 0

    def test_unregister(self):
        reg = PluginRegistry()
        reg.register(_manifest())
        assert reg.unregister("test-plugin") is True
        assert reg.count == 0

    def test_unregister_not_found(self):
        reg = PluginRegistry()
        assert reg.unregister("nope") is False

    def test_get(self):
        reg = PluginRegistry()
        reg.register(_manifest())
        m = reg.get("test-plugin")
        assert m is not None
        assert m.name == "test-plugin"

    def test_get_not_found(self):
        reg = PluginRegistry()
        assert reg.get("nope") is None

    def test_list_plugins(self):
        reg = PluginRegistry()
        reg.register(_manifest(name="a"))
        reg.register(_manifest(name="b"))
        assert len(reg.list_plugins()) == 2

    def test_list_names(self):
        reg = PluginRegistry()
        reg.register(_manifest(name="b"))
        reg.register(_manifest(name="a"))
        assert reg.list_names() == ["a", "b"]

    def test_get_all_rules(self):
        reg = PluginRegistry()
        reg.register(_manifest(name="a", rules=[_rule(), _rule(rule_id="R2")]))
        reg.register(_manifest(name="b", rules=[_rule(rule_id="R3")]))
        assert len(reg.get_all_rules()) == 3

    def test_clear(self):
        reg = PluginRegistry()
        reg.register(_manifest())
        reg.clear()
        assert reg.count == 0


# ---------------------------------------------------------------------------
# PluginRunner
# ---------------------------------------------------------------------------


class TestPluginRunner:
    def test_run_rule_match(self):
        reg = PluginRegistry()
        runner = PluginRunner(reg)
        rule = _rule(pattern=r"eval\(")
        matches = runner.run_rule(
            rule, "x = eval(input())", file_path="test.py",
        )
        assert len(matches) == 1
        assert matches[0].line == 1
        assert "eval(input())" in matches[0].code_snippet

    def test_run_rule_no_match(self):
        reg = PluginRegistry()
        runner = PluginRunner(reg)
        rule = _rule(pattern=r"eval\(")
        matches = runner.run_rule(
            rule, "x = safe_function()", file_path="test.py",
        )
        assert matches == []

    def test_run_rule_disabled(self):
        reg = PluginRegistry()
        runner = PluginRunner(reg)
        rule = _rule()
        rule.enabled = False
        matches = runner.run_rule(
            rule, "eval(x)", file_path="test.py",
        )
        assert matches == []

    def test_run_rule_invalid_regex(self):
        reg = PluginRegistry()
        runner = PluginRunner(reg)
        rule = _rule(pattern="[invalid")
        matches = runner.run_rule(
            rule, "test content", file_path="test.py",
        )
        assert matches == []

    def test_run_rule_multiline(self):
        reg = PluginRegistry()
        runner = PluginRunner(reg)
        rule = _rule(pattern=r"eval\(")
        content = "line1\neval(x)\nline3\neval(y)"
        matches = runner.run_rule(
            rule, content, file_path="test.py",
        )
        assert len(matches) == 2
        assert matches[0].line == 2
        assert matches[1].line == 4

    def test_run_plugin(self):
        reg = PluginRegistry()
        reg.register(_manifest())
        runner = PluginRunner(reg)
        matches = runner.run_plugin(
            "test-plugin", "result = eval(data)",
            file_path="script.py",
        )
        assert len(matches) == 1

    def test_run_plugin_not_found(self):
        reg = PluginRegistry()
        runner = PluginRunner(reg)
        matches = runner.run_plugin(
            "nonexistent", "eval(x)",
        )
        assert matches == []

    def test_run_all(self):
        reg = PluginRegistry()
        reg.register(_manifest(name="a", rules=[_rule(rule_id="R1")]))
        reg.register(_manifest(
            name="b",
            rules=[_rule(rule_id="R2", pattern=r"exec\(")],
        ))
        runner = PluginRunner(reg)
        matches = runner.run_all(
            "eval(x)\nexec(y)", file_path="test.py",
        )
        assert len(matches) == 2

    def test_run_all_empty(self):
        reg = PluginRegistry()
        runner = PluginRunner(reg)
        matches = runner.run_all("safe code")
        assert matches == []

    def test_match_has_plugin_name(self):
        reg = PluginRegistry()
        reg.register(_manifest(name="my-plugin"))
        runner = PluginRunner(reg)
        matches = runner.run_plugin(
            "my-plugin", "eval(x)", file_path="t.py",
        )
        assert matches[0].plugin_name == "my-plugin"


# ---------------------------------------------------------------------------
# Formatting
# ---------------------------------------------------------------------------


class TestFormatPluginsText:
    def test_empty(self):
        reg = PluginRegistry()
        result = format_plugins_text(reg)
        assert "No plugins" in result

    def test_with_plugins(self):
        reg = PluginRegistry()
        reg.register(_manifest(name="my-plugin"))
        result = format_plugins_text(reg)
        assert "my-plugin" in result
        assert "1.0.0" in result
        assert "Rules: 1" in result

    def test_with_author(self):
        reg = PluginRegistry()
        m = _manifest()
        m.author = "Alice"
        reg.register(m)
        result = format_plugins_text(reg)
        assert "Alice" in result


class TestFormatMatchesJson:
    def test_valid_json(self):
        matches = [
            PluginMatch(
                rule_id="R1", severity="HIGH",
                title="T", description="D",
                file="a.py", line=1,
            ),
        ]
        result = format_matches_json(matches)
        data = json.loads(result)
        assert data["total"] == 1
        assert data["matches"][0]["rule_id"] == "R1"

    def test_empty(self):
        result = format_matches_json([])
        data = json.loads(result)
        assert data["total"] == 0
        assert data["matches"] == []
