"""Tests for policy evaluator."""

import pytest

from hieromem_core.policy.evaluator import (
    PolicyEvaluator,
    PolicyEngine,
    PolicyAction,
    PolicyResult,
    PolicyDecision,
    EvaluationContext,
)
from hieromem_core.policy.schema import (
    MemoryPolicy,
    RetentionPolicy,
    GovernancePolicy,
    GraphPolicy,
    PriorityPolicy,
    DecayCurve,
)


class TestPolicyDecision:
    """Tests for PolicyDecision."""

    def test_is_allowed_allow(self) -> None:
        decision = PolicyDecision(
            result=PolicyResult.ALLOW,
            policy_name="test",
            action=PolicyAction.INSERT,
            reason="Allowed",
        )
        assert decision.is_allowed() is True

    def test_is_allowed_warn(self) -> None:
        decision = PolicyDecision(
            result=PolicyResult.WARN,
            policy_name="test",
            action=PolicyAction.INSERT,
            reason="Warned",
        )
        assert decision.is_allowed() is True

    def test_is_allowed_deny(self) -> None:
        decision = PolicyDecision(
            result=PolicyResult.DENY,
            policy_name="test",
            action=PolicyAction.INSERT,
            reason="Denied",
        )
        assert decision.is_allowed() is False

    def test_to_dict(self) -> None:
        decision = PolicyDecision(
            result=PolicyResult.ALLOW,
            policy_name="test",
            action=PolicyAction.INSERT,
            reason="Test reason",
            namespace="test-ns",
            episode_id="ep-123",
            modifications={"key": "value"},
            warnings=["warning1"],
        )
        d = decision.to_dict()

        assert d["result"] == "allow"
        assert d["policy_name"] == "test"
        assert d["action"] == "insert"
        assert d["reason"] == "Test reason"
        assert d["namespace"] == "test-ns"
        assert d["modifications"] == {"key": "value"}


class TestPolicyEvaluator:
    """Tests for PolicyEvaluator."""

    def test_disabled_policy_allows_all(self) -> None:
        policy = MemoryPolicy(name="test", enabled=False)
        evaluator = PolicyEvaluator(policy)

        ctx = EvaluationContext(
            action=PolicyAction.INSERT,
            namespace="test",
        )
        decision = evaluator.evaluate(ctx)

        assert decision.result == PolicyResult.ALLOW
        assert "disabled" in decision.reason.lower()

    def test_namespace_mismatch_allows(self) -> None:
        policy = MemoryPolicy(name="test", namespace="specific-ns")
        evaluator = PolicyEvaluator(policy)

        ctx = EvaluationContext(
            action=PolicyAction.INSERT,
            namespace="other-ns",
        )
        decision = evaluator.evaluate(ctx)

        assert decision.result == PolicyResult.ALLOW
        assert "not matched" in decision.reason.lower()

    def test_wildcard_namespace_matches_all(self) -> None:
        policy = MemoryPolicy(name="test", namespace="*")
        evaluator = PolicyEvaluator(policy)

        ctx = EvaluationContext(
            action=PolicyAction.INSERT,
            namespace="any-namespace",
        )
        decision = evaluator.evaluate(ctx)

        # Should proceed with evaluation (not skip due to namespace)
        assert "not matched" not in decision.reason.lower()

    def test_insert_forbidden_relation_denied(self) -> None:
        policy = MemoryPolicy(
            name="test",
            governance=GovernancePolicy(forbidden_relations={"kills", "harms"}),
        )
        evaluator = PolicyEvaluator(policy)

        ctx = EvaluationContext(
            action=PolicyAction.INSERT,
            namespace="test",
            relations=["helps", "kills"],  # "kills" is forbidden
        )
        decision = evaluator.evaluate(ctx)

        assert decision.result == PolicyResult.DENY
        assert "forbidden relation" in decision.reason.lower()

    def test_insert_blocked_content_denied(self) -> None:
        policy = MemoryPolicy(
            name="test",
            governance=GovernancePolicy(blocked_content_patterns=["password"]),
        )
        evaluator = PolicyEvaluator(policy)

        ctx = EvaluationContext(
            action=PolicyAction.INSERT,
            namespace="test",
            content="my password is secret123",
        )
        decision = evaluator.evaluate(ctx)

        assert decision.result == PolicyResult.DENY
        assert "blocked pattern" in decision.reason.lower()

    def test_insert_capacity_exceeded_denied(self) -> None:
        policy = MemoryPolicy(
            name="test",
            governance=GovernancePolicy(max_episodes=100),
        )
        evaluator = PolicyEvaluator(policy)

        ctx = EvaluationContext(
            action=PolicyAction.INSERT,
            namespace="test",
            total_episodes=100,  # At limit
        )
        decision = evaluator.evaluate(ctx)

        assert decision.result == PolicyResult.DENY
        assert "limit exceeded" in decision.reason.lower()

    def test_insert_approaching_capacity_warns(self) -> None:
        policy = MemoryPolicy(
            name="test",
            governance=GovernancePolicy(max_episodes=100),
        )
        evaluator = PolicyEvaluator(policy)

        ctx = EvaluationContext(
            action=PolicyAction.INSERT,
            namespace="test",
            total_episodes=95,  # 95% usage
        )
        decision = evaluator.evaluate(ctx)

        assert decision.result == PolicyResult.WARN
        assert len(decision.warnings) > 0
        assert "capacity" in decision.warnings[0].lower()

    def test_retrieve_applies_trust_boost(self) -> None:
        policy = MemoryPolicy(
            name="test",
            retention=RetentionPolicy(access_boost=0.8),
            governance=GovernancePolicy(max_trust_boost=1.0),
        )
        evaluator = PolicyEvaluator(policy)

        ctx = EvaluationContext(
            action=PolicyAction.RETRIEVE,
            namespace="test",
        )
        decision = evaluator.evaluate(ctx)

        assert decision.result == PolicyResult.ALLOW
        assert decision.modifications["trust_boost"] == 0.8

    def test_decay_computes_new_trust(self) -> None:
        policy = MemoryPolicy(
            name="test",
            retention=RetentionPolicy(
                decay_curve=DecayCurve.EXPONENTIAL,
                base_lambda=0.9,
                min_trust=0.1,
            ),
        )
        evaluator = PolicyEvaluator(policy)

        ctx = EvaluationContext(
            action=PolicyAction.DECAY,
            namespace="test",
            current_trust=1.0,
            cycles_unused=1,
            metadata={},
        )
        decision = evaluator.evaluate(ctx)

        assert decision.result == PolicyResult.ALLOW
        assert decision.modifications["new_trust"] == pytest.approx(0.9)
        assert decision.modifications["decayed"] is True

    def test_decay_critical_memory_no_decay(self) -> None:
        policy = MemoryPolicy(
            name="test",
            priority=PriorityPolicy(critical_tags={"important"}),
        )
        evaluator = PolicyEvaluator(policy)

        ctx = EvaluationContext(
            action=PolicyAction.DECAY,
            namespace="test",
            current_trust=1.0,
            cycles_unused=10,
            metadata={"tags": ["important"]},
        )
        decision = evaluator.evaluate(ctx)

        assert decision.result == PolicyResult.ALLOW
        assert decision.modifications["new_trust"] == 1.0
        assert decision.modifications["decayed"] is False

    def test_prune_with_orphan_setting(self) -> None:
        policy = MemoryPolicy(
            name="test",
            graph=GraphPolicy(prune_orphans=False),
        )
        evaluator = PolicyEvaluator(policy)

        ctx = EvaluationContext(
            action=PolicyAction.PRUNE,
            namespace="test",
            edge_count=0,  # Orphan
        )
        decision = evaluator.evaluate(ctx)

        assert decision.result == PolicyResult.DENY
        assert "orphan pruning disabled" in decision.reason.lower()


class TestPolicyEngine:
    """Tests for PolicyEngine with multiple policies."""

    def test_register_and_list_policies(self) -> None:
        engine = PolicyEngine()
        policy1 = MemoryPolicy(name="policy1")
        policy2 = MemoryPolicy(name="policy2")

        engine.register_policy(policy1)
        engine.register_policy(policy2)

        policies = engine.list_policies()
        assert len(policies) == 2
        names = {p.name for p in policies}
        assert "policy1" in names
        assert "policy2" in names

    def test_unregister_policy(self) -> None:
        engine = PolicyEngine()
        policy = MemoryPolicy(name="test")
        engine.register_policy(policy)

        assert engine.unregister_policy("test") is True
        assert engine.unregister_policy("nonexistent") is False
        assert len(engine.list_policies()) == 0

    def test_get_policy(self) -> None:
        engine = PolicyEngine()
        policy = MemoryPolicy(name="test")
        engine.register_policy(policy)

        assert engine.get_policy("test") is policy
        assert engine.get_policy("nonexistent") is None

    def test_no_policies_allows_all(self) -> None:
        engine = PolicyEngine()
        ctx = EvaluationContext(
            action=PolicyAction.INSERT,
            namespace="test",
        )
        decision = engine.evaluate(ctx)

        assert decision.result == PolicyResult.ALLOW
        assert "no applicable policies" in decision.reason.lower()

    def test_first_deny_wins(self) -> None:
        engine = PolicyEngine()

        # First policy allows
        policy1 = MemoryPolicy(name="allow", namespace="*")
        engine.register_policy(policy1)

        # Second policy denies
        policy2 = MemoryPolicy(
            name="deny",
            namespace="*",
            governance=GovernancePolicy(max_episodes=10),
        )
        engine.register_policy(policy2)

        ctx = EvaluationContext(
            action=PolicyAction.INSERT,
            namespace="test",
            total_episodes=15,  # Exceeds limit
        )
        decision = engine.evaluate(ctx)

        assert decision.result == PolicyResult.DENY
        assert decision.policy_name == "deny"

    def test_namespace_specific_before_wildcard(self) -> None:
        engine = PolicyEngine()

        # Wildcard policy
        wildcard = MemoryPolicy(
            name="wildcard",
            namespace="*",
            governance=GovernancePolicy(max_episodes=100),
        )
        engine.register_policy(wildcard)

        # Specific namespace policy with different limit
        specific = MemoryPolicy(
            name="specific",
            namespace="special",
            governance=GovernancePolicy(max_episodes=10),
        )
        engine.register_policy(specific)

        # Test with "special" namespace - should use specific policy
        ctx = EvaluationContext(
            action=PolicyAction.INSERT,
            namespace="special",
            total_episodes=50,  # Exceeds specific limit
        )
        decision = engine.evaluate(ctx)

        assert decision.result == PolicyResult.DENY

    def test_decision_handler_called(self) -> None:
        engine = PolicyEngine()
        policy = MemoryPolicy(name="test")
        engine.register_policy(policy)

        decisions = []
        engine.add_decision_handler(lambda d: decisions.append(d))

        ctx = EvaluationContext(
            action=PolicyAction.INSERT,
            namespace="test",
        )
        engine.evaluate(ctx)

        assert len(decisions) == 1
        assert decisions[0].policy_name == "test"

    def test_convenience_check_insert(self) -> None:
        engine = PolicyEngine()
        policy = MemoryPolicy(
            name="test",
            governance=GovernancePolicy(forbidden_relations={"harms"}),
        )
        engine.register_policy(policy)

        decision = engine.check_insert(
            namespace="test",
            relations=["helps"],
        )
        assert decision.is_allowed()

        decision = engine.check_insert(
            namespace="test",
            relations=["harms"],
        )
        assert not decision.is_allowed()

    def test_convenience_check_decay(self) -> None:
        engine = PolicyEngine()
        policy = MemoryPolicy(
            name="test",
            retention=RetentionPolicy(base_lambda=0.9),
        )
        engine.register_policy(policy)

        decision = engine.check_decay(
            namespace="test",
            episode_id="ep-1",
            current_trust=1.0,
            cycles_unused=2,
        )

        assert decision.is_allowed()
        # 1.0 * 0.9 * 0.9 = 0.81
        assert decision.modifications["new_trust"] == pytest.approx(0.81)
