"""Tests for the Security Posture Score (mcpaudit.posture_score).

Tests posture scoring across 8 dimensions, grade computation,
industry benchmarking, trend analysis, and format helpers.
"""

from __future__ import annotations

import json

import pytest

from mcpaudit.posture_score import (
    DEFAULT_WEIGHTS,
    DimensionResult,
    PostureDimension,
    PostureScore,
    TrendPoint,
    calculate_posture,
    classify_dimension,
    format_posture_html,
    format_posture_json,
    format_posture_text,
    _compute_dimension_score,
    _compute_percentile,
    _compute_trend,
    _extract_findings,
    _grade_color,
    _score_bar,
    _score_color,
    _score_to_grade,
)


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

def _make_finding(
    rule_id: str = "CMD-001",
    severity: str = "MEDIUM",
) -> dict:
    return {
        "rule_id": rule_id,
        "severity": severity,
        "title": f"Test {rule_id}",
        "description": "test finding",
        "file": "server.py",
        "line": 10,
    }


def _make_scan(
    findings: list[dict] | None = None,
    target: str = "test-server.py",
) -> dict:
    if findings is None:
        findings = []
    return {
        "summary": {"target": target, "total_findings": len(findings)},
        "results": [{"file": "server.py", "findings": findings}],
    }


# ===========================================================================
# PostureDimension Enum
# ===========================================================================

class TestPostureDimension:
    def test_all_8_dimensions(self) -> None:
        assert len(PostureDimension) == 8

    def test_values(self) -> None:
        assert PostureDimension.CODE_QUALITY.value == "code_quality"
        assert PostureDimension.AUTH.value == "auth"
        assert PostureDimension.DATA_PROTECTION.value == "data_protection"
        assert PostureDimension.INPUT_VALIDATION.value == "input_validation"
        assert PostureDimension.LOGGING.value == "logging"
        assert PostureDimension.CONFIG.value == "config"
        assert PostureDimension.DEPENDENCIES.value == "dependencies"
        assert PostureDimension.COMPLIANCE.value == "compliance"


# ===========================================================================
# classify_dimension
# ===========================================================================

class TestClassifyDimension:
    def test_auth_rules(self) -> None:
        assert classify_dimension("AUTH-001") == PostureDimension.AUTH
        assert classify_dimension("RATE-001") == PostureDimension.AUTH
        assert classify_dimension("PRIV-001") == PostureDimension.AUTH

    def test_data_rules(self) -> None:
        assert classify_dimension("DATA-001") == PostureDimension.DATA_PROTECTION
        assert classify_dimension("LEAK-001") == PostureDimension.DATA_PROTECTION
        assert classify_dimension("SECRET-001") == PostureDimension.DATA_PROTECTION

    def test_input_validation_rules(self) -> None:
        assert classify_dimension("INJ-001") == PostureDimension.INPUT_VALIDATION
        assert classify_dimension("PROMPT-001") == PostureDimension.INPUT_VALIDATION
        assert classify_dimension("TRAV-001") == PostureDimension.INPUT_VALIDATION

    def test_logging_rules(self) -> None:
        assert classify_dimension("LOG-001") == PostureDimension.LOGGING

    def test_config_rules(self) -> None:
        assert classify_dimension("GUARD-001") == PostureDimension.CONFIG
        assert classify_dimension("CONF-001") == PostureDimension.CONFIG
        assert classify_dimension("PERM-001") == PostureDimension.CONFIG

    def test_dependency_rules(self) -> None:
        assert classify_dimension("DEP-001") == PostureDimension.DEPENDENCIES
        assert classify_dimension("SUPPLY-001") == PostureDimension.DEPENDENCIES

    def test_compliance_rules(self) -> None:
        assert classify_dimension("OWASP-001") == PostureDimension.COMPLIANCE
        assert classify_dimension("MCP-001") == PostureDimension.COMPLIANCE

    def test_code_quality_default(self) -> None:
        assert classify_dimension("CMD-001") == PostureDimension.CODE_QUALITY
        assert classify_dimension("FLOW-001") == PostureDimension.CODE_QUALITY

    def test_unknown_prefix_defaults_code_quality(self) -> None:
        assert classify_dimension("UNKNOWN-001") == PostureDimension.CODE_QUALITY


# ===========================================================================
# _score_to_grade
# ===========================================================================

class TestScoreToGrade:
    def test_a_plus(self) -> None:
        assert _score_to_grade(100) == "A+"
        assert _score_to_grade(95) == "A+"

    def test_a(self) -> None:
        assert _score_to_grade(90) == "A"
        assert _score_to_grade(94) == "A"

    def test_b(self) -> None:
        assert _score_to_grade(80) == "B"
        assert _score_to_grade(89) == "B"

    def test_c(self) -> None:
        assert _score_to_grade(70) == "C"
        assert _score_to_grade(79) == "C"

    def test_d(self) -> None:
        assert _score_to_grade(60) == "D"
        assert _score_to_grade(69) == "D"

    def test_f(self) -> None:
        assert _score_to_grade(59) == "F"
        assert _score_to_grade(0) == "F"


# ===========================================================================
# _compute_dimension_score
# ===========================================================================

class TestComputeDimensionScore:
    def test_no_findings_perfect_score(self) -> None:
        result = _compute_dimension_score([], PostureDimension.AUTH)
        assert result.score == 100
        assert result.finding_count == 0

    def test_single_medium_finding(self) -> None:
        findings = [_make_finding("AUTH-001", "MEDIUM")]
        result = _compute_dimension_score(findings, PostureDimension.AUTH)
        assert result.score == 90  # 100 - 10
        assert result.finding_count == 1

    def test_critical_finding_heavy_penalty(self) -> None:
        findings = [_make_finding("AUTH-001", "CRITICAL")]
        result = _compute_dimension_score(findings, PostureDimension.AUTH)
        assert result.score == 70  # 100 - 30

    def test_multiple_findings(self) -> None:
        findings = [
            _make_finding("AUTH-001", "HIGH"),
            _make_finding("AUTH-002", "MEDIUM"),
        ]
        result = _compute_dimension_score(findings, PostureDimension.AUTH)
        assert result.score == 72  # 100 - 18 - 10

    def test_score_never_negative(self) -> None:
        findings = [_make_finding("AUTH-001", "CRITICAL")] * 10
        result = _compute_dimension_score(findings, PostureDimension.AUTH)
        assert result.score == 0

    def test_ignores_other_dimensions(self) -> None:
        findings = [_make_finding("CMD-001", "CRITICAL")]
        result = _compute_dimension_score(findings, PostureDimension.AUTH)
        assert result.score == 100  # CMD is code_quality, not auth

    def test_details_populated(self) -> None:
        findings = [_make_finding("AUTH-001", "HIGH")]
        result = _compute_dimension_score(findings, PostureDimension.AUTH)
        assert len(result.details) == 1
        assert "AUTH-001" in result.details[0]

    def test_grade_assigned(self) -> None:
        findings = [_make_finding("AUTH-001", "HIGH")]
        result = _compute_dimension_score(findings, PostureDimension.AUTH)
        assert result.grade == "B"  # score 82


# ===========================================================================
# _compute_percentile
# ===========================================================================

class TestComputePercentile:
    def test_high_score(self) -> None:
        assert _compute_percentile(95) == 95

    def test_mid_score(self) -> None:
        assert _compute_percentile(70) == 50

    def test_low_score(self) -> None:
        pctl = _compute_percentile(30)
        assert pctl <= 3

    def test_perfect_score(self) -> None:
        assert _compute_percentile(100) == 95


# ===========================================================================
# _compute_trend
# ===========================================================================

class TestComputeTrend:
    def test_single_point_stable(self) -> None:
        points = [TrendPoint(timestamp="t", score=80, grade="B")]
        assert _compute_trend(points) == "stable"

    def test_improving(self) -> None:
        points = [
            TrendPoint(timestamp="t1", score=60, grade="D"),
            TrendPoint(timestamp="t2", score=70, grade="C"),
            TrendPoint(timestamp="t3", score=80, grade="B"),
        ]
        assert _compute_trend(points) == "improving"

    def test_degrading(self) -> None:
        points = [
            TrendPoint(timestamp="t1", score=90, grade="A"),
            TrendPoint(timestamp="t2", score=80, grade="B"),
            TrendPoint(timestamp="t3", score=65, grade="D"),
        ]
        assert _compute_trend(points) == "degrading"

    def test_stable(self) -> None:
        points = [
            TrendPoint(timestamp="t1", score=80, grade="B"),
            TrendPoint(timestamp="t2", score=80, grade="B"),
        ]
        assert _compute_trend(points) == "stable"


# ===========================================================================
# DimensionResult
# ===========================================================================

class TestDimensionResult:
    def test_to_dict(self) -> None:
        dr = DimensionResult(dimension="auth", score=85, weight=0.15,
                             finding_count=2, penalty=15, grade="B")
        d = dr.to_dict()
        assert d["dimension"] == "auth"
        assert d["score"] == 85
        assert d["grade"] == "B"

    def test_defaults(self) -> None:
        dr = DimensionResult(dimension="test", score=100)
        assert dr.weight == 0.125
        assert dr.finding_count == 0
        assert dr.grade == "A+"


class TestTrendPoint:
    def test_to_dict(self) -> None:
        tp = TrendPoint(timestamp="2026-01-01", score=85, grade="B")
        d = tp.to_dict()
        assert d["score"] == 85


class TestPostureScoreModel:
    def test_to_dict(self) -> None:
        ps = PostureScore(overall_score=85, grade="B", percentile=70)
        d = ps.to_dict()
        assert d["overall_score"] == 85
        assert d["grade"] == "B"

    def test_defaults(self) -> None:
        ps = PostureScore()
        assert ps.overall_score == 100
        assert ps.grade == "A+"


# ===========================================================================
# _extract_findings
# ===========================================================================

class TestExtractFindings:
    def test_basic(self) -> None:
        scan = _make_scan([_make_finding(), _make_finding()])
        findings = _extract_findings(scan)
        assert len(findings) == 2

    def test_empty_scan(self) -> None:
        assert _extract_findings({}) == []

    def test_multiple_files(self) -> None:
        scan = {
            "results": [
                {"file": "a.py", "findings": [_make_finding()]},
                {"file": "b.py", "findings": [_make_finding(), _make_finding()]},
            ],
        }
        assert len(_extract_findings(scan)) == 3


# ===========================================================================
# DEFAULT_WEIGHTS
# ===========================================================================

class TestDefaultWeights:
    def test_sum_to_one(self) -> None:
        assert abs(sum(DEFAULT_WEIGHTS.values()) - 1.0) < 0.001

    def test_all_dimensions_present(self) -> None:
        for dim in PostureDimension:
            assert dim.value in DEFAULT_WEIGHTS


# ===========================================================================
# calculate_posture
# ===========================================================================

class TestCalculatePosture:
    def test_clean_scan_perfect_score(self) -> None:
        scan = _make_scan([])
        result = calculate_posture(scan)
        assert result.overall_score == 100
        assert result.grade == "A+"

    def test_with_findings(self) -> None:
        scan = _make_scan([
            _make_finding("AUTH-001", "HIGH"),
            _make_finding("INJ-001", "CRITICAL"),
        ])
        result = calculate_posture(scan)
        assert result.overall_score < 100
        assert result.total_findings == 2

    def test_dimensions_populated(self) -> None:
        scan = _make_scan([_make_finding("AUTH-001", "HIGH")])
        result = calculate_posture(scan)
        assert len(result.dimensions) == 8

    def test_custom_weights(self) -> None:
        scan = _make_scan([_make_finding("AUTH-001", "CRITICAL")])
        # Give auth 100% weight
        weights = {dim.value: 0.0 for dim in PostureDimension}
        weights["auth"] = 1.0
        result = calculate_posture(scan, weights=weights)
        # Auth got a critical finding (penalty 30), so score ~70
        assert result.overall_score <= 75

    def test_with_history(self) -> None:
        scan = _make_scan([])
        history = [
            {"timestamp": "2026-01-01", "score": 60, "grade": "D"},
            {"timestamp": "2026-02-01", "score": 70, "grade": "C"},
        ]
        result = calculate_posture(scan, history=history)
        assert len(result.trend_points) == 3  # 2 historical + 1 current
        assert result.trend == "improving"

    def test_target_extraction(self) -> None:
        scan = _make_scan(target="my-server")
        result = calculate_posture(scan)
        assert result.scan_target == "my-server"

    def test_timestamp_set(self) -> None:
        scan = _make_scan([])
        result = calculate_posture(scan)
        assert result.timestamp

    def test_weights_preserved(self) -> None:
        scan = _make_scan([])
        result = calculate_posture(scan)
        assert len(result.weights_used) == 8

    def test_percentile_set(self) -> None:
        scan = _make_scan([])
        result = calculate_posture(scan)
        assert result.percentile > 0

    def test_heavy_findings_low_score(self) -> None:
        findings = []
        for prefix in ["AUTH", "INJ", "DATA", "CMD", "LOG", "CONF", "DEP", "OWASP"]:
            findings.extend([
                _make_finding(f"{prefix}-001", "CRITICAL"),
                _make_finding(f"{prefix}-002", "HIGH"),
            ])
        scan = _make_scan(findings)
        result = calculate_posture(scan)
        assert result.overall_score < 60
        assert result.grade in ("D", "F")

    def test_empty_scan(self) -> None:
        result = calculate_posture({})
        assert result.overall_score == 100

    def test_target_from_first_result(self) -> None:
        scan = {
            "summary": {},
            "results": [{"file": "special.py", "findings": []}],
        }
        result = calculate_posture(scan)
        assert result.scan_target == "special.py"


# ===========================================================================
# Format functions
# ===========================================================================

class TestFormatPostureJson:
    def test_valid_json(self) -> None:
        scan = _make_scan([_make_finding()])
        posture = calculate_posture(scan)
        j = format_posture_json(posture)
        data = json.loads(j)
        assert "overall_score" in data
        assert "dimensions" in data

    def test_roundtrip(self) -> None:
        scan = _make_scan([])
        posture = calculate_posture(scan)
        data = json.loads(format_posture_json(posture))
        assert data["grade"] == "A+"


class TestFormatPostureText:
    def test_header(self) -> None:
        posture = calculate_posture(_make_scan([]))
        text = format_posture_text(posture)
        assert "SECURITY POSTURE SCORE" in text

    def test_dimensions_listed(self) -> None:
        posture = calculate_posture(_make_scan([]))
        text = format_posture_text(posture)
        assert "code_quality" in text
        assert "auth" in text

    def test_score_bar(self) -> None:
        posture = calculate_posture(_make_scan([]))
        text = format_posture_text(posture)
        assert "[" in text and "]" in text


class TestFormatPostureHtml:
    def test_returns_html(self) -> None:
        posture = calculate_posture(_make_scan([]))
        html = format_posture_html(posture)
        assert "<!DOCTYPE html>" in html
        assert "Security Posture" in html

    def test_contains_grade(self) -> None:
        posture = calculate_posture(_make_scan([]))
        html = format_posture_html(posture)
        assert posture.grade in html

    def test_contains_dimensions(self) -> None:
        posture = calculate_posture(_make_scan([]))
        html = format_posture_html(posture)
        assert "code_quality" in html


# ===========================================================================
# Internal helpers
# ===========================================================================

class TestScoreBar:
    def test_full(self) -> None:
        bar = _score_bar(100)
        assert "#" * 20 in bar

    def test_empty(self) -> None:
        bar = _score_bar(0)
        assert "." * 20 in bar

    def test_half(self) -> None:
        bar = _score_bar(50)
        assert "#" * 10 in bar


class TestScoreColor:
    def test_high(self) -> None:
        assert _score_color(95) == "#22c55e"

    def test_medium(self) -> None:
        assert _score_color(65) == "#eab308"

    def test_low(self) -> None:
        assert _score_color(30) == "#dc2626"


class TestGradeColor:
    def test_a_plus(self) -> None:
        assert _grade_color("A+") == "#22c55e"

    def test_f(self) -> None:
        assert _grade_color("F") == "#dc2626"

    def test_unknown(self) -> None:
        assert _grade_color("Z") == "#6b7280"
