"""Tests for the Risk Calculator module."""

from __future__ import annotations

import json

import pytest

from mcpaudit.risk_calculator import (
    DimensionScore,
    RiskAssessment,
    RiskCalculator,
    RiskDimension,
    RiskLevel,
    _score_to_level,
)


# ===========================================================================
# RiskLevel
# ===========================================================================

class TestRiskLevel:
    def test_all_values(self):
        assert len(RiskLevel) == 5

    def test_color(self):
        assert RiskLevel.MINIMAL.color == "green"
        assert RiskLevel.CRITICAL.color == "red"


# ===========================================================================
# RiskDimension
# ===========================================================================

class TestRiskDimension:
    def test_all_values(self):
        assert len(RiskDimension) == 5


# ===========================================================================
# _score_to_level
# ===========================================================================

class TestScoreToLevel:
    def test_minimal(self):
        assert _score_to_level(0) == RiskLevel.MINIMAL
        assert _score_to_level(15) == RiskLevel.MINIMAL

    def test_low(self):
        assert _score_to_level(16) == RiskLevel.LOW
        assert _score_to_level(35) == RiskLevel.LOW

    def test_moderate(self):
        assert _score_to_level(36) == RiskLevel.MODERATE
        assert _score_to_level(55) == RiskLevel.MODERATE

    def test_high(self):
        assert _score_to_level(56) == RiskLevel.HIGH
        assert _score_to_level(75) == RiskLevel.HIGH

    def test_critical(self):
        assert _score_to_level(76) == RiskLevel.CRITICAL
        assert _score_to_level(100) == RiskLevel.CRITICAL


# ===========================================================================
# DimensionScore
# ===========================================================================

class TestDimensionScore:
    def test_creation(self):
        d = DimensionScore(
            dimension=RiskDimension.ATTACK_SURFACE,
            score=50.0,
        )
        assert d.level == RiskLevel.MODERATE

    def test_weighted_score(self):
        d = DimensionScore(
            dimension=RiskDimension.CODE_EXECUTION,
            score=100.0,
        )
        # Code execution has weight 0.30
        assert d.weighted_score == 30.0

    def test_to_dict(self):
        d = DimensionScore(
            dimension=RiskDimension.DATA_EXPOSURE,
            score=40.0,
            factors=("sensitive path",),
            recommendation="restrict access",
        )
        result = d.to_dict()
        assert result["dimension"] == "data_exposure"
        assert result["score"] == 40.0
        assert result["level"] == "MODERATE"
        assert len(result["factors"]) == 1


# ===========================================================================
# RiskAssessment
# ===========================================================================

class TestRiskAssessment:
    def test_empty(self):
        a = RiskAssessment()
        assert a.composite_score == 0.0
        assert a.risk_level == RiskLevel.MINIMAL
        assert a.highest_risk is None
        assert a.lowest_risk is None

    def test_composite_score(self):
        a = RiskAssessment(dimensions=[
            DimensionScore(RiskDimension.CODE_EXECUTION, 100.0),  # 0.30 * 100 = 30
            DimensionScore(RiskDimension.ATTACK_SURFACE, 50.0),   # 0.15 * 50 = 7.5
        ])
        assert a.composite_score == 37.5

    def test_highest_lowest(self):
        d1 = DimensionScore(RiskDimension.CODE_EXECUTION, 80.0)
        d2 = DimensionScore(RiskDimension.ATTACK_SURFACE, 20.0)
        a = RiskAssessment(dimensions=[d1, d2])
        assert a.highest_risk == d1
        assert a.lowest_risk == d2

    def test_to_dict(self):
        a = RiskAssessment(
            dimensions=[DimensionScore(RiskDimension.NETWORK_ACCESS, 30.0)],
            metadata={"tools": 5},
        )
        d = a.to_dict()
        assert "composite_score" in d
        assert "risk_level" in d
        assert len(d["dimensions"]) == 1

    def test_to_json(self):
        a = RiskAssessment(
            dimensions=[DimensionScore(RiskDimension.NETWORK_ACCESS, 30.0)]
        )
        j = a.to_json()
        data = json.loads(j)
        assert data["risk_level"] in ("MINIMAL", "LOW")  # depends on weight


# ===========================================================================
# RiskCalculator - individual dimensions
# ===========================================================================

class TestRiskCalculatorTools:
    def test_no_tools(self):
        calc = RiskCalculator()
        result = calc.assess_tools([])
        assert result.score == 0.0

    def test_safe_tools(self):
        calc = RiskCalculator()
        result = calc.assess_tools(["Read", "Grep", "Glob"])
        assert result.score < 20

    def test_high_risk_tools(self):
        calc = RiskCalculator()
        result = calc.assess_tools(["Bash", "MCP"])
        assert result.score >= 50

    def test_all_tools(self):
        calc = RiskCalculator()
        result = calc.assess_tools(["Read", "Write", "Edit", "Bash", "Grep", "Glob", "MCP"])
        assert result.score >= 60
        assert result.recommendation != ""

    def test_factors_listed(self):
        calc = RiskCalculator()
        result = calc.assess_tools(["Bash"])
        assert any("Bash" in f for f in result.factors)


class TestRiskCalculatorPermissions:
    def test_read_only(self):
        calc = RiskCalculator()
        result = calc.assess_permissions("ReadOnly")
        assert result.score <= 15

    def test_workspace_write(self):
        calc = RiskCalculator()
        result = calc.assess_permissions("WorkspaceWrite")
        assert 30 <= result.score <= 50

    def test_full_access(self):
        calc = RiskCalculator()
        result = calc.assess_permissions("FullAccess")
        assert result.score >= 70
        assert result.recommendation != ""

    def test_wildcards(self):
        calc = RiskCalculator()
        result = calc.assess_permissions("WorkspaceWrite", ["**/*.py", "src/*"])
        assert result.score > 40
        assert any("wildcard" in f for f in result.factors)


class TestRiskCalculatorCodeExecution:
    def test_no_execution(self):
        calc = RiskCalculator()
        result = calc.assess_code_execution(False, False)
        assert result.score == 0.0

    def test_bash_only(self):
        calc = RiskCalculator()
        result = calc.assess_code_execution(True, False)
        assert result.score >= 50

    def test_both(self):
        calc = RiskCalculator()
        result = calc.assess_code_execution(True, True)
        assert result.score >= 80
        assert result.recommendation != ""

    def test_extended_steps(self):
        calc = RiskCalculator()
        result = calc.assess_code_execution(False, False, max_steps=50)
        assert result.score > 0


class TestRiskCalculatorDataExposure:
    def test_no_sensitive(self):
        calc = RiskCalculator()
        result = calc.assess_data_exposure([])
        assert result.score == 0.0

    def test_sensitive_paths(self):
        calc = RiskCalculator()
        result = calc.assess_data_exposure([".env", "credentials.json"])
        assert result.score >= 50

    def test_web_access(self):
        calc = RiskCalculator()
        result = calc.assess_data_exposure([], has_web_access=True)
        assert result.score >= 20

    def test_combined(self):
        calc = RiskCalculator()
        result = calc.assess_data_exposure(["secrets/key.pem", ".env"], has_web_access=True)
        assert result.score >= 50
        assert result.recommendation != ""


class TestRiskCalculatorNetwork:
    def test_no_network(self):
        calc = RiskCalculator()
        result = calc.assess_network(False, False)
        assert result.score == 0.0

    def test_web_search(self):
        calc = RiskCalculator()
        result = calc.assess_network(True, False)
        assert result.score >= 20

    def test_web_fetch(self):
        calc = RiskCalculator()
        result = calc.assess_network(False, True)
        assert result.score >= 30

    def test_all_network(self):
        calc = RiskCalculator()
        result = calc.assess_network(True, True, True)
        assert result.score >= 70


# ===========================================================================
# RiskCalculator - full assessment
# ===========================================================================

class TestRiskCalculatorFullAssessment:
    def test_minimal_config(self):
        calc = RiskCalculator()
        result = calc.full_assessment(
            tools=["Read", "Grep"],
            sandbox_tier="ReadOnly",
        )
        assert result.composite_score < 30
        assert len(result.dimensions) == 5

    def test_high_risk_config(self):
        calc = RiskCalculator()
        result = calc.full_assessment(
            tools=["Read", "Write", "Edit", "Bash", "MCP", "WebFetch"],
            sandbox_tier="FullAccess",
            sensitive_paths=[".env", "secrets/"],
            max_steps=50,
        )
        assert result.composite_score > 50
        assert result.risk_level in (RiskLevel.HIGH, RiskLevel.CRITICAL)

    def test_metadata(self):
        calc = RiskCalculator()
        result = calc.full_assessment(
            tools=["Read"],
            sandbox_tier="ReadOnly",
            max_steps=5,
        )
        assert result.metadata["tools_count"] == 1
        assert result.metadata["sandbox_tier"] == "ReadOnly"


# ===========================================================================
# RiskCalculator - compare
# ===========================================================================

class TestRiskCalculatorCompare:
    def test_compare_improved(self):
        calc = RiskCalculator()
        a = calc.full_assessment(["Bash", "MCP"], "FullAccess")
        b = calc.full_assessment(["Read"], "ReadOnly")
        comp = RiskCalculator.compare(a, b)
        assert comp["direction"] == "IMPROVED"
        assert comp["composite_delta"] < 0

    def test_compare_degraded(self):
        calc = RiskCalculator()
        a = calc.full_assessment(["Read"], "ReadOnly")
        b = calc.full_assessment(["Bash", "MCP"], "FullAccess")
        comp = RiskCalculator.compare(a, b)
        assert comp["direction"] == "DEGRADED"
        assert comp["composite_delta"] > 0

    def test_compare_unchanged(self):
        calc = RiskCalculator()
        a = calc.full_assessment(["Read"], "ReadOnly")
        b = calc.full_assessment(["Read"], "ReadOnly")
        comp = RiskCalculator.compare(a, b)
        assert comp["direction"] == "UNCHANGED"

    def test_compare_dimensions(self):
        calc = RiskCalculator()
        a = calc.full_assessment(["Read"], "ReadOnly")
        b = calc.full_assessment(["Bash"], "WorkspaceWrite")
        comp = RiskCalculator.compare(a, b)
        assert "code_execution" in comp["dimensions"]


# ===========================================================================
# RiskCalculator - report
# ===========================================================================

class TestRiskCalculatorReport:
    def test_markdown_report(self):
        calc = RiskCalculator()
        assessment = calc.full_assessment(
            tools=["Read", "Bash", "Write"],
            sandbox_tier="WorkspaceWrite",
        )
        report = calc.markdown_report(assessment)
        assert "# Risk Assessment Report" in report
        assert "Composite Score" in report
        assert "Dimensions" in report
        assert "Risk Factors" in report


# ===========================================================================
# RiskCalculator - custom weights
# ===========================================================================

class TestRiskCalculatorCustomWeights:
    def test_custom_weights(self):
        calc = RiskCalculator(custom_weights={"code_execution": 0.50})
        # Custom weights are stored on the calculator for future use
        assert calc._weights[RiskDimension.CODE_EXECUTION] == 0.50
        result = calc.full_assessment(["Bash"], "ReadOnly")
        ce_dim = next(d for d in result.dimensions if d.dimension == RiskDimension.CODE_EXECUTION)
        assert ce_dim.score >= 50  # Bash alone is 50 points
