"""Tests for policy hooks module."""

from __future__ import annotations

from typing import Any, List
from unittest.mock import MagicMock

import networkx as nx
import numpy as np
import pytest

from hieromem_core.policy.hooks import (
    PolicyAwareMemory,
    PolicyViolationError,
)
from hieromem_core.policy.evaluator import (
    PolicyAction,
    PolicyDecision,
    PolicyEngine,
    PolicyResult,
)
from hieromem_core.policy.events import PolicyEvent, PolicyEventEmitter
from hieromem_core.policy.schema import MemoryPolicy


class TestPolicyViolationError:
    """Tests for PolicyViolationError."""

    def test_creates_error_from_decision(self) -> None:
        decision = PolicyDecision(
            result=PolicyResult.DENY,
            policy_name="test-policy",
            action=PolicyAction.INSERT,
            reason="Forbidden content",
        )
        error = PolicyViolationError(decision)

        assert error.decision == decision
        assert "test-policy" in str(error)
        assert "Forbidden content" in str(error)

    def test_error_is_exception(self) -> None:
        decision = PolicyDecision(
            result=PolicyResult.DENY,
            policy_name="test",
            action=PolicyAction.INSERT,
            reason="Test",
        )
        error = PolicyViolationError(decision)

        assert isinstance(error, Exception)


class TestPolicyAwareMemory:
    """Tests for PolicyAwareMemory."""

    @pytest.fixture
    def mock_memory(self) -> MagicMock:
        """Create a mock memory instance."""
        memory = MagicMock()
        memory.namespace = "test-ns"
        memory.episodes = []
        memory.store = None
        return memory

    @pytest.fixture
    def policy_engine(self) -> PolicyEngine:
        """Create a policy engine with default policy."""
        engine = PolicyEngine()
        engine.register_policy(MemoryPolicy.default())
        return engine

    @pytest.fixture
    def policy_aware_memory(
        self, mock_memory: MagicMock, policy_engine: PolicyEngine
    ) -> PolicyAwareMemory:
        """Create a PolicyAwareMemory instance."""
        return PolicyAwareMemory(
            memory=mock_memory,
            policy_engine=policy_engine,
        )

    def test_init(self, mock_memory: MagicMock, policy_engine: PolicyEngine) -> None:
        pam = PolicyAwareMemory(
            memory=mock_memory,
            policy_engine=policy_engine,
            raise_on_deny=False,
        )

        assert pam.memory == mock_memory
        assert pam.engine == policy_engine
        assert pam._raise_on_deny is False

    def test_memory_property(
        self, policy_aware_memory: PolicyAwareMemory, mock_memory: MagicMock
    ) -> None:
        assert policy_aware_memory.memory == mock_memory

    def test_engine_property(
        self, policy_aware_memory: PolicyAwareMemory, policy_engine: PolicyEngine
    ) -> None:
        assert policy_aware_memory.engine == policy_engine

    def test_get_trace_id_without_generator(
        self, policy_aware_memory: PolicyAwareMemory
    ) -> None:
        assert policy_aware_memory._get_trace_id() is None

    def test_get_trace_id_with_generator(
        self, mock_memory: MagicMock, policy_engine: PolicyEngine
    ) -> None:
        pam = PolicyAwareMemory(
            memory=mock_memory,
            policy_engine=policy_engine,
            trace_id_generator=lambda: "trace-123",
        )
        assert pam._get_trace_id() == "trace-123"

    def test_emit_event_with_emitter(
        self, mock_memory: MagicMock, policy_engine: PolicyEngine
    ) -> None:
        emitter = MagicMock()
        pam = PolicyAwareMemory(
            memory=mock_memory,
            policy_engine=policy_engine,
            event_emitter=emitter,
        )

        event = MagicMock()
        pam._emit_event(event)

        emitter.emit.assert_called_once_with(event)

    def test_emit_event_without_emitter(
        self, policy_aware_memory: PolicyAwareMemory
    ) -> None:
        # Should not raise when no emitter is configured
        event = MagicMock()
        policy_aware_memory._emit_event(event)  # Should not raise

    def test_handle_decision_allow(
        self, mock_memory: MagicMock, policy_engine: PolicyEngine
    ) -> None:
        pam = PolicyAwareMemory(memory=mock_memory, policy_engine=policy_engine)

        decision = PolicyDecision(
            result=PolicyResult.ALLOW,
            policy_name="test",
            action=PolicyAction.INSERT,
            reason="Allowed",
        )

        result = pam._handle_decision(decision)
        assert result is True

    def test_handle_decision_deny_raises(
        self, mock_memory: MagicMock, policy_engine: PolicyEngine
    ) -> None:
        pam = PolicyAwareMemory(
            memory=mock_memory,
            policy_engine=policy_engine,
            raise_on_deny=True,
        )

        decision = PolicyDecision(
            result=PolicyResult.DENY,
            policy_name="test",
            action=PolicyAction.INSERT,
            reason="Denied",
        )

        with pytest.raises(PolicyViolationError) as exc_info:
            pam._handle_decision(decision)

        assert exc_info.value.decision == decision

    def test_handle_decision_deny_no_raise(
        self, mock_memory: MagicMock, policy_engine: PolicyEngine
    ) -> None:
        pam = PolicyAwareMemory(
            memory=mock_memory,
            policy_engine=policy_engine,
            raise_on_deny=False,
        )

        decision = PolicyDecision(
            result=PolicyResult.DENY,
            policy_name="test",
            action=PolicyAction.INSERT,
            reason="Denied",
        )

        result = pam._handle_decision(decision)
        assert result is False

    def test_handle_decision_warn_logs_warnings(
        self, mock_memory: MagicMock, policy_engine: PolicyEngine, caplog: Any
    ) -> None:
        import logging
        caplog.set_level(logging.WARNING)

        pam = PolicyAwareMemory(memory=mock_memory, policy_engine=policy_engine)

        decision = PolicyDecision(
            result=PolicyResult.WARN,
            policy_name="test",
            action=PolicyAction.INSERT,
            reason="Warning",
            warnings=["Warning 1", "Warning 2"],
        )

        result = pam._handle_decision(decision)

        assert result is True
        assert "Warning 1" in caplog.text
        assert "Warning 2" in caplog.text

    def test_insert_success(
        self, mock_memory: MagicMock, policy_engine: PolicyEngine
    ) -> None:
        pam = PolicyAwareMemory(memory=mock_memory, policy_engine=policy_engine)

        vector = np.array([1.0, 2.0, 3.0])
        graph = None
        meta = {"text": "Hello world"}

        mock_episode = MagicMock()
        mock_memory.insert.return_value = mock_episode

        result = pam.insert(vector, graph, meta)

        assert result == mock_episode
        mock_memory.insert.assert_called_once()

    def test_insert_with_graph_extracts_relations(
        self, mock_memory: MagicMock, policy_engine: PolicyEngine
    ) -> None:
        pam = PolicyAwareMemory(memory=mock_memory, policy_engine=policy_engine)

        vector = np.array([1.0, 2.0, 3.0])
        graph = nx.DiGraph()
        graph.add_edge("A", "B", relation="knows")
        graph.add_edge("B", "C", label="works_with")

        mock_memory.insert.return_value = MagicMock()

        pam.insert(vector, graph, {"text": "test"})

        mock_memory.insert.assert_called_once()

    def test_insert_denied_raises(
        self, mock_memory: MagicMock
    ) -> None:
        # Create policy that denies inserts with forbidden relation
        from hieromem_core.policy.schema import GovernancePolicy

        policy = MemoryPolicy(
            name="strict",
            governance=GovernancePolicy(forbidden_relations={"kills"}),
        )
        engine = PolicyEngine()
        engine.register_policy(policy)

        pam = PolicyAwareMemory(
            memory=mock_memory,
            policy_engine=engine,
            raise_on_deny=True,
        )

        vector = np.array([1.0, 2.0, 3.0])
        graph = nx.DiGraph()
        graph.add_edge("A", "B", relation="kills")

        with pytest.raises(PolicyViolationError):
            pam.insert(vector, graph, {"text": "test"})

    def test_insert_denied_returns_none(
        self, mock_memory: MagicMock
    ) -> None:
        from hieromem_core.policy.schema import GovernancePolicy

        policy = MemoryPolicy(
            name="strict",
            governance=GovernancePolicy(forbidden_relations={"kills"}),
        )
        engine = PolicyEngine()
        engine.register_policy(policy)

        pam = PolicyAwareMemory(
            memory=mock_memory,
            policy_engine=engine,
            raise_on_deny=False,
        )

        vector = np.array([1.0, 2.0, 3.0])
        graph = nx.DiGraph()
        graph.add_edge("A", "B", relation="kills")

        result = pam.insert(vector, graph, {"text": "test"})
        assert result is None

    def test_summarise_success(
        self, mock_memory: MagicMock, policy_engine: PolicyEngine
    ) -> None:
        pam = PolicyAwareMemory(memory=mock_memory, policy_engine=policy_engine)

        pam.summarise()

        mock_memory.summarise.assert_called_once()

    def test_apply_decay_basic(
        self, mock_memory: MagicMock, policy_engine: PolicyEngine
    ) -> None:
        # Create mock episode
        mock_episode = MagicMock()
        mock_episode.id = "ep-1"
        mock_episode.trust = 0.8
        mock_episode.meta = {}
        mock_memory.episodes = [mock_episode]

        pam = PolicyAwareMemory(memory=mock_memory, policy_engine=policy_engine)

        decisions = pam.apply_decay(cycles=1)

        assert len(decisions) == 1

    def test_check_capacity_no_warnings(
        self, mock_memory: MagicMock, policy_engine: PolicyEngine
    ) -> None:
        mock_memory.episodes = [MagicMock() for _ in range(5)]  # Few episodes

        pam = PolicyAwareMemory(memory=mock_memory, policy_engine=policy_engine)

        result = pam.check_capacity()

        # Default policy allows 10000 episodes, so no warnings
        assert result is None

    def test_getattr_delegates_to_memory(
        self, mock_memory: MagicMock, policy_engine: PolicyEngine
    ) -> None:
        mock_memory.some_method.return_value = "delegated"

        pam = PolicyAwareMemory(memory=mock_memory, policy_engine=policy_engine)

        result = pam.some_method()

        assert result == "delegated"
        mock_memory.some_method.assert_called_once()


class TestIntegration:
    """Integration tests for policy hooks."""

    def test_full_workflow(self) -> None:
        """Test a full workflow with mocked memory."""
        # Create mock memory
        memory = MagicMock()
        memory.namespace = "test"
        memory.episodes = []

        # Create engine with policy
        engine = PolicyEngine()
        engine.register_policy(MemoryPolicy.default())

        # Create emitter
        emitter = PolicyEventEmitter()  # type: ignore[no-untyped-call]
        events_received: List[PolicyEvent] = []

        from hieromem_core.policy.events import CallbackEventHandler
        emitter.add_handler(CallbackEventHandler(lambda e: events_received.append(e)))

        # Create policy-aware memory
        pam = PolicyAwareMemory(
            memory=memory,
            policy_engine=engine,
            event_emitter=emitter,
        )

        # Insert an episode
        mock_episode = MagicMock()
        mock_episode.id = "ep-1"
        memory.insert.return_value = mock_episode

        result = pam.insert(
            np.array([1.0, 2.0, 3.0]),
            None,
            {"text": "Hello world"},
        )

        assert result == mock_episode
        assert len(events_received) == 1
        assert events_received[0].action == PolicyAction.INSERT
