"""Tests for MCP Registry Scanner."""

from __future__ import annotations

import json
from pathlib import Path
from typing import Any

import pytest

from mcpaudit.registry_scanner import (
    MCPCategory,
    MCPEntry,
    RegistryReport,
    RiskAssessment,
    RiskFactor,
    RiskLevel,
    assess_mcp_risk,
    format_assessment_text,
    format_mcps_list_text,
    format_registry_json,
    format_registry_text,
    get_known_mcp,
    get_known_mcps,
    load_registry,
    save_registry,
    scan_registry,
    _compute_data_risk,
    _compute_permission_risk,
    _compute_tool_risk,
    _generate_mitigations,
    _KNOWN_MCPS,
    _PERMISSION_WEIGHTS,
    _TOOL_RISK_KEYWORDS,
    _DATA_ACCESS_WEIGHTS,
)


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

def _sample_mcp() -> MCPEntry:
    return MCPEntry(
        name="test-mcp",
        description="A test MCP",
        repo="test/repo",
        category="utility",
        permissions=["file_read", "network_access"],
        tools=["read_file", "execute_command"],
        data_access=["source_code", "logs"],
        risk_level="medium",
        version="1.0.0",
        verified=True,
    )


def _high_risk_mcp() -> MCPEntry:
    return MCPEntry(
        name="danger-mcp",
        description="Dangerous MCP",
        permissions=["shell_execute", "file_write", "file_delete",
                     "secret_access", "code_execute"],
        tools=["execute", "run_shell", "delete_all", "admin_reset"],
        data_access=["credentials", "api_keys", "personal_data"],
        risk_level="critical",
    )


def _minimal_mcp() -> MCPEntry:
    return MCPEntry(
        name="safe-mcp",
        description="Safe read-only MCP",
        permissions=["file_read"],
        tools=["list_items", "get_info"],
        data_access=["logs"],
        risk_level="minimal",
    )


# ---------------------------------------------------------------------------
# MCPCategory
# ---------------------------------------------------------------------------

class TestMCPCategory:
    def test_values(self):
        assert MCPCategory.FILESYSTEM.value == "filesystem"
        assert MCPCategory.DATABASE.value == "database"
        assert MCPCategory.CODE_EXECUTION.value == "code_execution"

    def test_all_categories(self):
        assert len(MCPCategory) >= 10


# ---------------------------------------------------------------------------
# RiskLevel
# ---------------------------------------------------------------------------

class TestRiskLevel:
    def test_values(self):
        assert RiskLevel.CRITICAL.value == "critical"
        assert RiskLevel.HIGH.value == "high"
        assert RiskLevel.LOW.value == "low"

    def test_score(self):
        assert RiskLevel.CRITICAL.score == 100
        assert RiskLevel.HIGH.score == 75
        assert RiskLevel.MEDIUM.score == 50
        assert RiskLevel.LOW.score == 25
        assert RiskLevel.MINIMAL.score == 10

    def test_from_score_critical(self):
        assert RiskLevel.from_score(90) == RiskLevel.CRITICAL

    def test_from_score_high(self):
        assert RiskLevel.from_score(70) == RiskLevel.HIGH

    def test_from_score_medium(self):
        assert RiskLevel.from_score(50) == RiskLevel.MEDIUM

    def test_from_score_low(self):
        assert RiskLevel.from_score(30) == RiskLevel.LOW

    def test_from_score_minimal(self):
        assert RiskLevel.from_score(10) == RiskLevel.MINIMAL

    def test_from_string(self):
        assert RiskLevel.from_string("critical") == RiskLevel.CRITICAL
        assert RiskLevel.from_string("HIGH") == RiskLevel.HIGH

    def test_from_string_invalid(self):
        with pytest.raises(ValueError, match="Invalid risk level"):
            RiskLevel.from_string("unknown")


# ---------------------------------------------------------------------------
# MCPEntry
# ---------------------------------------------------------------------------

class TestMCPEntry:
    def test_basic(self):
        mcp = _sample_mcp()
        assert mcp.name == "test-mcp"
        assert mcp.verified is True

    def test_defaults(self):
        mcp = MCPEntry(name="x")
        assert mcp.category == "utility"
        assert mcp.risk_level == "medium"
        assert mcp.permissions == []
        assert mcp.tools == []
        assert mcp.verified is False

    def test_to_dict(self):
        mcp = _sample_mcp()
        d = mcp.to_dict()
        assert d["name"] == "test-mcp"
        assert d["verified"] is True
        assert "permissions" in d

    def test_from_dict(self):
        data = {
            "name": "test",
            "description": "Test MCP",
            "permissions": ["file_read"],
            "verified": True,
        }
        mcp = MCPEntry.from_dict(data)
        assert mcp.name == "test"
        assert mcp.verified is True
        assert mcp.permissions == ["file_read"]

    def test_from_dict_defaults(self):
        mcp = MCPEntry.from_dict({"name": "x"})
        assert mcp.category == "utility"
        assert mcp.risk_level == "medium"

    def test_roundtrip(self):
        mcp = _sample_mcp()
        d = mcp.to_dict()
        mcp2 = MCPEntry.from_dict(d)
        assert mcp2.name == mcp.name
        assert mcp2.permissions == mcp.permissions
        assert mcp2.verified == mcp.verified


# ---------------------------------------------------------------------------
# RiskFactor
# ---------------------------------------------------------------------------

class TestRiskFactor:
    def test_basic(self):
        f = RiskFactor(category="permission", name="file_read", weight=15)
        assert f.category == "permission"
        assert f.weight == 15

    def test_to_dict(self):
        f = RiskFactor(category="tool", name="execute", weight=30,
                       description="Risky tool")
        d = f.to_dict()
        assert d["category"] == "tool"
        assert d["weight"] == 30


# ---------------------------------------------------------------------------
# RiskAssessment
# ---------------------------------------------------------------------------

class TestRiskAssessment:
    def test_basic(self):
        a = RiskAssessment(mcp_name="test", risk_score=50)
        assert a.mcp_name == "test"
        assert a.risk_score == 50

    def test_to_dict(self):
        a = RiskAssessment(
            mcp_name="test",
            risk_level="high",
            risk_score=75,
            mitigations=["Fix it"],
        )
        d = a.to_dict()
        assert d["risk_level"] == "high"
        assert len(d["mitigations"]) == 1


# ---------------------------------------------------------------------------
# RegistryReport
# ---------------------------------------------------------------------------

class TestRegistryReport:
    def test_defaults(self):
        r = RegistryReport()
        assert r.total_mcps == 0
        assert r.scanned_at  # auto-set

    def test_to_dict(self):
        r = RegistryReport(
            total_mcps=5,
            risk_distribution={"critical": 1, "high": 2},
            high_risk_mcps=["danger"],
        )
        d = r.to_dict()
        assert d["total_mcps"] == 5
        assert d["high_risk_mcps"] == ["danger"]


# ---------------------------------------------------------------------------
# Permission risk
# ---------------------------------------------------------------------------

class TestComputePermissionRisk:
    def test_basic(self):
        score, factors = _compute_permission_risk(["file_read", "file_write"])
        assert score > 0
        assert len(factors) == 2

    def test_empty(self):
        score, factors = _compute_permission_risk([])
        assert score == 0
        assert factors == []

    def test_shell_execute_high(self):
        score, factors = _compute_permission_risk(["shell_execute"])
        assert score == _PERMISSION_WEIGHTS["shell_execute"]

    def test_unknown_permission(self):
        score, factors = _compute_permission_risk(["unknown_perm"])
        assert score == 5  # Default weight


# ---------------------------------------------------------------------------
# Tool risk
# ---------------------------------------------------------------------------

class TestComputeToolRisk:
    def test_basic(self):
        score, factors = _compute_tool_risk(["execute_command", "list_files"])
        assert score > 0
        assert len(factors) >= 1

    def test_empty(self):
        score, factors = _compute_tool_risk([])
        assert score == 0

    def test_safe_tools(self):
        score, factors = _compute_tool_risk(["list_items", "get_info"])
        assert score == 0
        assert factors == []

    def test_dangerous_tool(self):
        score, factors = _compute_tool_risk(["run_shell_command"])
        assert score > 0
        assert factors[0].name == "run_shell_command"


# ---------------------------------------------------------------------------
# Data access risk
# ---------------------------------------------------------------------------

class TestComputeDataRisk:
    def test_basic(self):
        score, factors = _compute_data_risk(["credentials", "logs"])
        assert score > 0
        assert len(factors) == 2

    def test_empty(self):
        score, factors = _compute_data_risk([])
        assert score == 0

    def test_high_risk_data(self):
        score, factors = _compute_data_risk(["api_keys"])
        assert score == _DATA_ACCESS_WEIGHTS["api_keys"]


# ---------------------------------------------------------------------------
# Mitigations
# ---------------------------------------------------------------------------

class TestGenerateMitigations:
    def test_shell_execute(self):
        factors = [RiskFactor("permission", "shell_execute", 35)]
        mits = _generate_mitigations(RiskLevel.CRITICAL, factors)
        assert any("allowlist" in m for m in mits)

    def test_file_write(self):
        factors = [RiskFactor("permission", "file_write", 25)]
        mits = _generate_mitigations(RiskLevel.HIGH, factors)
        assert any("filesystem" in m.lower() or "directory" in m.lower() for m in mits)

    def test_network(self):
        factors = [RiskFactor("permission", "network_access", 20)]
        mits = _generate_mitigations(RiskLevel.MEDIUM, factors)
        assert any("network" in m.lower() for m in mits)

    def test_low_risk(self):
        factors = [RiskFactor("permission", "env_read", 10)]
        mits = _generate_mitigations(RiskLevel.LOW, factors)
        assert len(mits) > 0

    def test_credentials(self):
        factors = [RiskFactor("data_access", "credentials", 35)]
        mits = _generate_mitigations(RiskLevel.HIGH, factors)
        assert any("secrets" in m.lower() for m in mits)


# ---------------------------------------------------------------------------
# Known MCPs catalog
# ---------------------------------------------------------------------------

class TestKnownMCPs:
    def test_has_20_mcps(self):
        mcps = get_known_mcps()
        assert len(mcps) == 20

    def test_all_have_names(self):
        for mcp in get_known_mcps():
            assert mcp.name

    def test_all_have_permissions(self):
        for mcp in get_known_mcps():
            assert isinstance(mcp.permissions, list)

    def test_get_known_mcp_exists(self):
        mcp = get_known_mcp("filesystem")
        assert mcp is not None
        assert mcp.name == "filesystem"

    def test_get_known_mcp_case_insensitive(self):
        mcp = get_known_mcp("FILESYSTEM")
        assert mcp is not None

    def test_get_known_mcp_not_found(self):
        mcp = get_known_mcp("nonexistent")
        assert mcp is None

    def test_catalog_has_verified(self):
        verified = [m for m in get_known_mcps() if m.verified]
        assert len(verified) >= 5

    def test_catalog_has_critical(self):
        critical = [m for m in get_known_mcps() if m.risk_level == "critical"]
        assert len(critical) >= 1

    def test_returns_copy(self):
        mcps1 = get_known_mcps()
        mcps2 = get_known_mcps()
        assert mcps1 is not mcps2


# ---------------------------------------------------------------------------
# assess_mcp_risk
# ---------------------------------------------------------------------------

class TestAssessMCPRisk:
    def test_basic(self):
        a = assess_mcp_risk(_sample_mcp())
        assert a.mcp_name == "test-mcp"
        assert a.risk_score >= 0
        assert a.risk_level in ("critical", "high", "medium", "low", "minimal")

    def test_high_risk(self):
        a = assess_mcp_risk(_high_risk_mcp())
        assert a.risk_level in ("critical", "high")
        assert a.risk_score >= 60

    def test_minimal_risk(self):
        a = assess_mcp_risk(_minimal_mcp())
        assert a.risk_score < 50

    def test_from_dict(self):
        data = _sample_mcp().to_dict()
        a = assess_mcp_risk(data)
        assert a.mcp_name == "test-mcp"

    def test_has_factors(self):
        a = assess_mcp_risk(_sample_mcp())
        assert len(a.factors) > 0

    def test_has_mitigations(self):
        a = assess_mcp_risk(_high_risk_mcp())
        assert len(a.mitigations) > 0

    def test_has_summary(self):
        a = assess_mcp_risk(_sample_mcp())
        assert "test-mcp" in a.summary

    def test_empty_mcp(self):
        mcp = MCPEntry(name="empty")
        a = assess_mcp_risk(mcp)
        assert a.risk_score == 0
        assert a.risk_level == "minimal"


# ---------------------------------------------------------------------------
# load_registry / save_registry
# ---------------------------------------------------------------------------

class TestLoadSaveRegistry:
    def test_load_file(self, tmp_path: Path):
        data = [_sample_mcp().to_dict(), _minimal_mcp().to_dict()]
        p = tmp_path / "registry.json"
        p.write_text(json.dumps(data))
        entries = load_registry(p)
        assert len(entries) == 2
        assert entries[0].name == "test-mcp"

    def test_load_object_format(self, tmp_path: Path):
        data = {"mcps": [_sample_mcp().to_dict()]}
        p = tmp_path / "registry.json"
        p.write_text(json.dumps(data))
        entries = load_registry(p)
        assert len(entries) == 1

    def test_load_servers_key(self, tmp_path: Path):
        data = {"servers": [_sample_mcp().to_dict()]}
        p = tmp_path / "registry.json"
        p.write_text(json.dumps(data))
        entries = load_registry(p)
        assert len(entries) == 1

    def test_load_not_found(self, tmp_path: Path):
        with pytest.raises(FileNotFoundError):
            load_registry(tmp_path / "missing.json")

    def test_load_invalid_json(self, tmp_path: Path):
        p = tmp_path / "bad.json"
        p.write_text("not json")
        with pytest.raises(ValueError, match="Invalid JSON"):
            load_registry(p)

    def test_load_invalid_format(self, tmp_path: Path):
        p = tmp_path / "bad.json"
        p.write_text('"just a string"')
        with pytest.raises(ValueError, match="must be a JSON array"):
            load_registry(p)

    def test_save_and_load(self, tmp_path: Path):
        entries = [_sample_mcp(), _minimal_mcp()]
        p = tmp_path / "registry.json"
        save_registry(entries, p)
        loaded = load_registry(p)
        assert len(loaded) == 2
        assert loaded[0].name == "test-mcp"


# ---------------------------------------------------------------------------
# scan_registry
# ---------------------------------------------------------------------------

class TestScanRegistry:
    def test_scan_builtin(self):
        report = scan_registry()
        assert report.total_mcps == 20
        assert len(report.assessments) == 20
        assert report.scanned_at

    def test_scan_from_file(self, tmp_path: Path):
        data = [_sample_mcp().to_dict(), _high_risk_mcp().to_dict()]
        p = tmp_path / "registry.json"
        p.write_text(json.dumps(data))
        report = scan_registry(path=p)
        assert report.total_mcps == 2

    def test_scan_from_entries(self):
        entries = [_sample_mcp(), _minimal_mcp()]
        report = scan_registry(entries=entries)
        assert report.total_mcps == 2

    def test_risk_distribution(self):
        report = scan_registry(entries=[_high_risk_mcp(), _minimal_mcp()])
        dist = report.risk_distribution
        assert isinstance(dist, dict)
        assert sum(dist.values()) == 2

    def test_high_risk_identified(self):
        report = scan_registry(entries=[_high_risk_mcp(), _minimal_mcp()])
        assert "danger-mcp" in report.high_risk_mcps

    def test_recommendations_generated(self):
        report = scan_registry(entries=[_high_risk_mcp()])
        assert len(report.recommendations) > 0

    def test_unverified_recommendation(self):
        report = scan_registry(
            entries=[MCPEntry(name="unv1", verified=False)],
        )
        assert any("unverified" in r.lower() for r in report.recommendations)

    def test_to_dict(self):
        report = scan_registry(entries=[_sample_mcp()])
        d = report.to_dict()
        assert d["total_mcps"] == 1
        assert len(d["assessments"]) == 1


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

class TestFormatRegistryText:
    def test_basic(self):
        report = scan_registry(entries=[_sample_mcp()])
        text = format_registry_text(report)
        assert "MCP Registry Scan Report" in text
        assert "test-mcp" in text

    def test_risk_distribution(self):
        report = scan_registry(entries=[_sample_mcp(), _high_risk_mcp()])
        text = format_registry_text(report)
        assert "Risk Distribution" in text


class TestFormatRegistryJson:
    def test_valid_json(self):
        report = scan_registry(entries=[_sample_mcp()])
        text = format_registry_json(report)
        data = json.loads(text)
        assert data["total_mcps"] == 1


class TestFormatAssessmentText:
    def test_basic(self):
        assessment = assess_mcp_risk(_sample_mcp())
        text = format_assessment_text(assessment)
        assert "Risk Assessment" in text
        assert "test-mcp" in text
        assert "Risk Level" in text


class TestFormatMCPsListText:
    def test_basic(self):
        mcps = get_known_mcps()
        text = format_mcps_list_text(mcps)
        assert "Known MCP Catalog" in text
        assert "filesystem" in text
        assert f"Total: {len(mcps)}" in text

    def test_empty(self):
        text = format_mcps_list_text([])
        assert "Total: 0" in text
