"""Tests for the policy engine — organizational policy enforcement."""

from __future__ import annotations

import json
import textwrap
from pathlib import Path

import pytest

from mcpaudit.models import Finding, ScanResult, ScanSummary, Severity
from mcpaudit.policy import (
    Policy,
    PolicyAction,
    PolicyEvaluation,
    PolicyResult,
    PolicyRule,
    format_policy_json,
    format_policy_text,
    load_policy,
    evaluate_policy,
)


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
def basic_policy_yaml(tmp_path: Path) -> Path:
    """Create a basic policy YAML file."""
    p = tmp_path / "policy.yaml"
    p.write_text(textwrap.dedent("""\
        policies:
          - name: "no-command-injection"
            rule: CMD-001
            severity: critical
            action: block
          - name: "require-auth"
            rule: AUTH-001
            action: warn
        min_trust_score: 70
        max_severity: medium
        required_grades:
          - A+
          - A
          - B
          - C
    """), encoding="utf-8")
    return p


@pytest.fixture
def strict_policy_yaml(tmp_path: Path) -> Path:
    """Create a strict policy — blocks everything above INFO."""
    p = tmp_path / "strict.yaml"
    p.write_text(textwrap.dedent("""\
        policies:
          - name: "zero-tolerance"
            rule: "*"
            severity: critical
            action: block
        min_trust_score: 90
        max_severity: info
        required_grades:
          - A+
          - A
    """), encoding="utf-8")
    return p


@pytest.fixture
def clean_summary() -> ScanSummary:
    """A scan summary with no findings."""
    return ScanSummary(results=[
        ScanResult(file="clean.py", findings=[], lines_scanned=100),
    ])


@pytest.fixture
def dirty_summary() -> ScanSummary:
    """A scan summary with findings at various severities."""
    return ScanSummary(results=[
        ScanResult(
            file="server.py",
            findings=[
                Finding(
                    rule_id="CMD-001",
                    severity=Severity.CRITICAL,
                    title="Command injection",
                    description="shell=True",
                    file="server.py",
                    line=10,
                ),
                Finding(
                    rule_id="AUTH-001",
                    severity=Severity.HIGH,
                    title="Missing auth",
                    description="No auth check",
                    file="server.py",
                    line=20,
                ),
                Finding(
                    rule_id="LOG-001",
                    severity=Severity.LOW,
                    title="Missing logging",
                    description="No log call",
                    file="server.py",
                    line=30,
                ),
            ],
            lines_scanned=200,
        ),
    ])


# ---------------------------------------------------------------------------
# Policy loading tests
# ---------------------------------------------------------------------------

class TestLoadPolicy:
    """Tests for YAML policy file loading."""

    def test_load_valid_yaml(self, basic_policy_yaml: Path) -> None:
        policy = load_policy(basic_policy_yaml)
        assert isinstance(policy, Policy)

    def test_policy_has_rules(self, basic_policy_yaml: Path) -> None:
        policy = load_policy(basic_policy_yaml)
        assert len(policy.rules) == 2

    def test_policy_rule_fields(self, basic_policy_yaml: Path) -> None:
        policy = load_policy(basic_policy_yaml)
        r = policy.rules[0]
        assert r.name == "no-command-injection"
        assert r.rule_id == "CMD-001"
        assert r.action == PolicyAction.BLOCK

    def test_policy_min_trust_score(self, basic_policy_yaml: Path) -> None:
        policy = load_policy(basic_policy_yaml)
        assert policy.min_trust_score == 70

    def test_policy_max_severity(self, basic_policy_yaml: Path) -> None:
        policy = load_policy(basic_policy_yaml)
        assert policy.max_severity == Severity.MEDIUM

    def test_policy_required_grades(self, basic_policy_yaml: Path) -> None:
        policy = load_policy(basic_policy_yaml)
        assert "A+" in policy.required_grades
        assert "C" in policy.required_grades

    def test_load_nonexistent_file(self, tmp_path: Path) -> None:
        with pytest.raises(FileNotFoundError):
            load_policy(tmp_path / "missing.yaml")

    def test_load_invalid_yaml(self, tmp_path: Path) -> None:
        p = tmp_path / "bad.yaml"
        p.write_text("policies: [[[invalid", encoding="utf-8")
        with pytest.raises(ValueError):
            load_policy(p)

    def test_load_yml_extension(self, tmp_path: Path) -> None:
        p = tmp_path / "policy.yml"
        p.write_text(textwrap.dedent("""\
            policies:
              - name: test
                rule: TEST-001
                action: warn
        """), encoding="utf-8")
        policy = load_policy(p)
        assert len(policy.rules) == 1

    def test_policy_action_ignore(self, tmp_path: Path) -> None:
        p = tmp_path / "ignore.yaml"
        p.write_text(textwrap.dedent("""\
            policies:
              - name: skip-info
                rule: INFO-001
                action: ignore
        """), encoding="utf-8")
        policy = load_policy(p)
        assert policy.rules[0].action == PolicyAction.IGNORE


# ---------------------------------------------------------------------------
# Policy evaluation tests
# ---------------------------------------------------------------------------

class TestEvaluatePolicy:
    """Tests for policy evaluation against scan results."""

    def test_clean_scan_passes(self, basic_policy_yaml: Path, clean_summary: ScanSummary) -> None:
        policy = load_policy(basic_policy_yaml)
        result = evaluate_policy(policy, clean_summary, trust_score=95)
        assert result.passed is True

    def test_blocked_rule_fails(self, basic_policy_yaml: Path, dirty_summary: ScanSummary) -> None:
        policy = load_policy(basic_policy_yaml)
        result = evaluate_policy(policy, dirty_summary, trust_score=80)
        assert result.passed is False

    def test_blocked_rule_has_reason(self, basic_policy_yaml: Path, dirty_summary: ScanSummary) -> None:
        policy = load_policy(basic_policy_yaml)
        result = evaluate_policy(policy, dirty_summary, trust_score=80)
        blocked = [e for e in result.evaluations if e.action == PolicyAction.BLOCK]
        assert len(blocked) >= 1

    def test_warn_rule_does_not_block(self, tmp_path: Path) -> None:
        p = tmp_path / "warn.yaml"
        p.write_text(textwrap.dedent("""\
            policies:
              - name: warn-auth
                rule: AUTH-001
                action: warn
        """), encoding="utf-8")
        policy = load_policy(p)
        summary = ScanSummary(results=[
            ScanResult(file="x.py", findings=[
                Finding(
                    rule_id="AUTH-001", severity=Severity.HIGH,
                    title="Missing auth", description="No auth",
                    file="x.py", line=1,
                ),
            ]),
        ])
        result = evaluate_policy(policy, summary, trust_score=95)
        assert result.passed is True
        assert len(result.warnings) >= 1

    def test_trust_score_gate(self, basic_policy_yaml: Path, clean_summary: ScanSummary) -> None:
        policy = load_policy(basic_policy_yaml)
        result = evaluate_policy(policy, clean_summary, trust_score=50)
        assert result.passed is False
        assert any("trust score" in r.lower() for r in result.reasons)

    def test_max_severity_gate(self, basic_policy_yaml: Path, dirty_summary: ScanSummary) -> None:
        policy = load_policy(basic_policy_yaml)
        # max_severity is medium, but we have critical findings
        result = evaluate_policy(policy, dirty_summary, trust_score=95)
        assert result.passed is False

    def test_grade_gate_fails(self, strict_policy_yaml: Path) -> None:
        policy = load_policy(strict_policy_yaml)
        # Create summary with grade D
        summary = ScanSummary(results=[
            ScanResult(file="bad.py", findings=[
                Finding(
                    rule_id="X-001", severity=Severity.HIGH,
                    title="Issue", description="Desc",
                    file="bad.py", line=1,
                ),
            ] * 5),
        ])
        result = evaluate_policy(policy, summary, trust_score=95)
        assert result.passed is False

    def test_grade_gate_passes(self, basic_policy_yaml: Path, clean_summary: ScanSummary) -> None:
        policy = load_policy(basic_policy_yaml)
        result = evaluate_policy(policy, clean_summary, trust_score=95)
        assert result.passed is True

    def test_result_has_evaluations(self, basic_policy_yaml: Path, dirty_summary: ScanSummary) -> None:
        policy = load_policy(basic_policy_yaml)
        result = evaluate_policy(policy, dirty_summary, trust_score=80)
        assert isinstance(result.evaluations, list)

    def test_result_to_dict(self, basic_policy_yaml: Path, dirty_summary: ScanSummary) -> None:
        policy = load_policy(basic_policy_yaml)
        result = evaluate_policy(policy, dirty_summary, trust_score=80)
        d = result.to_dict()
        assert "passed" in d
        assert "evaluations" in d
        assert "reasons" in d

    def test_wildcard_rule_matches_all(self, strict_policy_yaml: Path) -> None:
        policy = load_policy(strict_policy_yaml)
        summary = ScanSummary(results=[
            ScanResult(file="x.py", findings=[
                Finding(
                    rule_id="ANYTHING-999", severity=Severity.CRITICAL,
                    title="Random", description="Test",
                    file="x.py", line=1,
                ),
            ]),
        ])
        result = evaluate_policy(policy, summary, trust_score=95)
        blocked = [e for e in result.evaluations if e.action == PolicyAction.BLOCK]
        assert len(blocked) >= 1

    def test_ignore_action_skips(self, tmp_path: Path) -> None:
        p = tmp_path / "ign.yaml"
        p.write_text(textwrap.dedent("""\
            policies:
              - name: skip-log
                rule: LOG-001
                action: ignore
        """), encoding="utf-8")
        policy = load_policy(p)
        summary = ScanSummary(results=[
            ScanResult(file="x.py", findings=[
                Finding(
                    rule_id="LOG-001", severity=Severity.LOW,
                    title="Logging", description="Missing",
                    file="x.py", line=1,
                ),
            ]),
        ])
        result = evaluate_policy(policy, summary, trust_score=95)
        assert result.passed is True


# ---------------------------------------------------------------------------
# Formatting tests
# ---------------------------------------------------------------------------

class TestPolicyFormatting:
    """Tests for policy result formatting."""

    def test_format_text_pass(self, basic_policy_yaml: Path, clean_summary: ScanSummary) -> None:
        policy = load_policy(basic_policy_yaml)
        result = evaluate_policy(policy, clean_summary, trust_score=95)
        text = format_policy_text(result)
        assert "PASS" in text

    def test_format_text_fail(self, basic_policy_yaml: Path, dirty_summary: ScanSummary) -> None:
        policy = load_policy(basic_policy_yaml)
        result = evaluate_policy(policy, dirty_summary, trust_score=80)
        text = format_policy_text(result)
        assert "FAIL" in text

    def test_format_json_valid(self, basic_policy_yaml: Path, dirty_summary: ScanSummary) -> None:
        policy = load_policy(basic_policy_yaml)
        result = evaluate_policy(policy, dirty_summary, trust_score=80)
        data = json.loads(format_policy_json(result))
        assert "passed" in data

    def test_format_text_shows_reasons(self, basic_policy_yaml: Path, dirty_summary: ScanSummary) -> None:
        policy = load_policy(basic_policy_yaml)
        result = evaluate_policy(policy, dirty_summary, trust_score=80)
        text = format_policy_text(result)
        assert "CMD-001" in text or "block" in text.lower()
