"""Tests for meta_reflect() integration in dream cycle."""
import time
import pytest
from unittest.mock import AsyncMock, MagicMock, patch, PropertyMock
from neo_cortex.digestor import EpisodeDigestor
from neo_cortex.models import ObservationLevel


class _Stop(BaseException):
    """Escapes the try/except Exception inside digestor.run()."""


@pytest.fixture(autouse=True)
def _no_sleep():
    """Patch asyncio.sleep so digestor.run() doesn't block."""
    with patch("asyncio.sleep", new_callable=AsyncMock):
        yield


@pytest.fixture
def cortex_with_dream():
    """Cortex mock that supports dream + meta_reflect dependencies."""
    cortex = AsyncMock()
    cortex.dream.return_value = MagicMock(
        surprisal=[], boosted=0, decayed=0, consolidated=0
    )
    # recall returns memories for evidence gathering
    cortex.recall.return_value = MagicMock(
        memories=[
            MagicMock(id="m1", summary="Fixed bug with Protocol D"),
            MagicMock(id="m2", summary="Wrote tests before implementation"),
        ]
    )
    return cortex


class TestMetaReflectCalledAfterDream:
    """meta_reflect is called after dream cycle completes."""

    @pytest.mark.asyncio
    async def test_meta_reflect_fires_after_dream(self, cortex_with_dream):
        log = MagicMock()
        digestor = EpisodeDigestor(log=log, cortex=cortex_with_dream)
        call_order = []
        process_calls = [0]

        async def mock_process_all():
            process_calls[0] += 1
            if process_calls[0] == 1:
                call_order.append("work")
                return True  # Work done → _needs_dream = True
            if process_calls[0] == 2:
                return False  # Idle → triggers dream + meta_reflect
            raise _Stop()  # 3rd call exits

        async def mock_dream():
            call_order.append("dream")
            return MagicMock(surprisal=[], boosted=0, decayed=0, consolidated=0)

        cortex_with_dream.dream = mock_dream
        digestor._process_all = mock_process_all

        with patch("neo_cortex.digestor.meta_reflect", new_callable=AsyncMock) as mock_mr, \
             patch("neo_cortex.llm.create_client", return_value=AsyncMock()):
            async def mock_meta_reflect(cortex, llm):
                call_order.append("meta_reflect")
                return 0

            mock_mr.side_effect = mock_meta_reflect

            with pytest.raises(_Stop):
                await digestor.run()

            assert "dream" in call_order
            assert "meta_reflect" in call_order
            assert call_order.index("dream") < call_order.index("meta_reflect")

    @pytest.mark.asyncio
    async def test_meta_reflect_skipped_when_no_dream(self, cortex_with_dream):
        log = MagicMock()
        digestor = EpisodeDigestor(log=log, cortex=cortex_with_dream)
        call_count = [0]

        async def mock_process_all():
            call_count[0] += 1
            if call_count[0] >= 2:
                raise _Stop()
            return False  # No work done → no dream → no meta_reflect

        digestor._process_all = mock_process_all

        with patch("neo_cortex.digestor.meta_reflect", new_callable=AsyncMock) as mock_mr, \
             patch("neo_cortex.llm.create_client", return_value=AsyncMock()):
            with pytest.raises(_Stop):
                await digestor.run()
            mock_mr.assert_not_called()

    @pytest.mark.asyncio
    async def test_meta_reflect_failure_does_not_block_digestor(self, cortex_with_dream):
        log = MagicMock()
        digestor = EpisodeDigestor(log=log, cortex=cortex_with_dream)
        call_count = [0]

        async def mock_process_all():
            call_count[0] += 1
            if call_count[0] == 1:
                return True  # Work done → _needs_dream = True
            if call_count[0] == 2:
                return False  # Idle → dream → meta_reflect (fails) → sleep → loop
            raise _Stop()  # 3rd call

        digestor._process_all = mock_process_all

        with patch("neo_cortex.digestor.meta_reflect", new_callable=AsyncMock) as mock_mr, \
             patch("neo_cortex.llm.create_client", return_value=AsyncMock()):
            mock_mr.side_effect = Exception("LLM call failed")
            with pytest.raises(_Stop):
                await digestor.run()
            # Digestor continued to 3rd cycle despite meta_reflect failure
            assert call_count[0] == 3


class TestMetaReflectFunction:
    """The meta_reflect function generates and stores metacognitive memories."""

    @pytest.mark.asyncio
    async def test_meta_reflect_stores_metacognitive_memories(self):
        from neo_cortex.reflection import meta_reflect

        cortex = AsyncMock()
        cortex._index = MagicMock()  # sync methods — not AsyncMock
        cortex._store = MagicMock()
        llm = AsyncMock()

        # Setup: cortex has recent memories (via get_recent_summaries helper)
        cortex._index.get_recent_summaries.return_value = [
            {"id": "m1", "summary": "Debugged with Protocol D", "timestamp": time.time()},
            {"id": "m2", "summary": "Wrote tests first", "timestamp": time.time()},
        ]
        # recall returns RecallResult with RecalledMemory objects
        # RecalledMemory has .record (MemoryRecord) and .similarity
        cortex.recall.return_value = MagicMock(
            memories=[
                MagicMock(record=MagicMock(id="m1", document="Debugged with Protocol D")),
                MagicMock(record=MagicMock(id="m2", document="Wrote tests first")),
            ]
        )

        # LLM: focal points
        llm.call.side_effect = [
            # generate_focal_points
            "1. How do I approach debugging?\n2. When do I write tests?\n3. How do I make decisions?",
            # generate_insight #1
            "INSIGHT: I use Protocol D systematically for debugging.\nEVIDENCE: m1",
            # verify_truthfulness #1
            "SUPPORTED: Evidence shows systematic debugging.",
            # generate_insight #2
            "INSIGHT: I prefer TDD approach.\nEVIDENCE: m2",
            # verify_truthfulness #2
            "SUPPORTED: Evidence shows tests-first.",
            # generate_insight #3
            "INSIGHT: I decide based on data.\nEVIDENCE: m1, m2",
            # verify_truthfulness #3
            "UNSUPPORTED: No clear evidence for this.",
        ]

        count = await meta_reflect(cortex, llm)

        # 3 focal points → 3 insights → 2 pass truthfulness
        assert count == 2
        # Verify store was called for the 2 that passed (insert(record, embedding))
        assert cortex._store.insert.call_count == 2
        # Verify index was called too
        assert cortex._index.insert.call_count == 2

    @pytest.mark.asyncio
    async def test_meta_reflect_returns_zero_when_no_recent_memories(self):
        from neo_cortex.reflection import meta_reflect

        cortex = AsyncMock()
        cortex._index = MagicMock()  # sync methods — not AsyncMock
        llm = AsyncMock()
        cortex._index.get_recent_summaries.return_value = []

        count = await meta_reflect(cortex, llm)
        assert count == 0
        llm.call.assert_not_called()
