"""Tests for the Trust Score module (mcpaudit.trust_score).

Tests the composite scoring algorithm with capped deductions,
bonus points, label mapping, CI/CD gate integration, and
JSON/text output formatting.
"""

from __future__ import annotations

import json
import tempfile
from pathlib import Path

import pytest

from mcpaudit.models import Finding, ScanResult, ScanSummary, Severity
from mcpaudit.trust_score import (
    TrustScoreResult,
    compute_trust_score,
    compute_summary_trust_score,
    format_trust_score_text,
    _score_label,
    _has_read_only_patterns,
    BASE_SCORE,
    SEVERITY_DEDUCTIONS,
    MAX_DEDUCTIONS,
    BONUS_ZERO_FINDINGS,
    BONUS_READ_ONLY,
)


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

def _make_findings(
    critical: int = 0,
    high: int = 0,
    medium: int = 0,
    low: int = 0,
    info: int = 0,
) -> list[Finding]:
    """Create a list of findings with specified severity counts."""
    findings: list[Finding] = []
    for i in range(critical):
        findings.append(Finding(
            rule_id=f"CRIT-{i}", severity=Severity.CRITICAL,
            title="test", description="test", file="t.py", line=i + 1,
        ))
    for i in range(high):
        findings.append(Finding(
            rule_id=f"HIGH-{i}", severity=Severity.HIGH,
            title="test", description="test", file="t.py", line=i + 1,
        ))
    for i in range(medium):
        findings.append(Finding(
            rule_id=f"MED-{i}", severity=Severity.MEDIUM,
            title="test", description="test", file="t.py", line=i + 1,
        ))
    for i in range(low):
        findings.append(Finding(
            rule_id=f"LOW-{i}", severity=Severity.LOW,
            title="test", description="test", file="t.py", line=i + 1,
        ))
    for i in range(info):
        findings.append(Finding(
            rule_id=f"INFO-{i}", severity=Severity.INFO,
            title="test", description="test", file="t.py", line=i + 1,
        ))
    return findings


# ===========================================================================
# 1. Base Score
# ===========================================================================

class TestBaseScore:
    """Tests for base score and zero-findings behavior."""

    def test_zero_findings_gives_105_clamped_to_100(self):
        """No findings: base 100 + bonus 5 = 105, clamped to 100."""
        result = compute_trust_score([])
        assert result.score == 100

    def test_base_is_100(self):
        assert BASE_SCORE == 100

    def test_zero_findings_bonus(self):
        result = compute_trust_score([])
        assert result.bonuses.get("zero_findings") == BONUS_ZERO_FINDINGS

    def test_score_never_negative(self):
        """Even with many criticals, score is clamped to 0."""
        findings = _make_findings(critical=10)
        result = compute_trust_score(findings)
        assert result.score >= 0


# ===========================================================================
# 2. Severity Deductions
# ===========================================================================

class TestSeverityDeductions:
    """Tests for per-severity deduction values."""

    def test_single_critical_deduction(self):
        """One CRITICAL: 100 - 25 = 75."""
        findings = _make_findings(critical=1)
        result = compute_trust_score(findings)
        assert result.score == 75

    def test_single_high_deduction(self):
        """One HIGH: 100 - 15 = 85."""
        findings = _make_findings(high=1)
        result = compute_trust_score(findings)
        assert result.score == 85

    def test_single_medium_deduction(self):
        """One MEDIUM: 100 - 8 = 92."""
        findings = _make_findings(medium=1)
        result = compute_trust_score(findings)
        assert result.score == 92

    def test_single_low_deduction(self):
        """One LOW: 100 - 3 = 97."""
        findings = _make_findings(low=1)
        result = compute_trust_score(findings)
        assert result.score == 97

    def test_single_info_deduction(self):
        """One INFO: 100 - 1 = 99."""
        findings = _make_findings(info=1)
        result = compute_trust_score(findings)
        assert result.score == 99


# ===========================================================================
# 3. Capped Deductions
# ===========================================================================

class TestCappedDeductions:
    """Tests that deductions are capped per severity level."""

    def test_critical_capped_at_2(self):
        """5 CRITICAL findings: only 2 counted, so 100 - 50 = 50."""
        findings = _make_findings(critical=5)
        result = compute_trust_score(findings)
        assert result.capped_counts["CRITICAL"] == 2
        assert result.score == 50

    def test_high_capped_at_3(self):
        """10 HIGH findings: only 3 counted, so 100 - 45 = 55."""
        findings = _make_findings(high=10)
        result = compute_trust_score(findings)
        assert result.capped_counts["HIGH"] == 3
        assert result.score == 55

    def test_medium_capped_at_4(self):
        """10 MEDIUM: only 4 counted, so 100 - 32 = 68."""
        findings = _make_findings(medium=10)
        result = compute_trust_score(findings)
        assert result.capped_counts["MEDIUM"] == 4
        assert result.score == 68

    def test_low_capped_at_5(self):
        """20 LOW: only 5 counted, so 100 - 15 = 85."""
        findings = _make_findings(low=20)
        result = compute_trust_score(findings)
        assert result.capped_counts["LOW"] == 5
        assert result.score == 85

    def test_info_capped_at_10(self):
        """50 INFO: only 10 counted, so 100 - 10 = 90."""
        findings = _make_findings(info=50)
        result = compute_trust_score(findings)
        assert result.capped_counts["INFO"] == 10
        assert result.score == 90

    def test_under_cap_uses_actual_count(self):
        """2 HIGH (cap 3): uses actual count of 2."""
        findings = _make_findings(high=2)
        result = compute_trust_score(findings)
        assert result.capped_counts["HIGH"] == 2
        assert result.score == 70  # 100 - 30


# ===========================================================================
# 4. Combined Severities
# ===========================================================================

class TestCombinedSeverities:
    """Tests for mixed severity combinations."""

    def test_mixed_critical_and_high(self):
        """1 CRITICAL + 1 HIGH: 100 - 25 - 15 = 60."""
        findings = _make_findings(critical=1, high=1)
        result = compute_trust_score(findings)
        assert result.score == 60

    def test_all_severities(self):
        """1 of each: 100 - 25 - 15 - 8 - 3 - 1 = 48."""
        findings = _make_findings(critical=1, high=1, medium=1, low=1, info=1)
        result = compute_trust_score(findings)
        assert result.score == 48

    def test_max_deductions_all_capped(self):
        """All severities at cap: 100 - 50 - 45 - 32 - 15 - 10 = 0 (clamped)."""
        findings = _make_findings(critical=5, high=10, medium=10, low=20, info=50)
        result = compute_trust_score(findings)
        assert result.score == 0


# ===========================================================================
# 5. Bonus Points
# ===========================================================================

class TestBonusPoints:
    """Tests for bonus point calculation."""

    def test_zero_findings_bonus_applied(self):
        result = compute_trust_score([])
        assert "zero_findings" in result.bonuses
        assert result.bonuses["zero_findings"] == 5

    def test_read_only_bonus_with_pattern(self):
        result = compute_trust_score([], source_hint="read_only = True")
        assert "read_only" in result.bonuses
        assert result.bonuses["read_only"] == 3

    def test_read_only_bonus_with_GET(self):
        result = compute_trust_score([], source_hint='method = "GET"')
        assert "read_only" in result.bonuses

    def test_no_read_only_bonus_without_pattern(self):
        result = compute_trust_score(_make_findings(low=1))
        assert "read_only" not in result.bonuses

    def test_both_bonuses_clamped_to_100(self):
        """Zero findings + read-only: 100 + 5 + 3 = 108, clamped to 100."""
        result = compute_trust_score([], source_hint="readonly mode")
        assert result.score == 100


# ===========================================================================
# 6. Score Labels
# ===========================================================================

class TestScoreLabels:
    """Tests for score-to-label mapping."""

    def test_label_excellent(self):
        assert _score_label(90) == "EXCELLENT"
        assert _score_label(100) == "EXCELLENT"

    def test_label_good(self):
        assert _score_label(75) == "GOOD"
        assert _score_label(89) == "GOOD"

    def test_label_moderate(self):
        assert _score_label(50) == "MODERATE"
        assert _score_label(74) == "MODERATE"

    def test_label_low(self):
        assert _score_label(25) == "LOW"
        assert _score_label(49) == "LOW"

    def test_label_critical(self):
        assert _score_label(0) == "CRITICAL"
        assert _score_label(24) == "CRITICAL"


# ===========================================================================
# 7. TrustScoreResult Model
# ===========================================================================

class TestTrustScoreResult:
    """Tests for TrustScoreResult data model."""

    def test_to_dict_structure(self):
        result = compute_trust_score([])
        d = result.to_dict()
        assert "score" in d
        assert "label" in d
        assert "base" in d
        assert "deductions" in d
        assert "bonuses" in d
        assert "finding_counts" in d
        assert "capped_counts" in d

    def test_to_dict_values(self):
        findings = _make_findings(critical=1)
        result = compute_trust_score(findings)
        d = result.to_dict()
        assert d["score"] == 75
        assert d["base"] == 100
        assert d["deductions"]["CRITICAL"] == 25

    def test_finding_counts_accurate(self):
        findings = _make_findings(critical=2, high=3, medium=1)
        result = compute_trust_score(findings)
        assert result.finding_counts["CRITICAL"] == 2
        assert result.finding_counts["HIGH"] == 3
        assert result.finding_counts["MEDIUM"] == 1
        assert result.finding_counts["LOW"] == 0


# ===========================================================================
# 8. Summary-Level Trust Score
# ===========================================================================

class TestSummaryTrustScore:
    """Tests for compute_summary_trust_score."""

    def test_empty_summary(self):
        summary = ScanSummary(results=[])
        result = compute_summary_trust_score(summary)
        assert result.score == 100

    def test_summary_aggregates_findings(self):
        """Findings across files are aggregated."""
        summary = ScanSummary(results=[
            ScanResult(file="a.py", findings=[
                Finding(rule_id="A", severity=Severity.CRITICAL,
                        title="", description="", file="a.py", line=1),
            ]),
            ScanResult(file="b.py", findings=[
                Finding(rule_id="B", severity=Severity.HIGH,
                        title="", description="", file="b.py", line=1),
            ]),
        ])
        result = compute_summary_trust_score(summary)
        # 100 - 25 - 15 = 60
        assert result.score == 60

    def test_summary_with_source_hint(self):
        summary = ScanSummary(results=[
            ScanResult(file="a.py", lines_scanned=10),
        ])
        result = compute_summary_trust_score(summary, source_hint="read_only")
        assert "read_only" in result.bonuses


# ===========================================================================
# 9. Text Formatting
# ===========================================================================

class TestTextFormatting:
    """Tests for trust score text output."""

    def test_format_includes_score(self):
        result = compute_trust_score([])
        text = format_trust_score_text(result)
        assert "100/100" in text
        assert "EXCELLENT" in text

    def test_format_includes_deductions(self):
        findings = _make_findings(critical=1)
        result = compute_trust_score(findings)
        text = format_trust_score_text(result)
        assert "Deductions" in text
        assert "CRITICAL" in text

    def test_format_includes_bonuses(self):
        result = compute_trust_score([], source_hint="readonly")
        text = format_trust_score_text(result)
        assert "Bonuses" in text

    def test_format_no_deductions_no_deductions_line(self):
        result = compute_trust_score([])
        text = format_trust_score_text(result)
        assert "Deductions" not in text


# ===========================================================================
# 10. CLI Integration (--min-score gate)
# ===========================================================================

class TestCLIIntegration:
    """Tests for --min-score CLI integration."""

    def test_min_score_pass(self, tmp_path: Path):
        """Clean file with --min-score 50 passes (exit 0)."""
        f = tmp_path / "clean.py"
        f.write_text("x = 1\n")
        from mcpaudit.__main__ import main
        exit_code = main([str(f), "--min-score", "50"])
        assert exit_code == 0

    def test_min_score_fail(self, tmp_path: Path):
        """File with findings below min-score threshold returns exit 1."""
        f = tmp_path / "bad.py"
        f.write_text('import subprocess\nsubprocess.run("ls", shell=True)\n')
        from mcpaudit.__main__ import main
        exit_code = main([str(f), "--min-score", "99"])
        assert exit_code != 0

    def test_min_score_exact_boundary(self, tmp_path: Path):
        """Score exactly at threshold should pass."""
        f = tmp_path / "clean.py"
        f.write_text("x = 1\n")
        from mcpaudit.__main__ import main
        # Score is 100 for clean file, --min-score 100 should pass
        exit_code = main([str(f), "--min-score", "100"])
        assert exit_code == 0

    def test_json_output_includes_trust_score(self, tmp_path: Path, capsys):
        """JSON output includes trust_score in summary."""
        f = tmp_path / "test.py"
        f.write_text("x = 1\n")
        from mcpaudit.__main__ import main
        main([str(f), "--format", "json"])
        captured = capsys.readouterr()
        data = json.loads(captured.out)
        assert "trust_score" in data["summary"]
        assert data["summary"]["trust_score"]["score"] == 100

    def test_text_output_includes_trust_score(self, tmp_path: Path, capsys):
        """Text output includes Trust Score line."""
        f = tmp_path / "test.py"
        f.write_text("x = 1\n")
        from mcpaudit.__main__ import main
        main([str(f)])
        captured = capsys.readouterr()
        assert "Trust Score:" in captured.out


# ===========================================================================
# 11. Read-Only Pattern Detection
# ===========================================================================

class TestReadOnlyPatterns:
    """Tests for _has_read_only_patterns helper."""

    def test_read_only_underscore(self):
        assert _has_read_only_patterns([], "read_only = True")

    def test_readonly_no_separator(self):
        assert _has_read_only_patterns([], "readonly mode enabled")

    def test_read_only_hyphen(self):
        assert _has_read_only_patterns([], "read-only server")

    def test_get_method(self):
        assert _has_read_only_patterns([], 'method = "GET"')

    def test_list_tools(self):
        assert _has_read_only_patterns([], "def list_tools():")

    def test_no_pattern(self):
        assert not _has_read_only_patterns([], "def process_data():")

    def test_describe_pattern(self):
        assert _has_read_only_patterns([], "describe the resource")

    def test_schema_pattern(self):
        assert _has_read_only_patterns([], "return schema")
