"""
Tests for autopil.policy_engine.PolicyEngine

Coverage:
  - Allowed sources are granted
  - Denied sources are blocked (even if in allowed list)
  - Sources not in allowed list are blocked
  - Sensitivity ceiling enforcement
  - No matching policy → default deny
  - Admin wildcard (empty allowed_sources = allow all non-denied)
  - Policy reload
"""

import textwrap

import pytest

from autopil.models import ContextRequest, Decision, SensitivityLevel
from autopil.policy_engine import PolicyEngine


def make_req(agent_role, source_id, sensitivity=SensitivityLevel.LOW) -> ContextRequest:
    return ContextRequest(
        query="test",
        agent_role=agent_role,
        user_id="u1",
        source_id=source_id,
        sensitivity_level=sensitivity,
    )


# ── allowed sources ───────────────────────────────────────────────────────────

class TestAllowedSources:
    def test_allowed_source_is_granted(self, engine):
        decision, policy, _ = engine.evaluate(make_req("analyst", "reports"))
        assert decision == Decision.ALLOW
        assert policy == "analyst_policy"

    def test_second_allowed_source_is_granted(self, engine):
        decision, _, _ = engine.evaluate(make_req("analyst", "market_data"))
        assert decision == Decision.ALLOW

    def test_viewer_allowed_source_is_granted(self, engine):
        decision, _, _ = engine.evaluate(make_req("viewer", "reports"))
        assert decision == Decision.ALLOW


# ── denied sources ────────────────────────────────────────────────────────────

class TestDeniedSources:
    def test_denied_source_is_blocked(self, engine):
        decision, policy, reason = engine.evaluate(make_req("analyst", "executive_comms"))
        assert decision == Decision.DENY
        assert "explicitly denied" in reason

    def test_denied_takes_priority(self, engine):
        """executive_comms appears in denied — should be blocked for all roles."""
        for role in ("analyst", "viewer", "admin"):
            decision, _, _ = engine.evaluate(make_req(role, "executive_comms"))
            assert decision == Decision.DENY, f"Expected DENY for role={role}"

    def test_source_not_in_allowed_list_is_blocked(self, engine):
        """viewer can only access reports — market_data is in their denied list."""
        decision, _, reason = engine.evaluate(make_req("viewer", "market_data"))
        assert decision == Decision.DENY

    def test_source_not_in_allowed_and_not_denied_is_blocked(self, engine):
        """analyst has an allowed list — unknown source not in it should be denied."""
        decision, _, _ = engine.evaluate(make_req("analyst", "mystery_source"))
        assert decision == Decision.DENY


# ── sensitivity ceiling ───────────────────────────────────────────────────────

class TestSensitivityCeiling:
    def test_within_ceiling_is_allowed(self, engine):
        decision, _, _ = engine.evaluate(make_req("analyst", "reports", SensitivityLevel.HIGH))
        assert decision == Decision.ALLOW

    def test_at_ceiling_is_allowed(self, engine):
        decision, _, _ = engine.evaluate(make_req("viewer", "reports", SensitivityLevel.MEDIUM))
        assert decision == Decision.ALLOW

    def test_above_ceiling_is_denied(self, engine):
        """viewer max_sensitivity=medium — restricted should be denied."""
        decision, _, reason = engine.evaluate(make_req("viewer", "reports", SensitivityLevel.RESTRICTED))
        assert decision == Decision.DENY
        assert "ceiling" in reason or "exceeds" in reason

    def test_admin_allows_restricted(self, engine):
        """admin max_sensitivity=restricted — restricted level should be allowed."""
        decision, _, _ = engine.evaluate(make_req("admin", "reports", SensitivityLevel.RESTRICTED))
        assert decision == Decision.ALLOW


# ── default deny ──────────────────────────────────────────────────────────────

class TestDefaultDeny:
    def test_unknown_role_is_denied(self, engine):
        decision, policy, reason = engine.evaluate(make_req("rogue", "reports"))
        assert decision == Decision.DENY
        assert policy == "default_deny"
        assert "rogue" in reason

    def test_empty_role_is_denied(self, engine):
        decision, _, _ = engine.evaluate(make_req("", "reports"))
        assert decision == Decision.DENY


# ── admin wildcard ────────────────────────────────────────────────────────────

class TestAdminWildcard:
    def test_admin_can_access_unlisted_source(self, engine):
        """admin has empty allowed_sources → allow everything not explicitly denied."""
        decision, _, _ = engine.evaluate(make_req("admin", "any_source"))
        assert decision == Decision.ALLOW

    def test_admin_still_blocked_on_denied(self, engine):
        decision, _, _ = engine.evaluate(make_req("admin", "executive_comms"))
        assert decision == Decision.DENY


# ── policy reload ─────────────────────────────────────────────────────────────

class TestPolicyReload:
    def test_reload_picks_up_new_policy(self, tmp_path):
        p = tmp_path / "policies.yaml"
        p.write_text(textwrap.dedent("""
            policies:
              - name: original
                agent_role: tester
                allowed_sources: [source_a]
                denied_sources: []
                max_sensitivity: high
        """))
        engine = PolicyEngine(str(p))
        assert engine.evaluate(make_req("tester", "source_a"))[0] == Decision.ALLOW
        assert engine.evaluate(make_req("tester", "source_b"))[0] == Decision.DENY

        # Update the policy file and reload
        p.write_text(textwrap.dedent("""
            policies:
              - name: updated
                agent_role: tester
                allowed_sources: [source_a, source_b]
                denied_sources: []
                max_sensitivity: high
        """))
        engine2 = PolicyEngine(str(p))
        assert engine2.evaluate(make_req("tester", "source_b"))[0] == Decision.ALLOW
