"""Tests for the MCP Security Scanner."""

from __future__ import annotations

from typing import Any

import pytest

from navil.scanner import MCPSecurityScanner, RiskLevel, Vulnerability
from navil.types import Finding, Severity


@pytest.fixture
def scanner() -> MCPSecurityScanner:
    return MCPSecurityScanner()


def test_scan_secure_config(
    scanner: MCPSecurityScanner, config_file, sample_secure_config: dict[str, Any]
) -> None:
    """Secure configuration should have high score and zero vulnerabilities."""
    path = config_file(sample_secure_config)
    result = scanner.scan(path)

    assert result["status"] == "completed"
    assert result["security_score"] > 70
    assert result["total_vulnerabilities"] == 0


def test_plaintext_credentials_detection(scanner: MCPSecurityScanner, config_file) -> None:
    """Should detect plaintext passwords and API keys."""
    config = {
        "server": {"name": "Test Server"},
        "database": {
            "password='supersecret123'": "value",
            "api_key='sk-1234567890abcdef'": "value",
        },
    }
    path = config_file(config)
    result = scanner.scan(path)

    assert result["total_vulnerabilities"] > 0
    assert len(result["vulnerabilities"]) > 0


def test_over_privileged_permissions(scanner: MCPSecurityScanner, config_file) -> None:
    """Should detect wildcard permissions."""
    config = {
        "server": {"name": "Test Server"},
        "tools": [{"name": "dangerous_tool", "permissions": ["*"]}],
    }
    path = config_file(config)
    result = scanner.scan(path)

    assert result["total_vulnerabilities"] > 0
    vulns = result["vulnerabilities"]
    assert any("privilege" in v.get("title", "").lower() for v in vulns)


def test_missing_authentication(scanner: MCPSecurityScanner, config_file) -> None:
    """Should detect missing authentication configuration."""
    config = {"server": {"name": "Test Server"}, "tools": []}
    path = config_file(config)
    result = scanner.scan(path)

    assert result["total_vulnerabilities"] > 0
    vulns = result["vulnerabilities"]
    assert any("authentication" in v.get("title", "").lower() for v in vulns)


def test_unencrypted_protocol(scanner: MCPSecurityScanner, config_file) -> None:
    """Should detect HTTP (unencrypted) protocol."""
    config = {
        "server": {"name": "Test Server", "protocol": "http"},
        "authentication": {"type": "api_key"},
    }
    path = config_file(config)
    result = scanner.scan(path)

    assert result["total_vulnerabilities"] > 0
    vulns = result["vulnerabilities"]
    assert any("encrypted" in v.get("title", "").lower() for v in vulns)


def test_malicious_patterns_detection(scanner: MCPSecurityScanner, config_file) -> None:
    """Should detect malicious patterns like backdoor references."""
    config = {
        "server": {"name": "Test Server"},
        "tools": [
            {
                "name": "backdoor_tool",
                "description": "This tool has a hidden backdoor access mechanism",
            }
        ],
    }
    path = config_file(config)
    result = scanner.scan(path)

    assert result["total_vulnerabilities"] > 0
    vulns = result["vulnerabilities"]
    assert any("malicious" in v.get("title", "").lower() for v in vulns)


def test_aws_key_detection(scanner: MCPSecurityScanner, config_file) -> None:
    """Should detect AWS access keys."""
    config = {
        "server": {"name": "Test Server"},
        "aws_credentials": "AKIA1234567890123456",
    }
    path = config_file(config)
    result = scanner.scan(path)

    assert result["total_vulnerabilities"] > 0
    vulns = result["vulnerabilities"]
    assert any("credential" in v.get("title", "").lower() for v in vulns)


def test_jwt_token_detection(scanner: MCPSecurityScanner, config_file) -> None:
    """Should detect JWT tokens in config."""
    config = {
        "server": {"name": "Test Server"},
        "token": (
            "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
            ".eyJzdWIiOiIxMjM0NTY3ODkwIn0"
            ".TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"
        ),
    }
    path = config_file(config)
    result = scanner.scan(path)

    assert result["total_vulnerabilities"] > 0


def test_nonexistent_file(scanner: MCPSecurityScanner) -> None:
    """Should handle nonexistent configuration file gracefully."""
    result = scanner.scan("/nonexistent/path/config.json")

    assert result["status"] == "error"
    assert result["security_score"] == 0


def test_security_score_calculation(scanner: MCPSecurityScanner, config_file) -> None:
    """Secure config should produce a score between 70 and 100."""
    config = {
        "server": {"protocol": "https", "verified": True},
        "authentication": {"type": "mTLS"},
        "tools": [{"name": "safe", "permissions": ["read"]}],
    }
    path = config_file(config)
    result = scanner.scan(path)

    score = result["security_score"]
    assert score > 70
    assert score <= 100


def test_vulnerability_grouping(scanner: MCPSecurityScanner, config_file) -> None:
    """Vulnerabilities should be grouped by risk level."""
    config = {
        "server": {"protocol": "http"},
        "tools": [{"name": "tool1", "permissions": ["*"]}],
    }
    path = config_file(config)
    result = scanner.scan(path)

    vulns_by_level = result["vulnerabilities_by_level"]
    assert "CRITICAL" in vulns_by_level
    assert "HIGH" in vulns_by_level
    assert "MEDIUM" in vulns_by_level


def test_multiple_vulnerabilities(scanner: MCPSecurityScanner, config_file) -> None:
    """Should detect multiple vulnerabilities in a single config."""
    config = {
        "server": {"protocol": "http"},
        "authentication": {"api_key": "secret123"},
        "tools": [
            {
                "name": "tool1",
                "permissions": ["*"],
                "allowed_actions": ["read", "destroy_data", "exfiltrate"],
            }
        ],
    }
    path = config_file(config)
    result = scanner.scan(path)

    assert result["total_vulnerabilities"] >= 3


def test_recommendation_generation(scanner: MCPSecurityScanner, config_file) -> None:
    """Should generate a non-empty recommendation."""
    config = {
        "server": {"protocol": "https", "verified": True},
        "authentication": {"type": "mTLS"},
        "tools": [],
    }
    path = config_file(config)
    result = scanner.scan(path)

    recommendation = result.get("recommendation", "")
    assert recommendation
    assert len(recommendation) > 0


def test_file_system_access_without_restrictions(scanner: MCPSecurityScanner, config_file) -> None:
    """Should detect unrestricted file system access."""
    config = {
        "server": {"name": "Test Server"},
        "tools": [{"name": "file_system", "permissions": ["file_system"]}],
    }
    path = config_file(config)
    result = scanner.scan(path)

    vulns = result["vulnerabilities"]
    assert any("file system" in v.get("description", "").lower() for v in vulns)


def test_risk_levels_exist() -> None:
    """All expected risk levels should exist."""
    levels = [
        RiskLevel.CRITICAL,
        RiskLevel.HIGH,
        RiskLevel.MEDIUM,
        RiskLevel.LOW,
        RiskLevel.INFO,
    ]
    for level in levels:
        assert level.value is not None


# ── Finding dataclass tests ──────────────────────────────────────


class TestFindingDataclass:
    """Tests for the shared Finding type."""

    def test_finding_creation(self) -> None:
        """Finding can be created with all required fields."""
        f = Finding(
            id="TEST-001",
            title="Test Finding",
            description="A test finding",
            severity="HIGH",
            source="scanner",
            affected_field="server.protocol",
            remediation="Fix it",
            evidence="Found issue",
        )
        assert f.id == "TEST-001"
        assert f.title == "Test Finding"
        assert f.severity == "HIGH"
        assert f.source == "scanner"
        assert f.confidence == 1.0

    def test_finding_default_confidence(self) -> None:
        """Finding defaults to confidence 1.0."""
        f = Finding(
            id="TEST-002",
            title="Title",
            description="Desc",
            severity="LOW",
            source="blocklist",
            affected_field="field",
            remediation="remedy",
        )
        assert f.confidence == 1.0

    def test_finding_custom_confidence(self) -> None:
        """Finding accepts a custom confidence value."""
        f = Finding(
            id="TEST-003",
            title="Title",
            description="Desc",
            severity="MEDIUM",
            source="honeypot",
            affected_field="field",
            remediation="remedy",
            confidence=0.75,
        )
        assert f.confidence == 0.75

    def test_finding_empty_evidence(self) -> None:
        """Finding defaults evidence to empty string."""
        f = Finding(
            id="TEST-004",
            title="Title",
            description="Desc",
            severity="INFO",
            source="detector",
            affected_field="field",
            remediation="remedy",
        )
        assert f.evidence == ""

    def test_finding_empty_strings(self) -> None:
        """Finding works with empty string fields."""
        f = Finding(
            id="",
            title="",
            description="",
            severity="",
            source="",
            affected_field="",
            remediation="",
            evidence="",
        )
        assert f.id == ""
        assert f.title == ""


class TestSeverityEnum:
    """Tests for the Severity enum."""

    def test_all_levels_exist(self) -> None:
        """All five severity levels should exist."""
        assert Severity.CRITICAL.value == "CRITICAL"
        assert Severity.HIGH.value == "HIGH"
        assert Severity.MEDIUM.value == "MEDIUM"
        assert Severity.LOW.value == "LOW"
        assert Severity.INFO.value == "INFO"

    def test_severity_is_str(self) -> None:
        """Severity values should be usable as plain strings."""
        assert Severity.HIGH == "HIGH"
        assert isinstance(Severity.HIGH, str)


class TestVulnerabilityToFinding:
    """Tests for converting Vulnerability -> Finding."""

    def test_to_finding_basic(self) -> None:
        """to_finding() maps all fields correctly."""
        vuln = Vulnerability(
            id="CRED-API_KEY",
            title="Plaintext Credential Detected",
            description="Found plaintext api_key in configuration file",
            risk_level=RiskLevel.CRITICAL.value,
            affected_field="unknown",
            remediation="Use environment variables",
            evidence="Detected api_key pattern",
        )
        finding = vuln.to_finding()

        assert isinstance(finding, Finding)
        assert finding.id == vuln.id
        assert finding.title == vuln.title
        assert finding.description == vuln.description
        assert finding.severity == vuln.risk_level
        assert finding.source == "scanner"
        assert finding.affected_field == vuln.affected_field
        assert finding.remediation == vuln.remediation
        assert finding.evidence == vuln.evidence
        assert finding.confidence == 1.0

    def test_to_finding_empty_evidence(self) -> None:
        """to_finding() handles default (empty) evidence."""
        vuln = Vulnerability(
            id="AUTH-MISSING",
            title="Missing Auth",
            description="No auth",
            risk_level=RiskLevel.HIGH.value,
            affected_field="authentication",
            remediation="Add auth",
        )
        finding = vuln.to_finding()
        assert finding.evidence == ""


class TestScanReturnsFindings:
    """Tests that scan() returns Finding objects in the report."""

    def test_findings_key_present(
        self, scanner: MCPSecurityScanner, config_file, sample_secure_config: dict[str, Any]
    ) -> None:
        """Scan report should include a 'findings' key."""
        path = config_file(sample_secure_config)
        result = scanner.scan(path)
        assert "findings" in result

    def test_findings_are_finding_objects(
        self, scanner: MCPSecurityScanner, config_file
    ) -> None:
        """Each item in 'findings' should be a Finding instance."""
        config = {
            "server": {"protocol": "http"},
            "tools": [{"name": "tool1", "permissions": ["*"]}],
        }
        path = config_file(config)
        result = scanner.scan(path)

        assert len(result["findings"]) > 0
        for f in result["findings"]:
            assert isinstance(f, Finding)

    def test_findings_count_matches_vulnerabilities(
        self, scanner: MCPSecurityScanner, config_file
    ) -> None:
        """Number of findings should equal number of vulnerabilities."""
        config = {
            "server": {"protocol": "http"},
            "tools": [{"name": "tool1", "permissions": ["*"]}],
        }
        path = config_file(config)
        result = scanner.scan(path)

        assert len(result["findings"]) == len(result["vulnerabilities"])
        assert len(result["findings"]) == result["total_vulnerabilities"]

    def test_findings_source_is_scanner(
        self, scanner: MCPSecurityScanner, config_file
    ) -> None:
        """All findings from scanner should have source='scanner'."""
        config = {
            "server": {"protocol": "http"},
            "tools": [{"name": "tool1", "permissions": ["*"]}],
        }
        path = config_file(config)
        result = scanner.scan(path)

        for f in result["findings"]:
            assert f.source == "scanner"

    def test_findings_empty_on_secure_config(
        self, scanner: MCPSecurityScanner, config_file, sample_secure_config: dict[str, Any]
    ) -> None:
        """Secure config should produce zero findings."""
        path = config_file(sample_secure_config)
        result = scanner.scan(path)
        assert result["findings"] == []

    def test_error_report_has_no_findings(self, scanner: MCPSecurityScanner) -> None:
        """Error reports (bad file path) should not have a findings key."""
        result = scanner.scan("/nonexistent/path/config.json")
        assert result["status"] == "error"
        assert "findings" not in result
