"""tests/test_dream_crash_recovery.py — Tests for #98 dream cycle crash recovery.

Verifies per-step error isolation: if one dream step raises, the others
still execute and the report captures the failure.
"""
from __future__ import annotations

import pytest
import pytest_asyncio
from unittest.mock import AsyncMock, MagicMock

from cortex.circadian.sleep_consolidator import SleepConsolidator
from cortex.circadian.sleep import DreamReport


@pytest_asyncio.fixture
async def consolidator():
    """Build a SleepConsolidator with fully-mocked dependencies."""
    storage = AsyncMock()
    storage.get_config = AsyncMock(return_value=None)
    storage.set_config = AsyncMock()
    storage.get_orphaned_sessions = AsyncMock(return_value=[])
    storage.get_claims_by_status = AsyncMock(return_value=[])
    storage.get_cornerstones = AsyncMock(return_value=[])
    storage.get_claims_needing_decay = AsyncMock(return_value=[])
    storage.get_session_by_id = AsyncMock(return_value=None)
    storage.update_session = AsyncMock()
    storage.get_active_peers = AsyncMock(return_value=[])
    storage.get_brain_graphs = AsyncMock(return_value=[])
    storage.get_pending_feedback = AsyncMock(return_value=[])

    config = MagicMock()
    config.owner_id = "default"
    config.dream_cycle_idle_timeout_sec = 3600
    config.dream_decay_window_hours = 24
    config.promoted_confidence_default = 0.8

    er = AsyncMock()
    er.suggest_merges = AsyncMock(return_value=[])
    fs = AsyncMock()
    fs.apply_pending = AsyncMock(return_value=0)

    return SleepConsolidator(
        storage=storage,
        config=config,
        llm=AsyncMock(),
        cornerstone_guardian=AsyncMock(),
        drift_calculator=AsyncMock(),
        deprecation_pipeline=AsyncMock(),
        entity_resolver=er,
        feedback_system=fs,
        event_emitter=None,
        peer_manager=None,
    )


class TestRunDreamStep:
    """Unit tests for _run_dream_step helper."""

    @pytest.mark.asyncio
    async def test_successful_step(self, consolidator):
        report = DreamReport(owner_id="test")

        async def ok_coro():
            return 42

        result = await consolidator._run_dream_step("test_step", ok_coro(), report)
        assert result == 42
        assert len(report.step_results) == 1
        assert report.step_results[0]["name"] == "test_step"
        assert report.step_results[0]["status"] == "success"
        assert "duration_ms" in report.step_results[0]

    @pytest.mark.asyncio
    async def test_failed_step_returns_none(self, consolidator):
        report = DreamReport(owner_id="test")

        async def bad_coro():
            raise RuntimeError("step exploded")

        result = await consolidator._run_dream_step("failing_step", bad_coro(), report)
        assert result is None
        assert len(report.step_results) == 1
        assert report.step_results[0]["status"] == "failed"
        assert "step exploded" in report.step_results[0]["error"]


class TestDreamCrashRecovery:
    """Integration tests: dream() continues despite individual step failures."""

    @pytest.mark.asyncio
    async def test_one_step_fails_others_continue(self, consolidator):
        """If one step raises, the rest still run and report captures the failure."""
        async def exploding_orphans(owner_id):
            raise RuntimeError("orphan step boom")

        consolidator._dream_recover_orphans = exploding_orphans

        report = await consolidator.dream("test-owner")

        # Should have 10 step results (all steps attempted)
        assert len(report.step_results) == 10

        # First step should be failed
        orphan_step = report.step_results[0]
        assert orphan_step["name"] == "orphan_recovery"
        assert orphan_step["status"] == "failed"
        assert "orphan step boom" in orphan_step["error"]

        # Remaining steps should be success
        for step in report.step_results[1:]:
            assert step["status"] == "success", f"Step {step['name']} should succeed"

        # orphans_recovered should default to 0
        assert report.orphans_recovered == 0

    @pytest.mark.asyncio
    async def test_all_steps_succeed(self, consolidator):
        """Happy path: all steps succeed, clean report."""
        report = await consolidator.dream("test-owner")

        assert len(report.step_results) == 10
        for step in report.step_results:
            assert step["status"] == "success", f"Step {step['name']} failed unexpectedly"
            assert "error" not in step

        assert report.duration_ms > 0
        assert report.completed_at

    @pytest.mark.asyncio
    async def test_step_results_correct_names(self, consolidator):
        """Verify all 10 steps are named correctly in order."""
        report = await consolidator.dream("test-owner")

        expected_names = [
            "orphan_recovery",
            "deep_consolidation",
            "cornerstone_evolution",
            "reconsolidation",
            "memory_decay",
            "entity_merges",
            "peer_refresh",
            "brain_graph_refresh",
            "apply_feedback",
            "curiosity_and_journal",
        ]
        actual_names = [s["name"] for s in report.step_results]
        assert actual_names == expected_names

    @pytest.mark.asyncio
    async def test_multiple_steps_fail(self, consolidator):
        """Multiple failures don't cascade — each step runs independently."""

        async def fail_consolidation(owner_id):
            raise ValueError("consolidation broken")

        async def fail_decay(owner_id):
            raise TypeError("decay broken")

        consolidator._dream_deep_consolidation = fail_consolidation
        consolidator._dream_memory_decay = fail_decay

        report = await consolidator.dream("test-owner")

        assert len(report.step_results) == 10

        failed = [s for s in report.step_results if s["status"] == "failed"]
        succeeded = [s for s in report.step_results if s["status"] == "success"]
        assert len(failed) == 2
        assert len(succeeded) == 8

        failed_names = {s["name"] for s in failed}
        assert "deep_consolidation" in failed_names
        assert "memory_decay" in failed_names

        # Tuple-returning step defaults handled
        assert report.claims_decayed == 0
        assert report.claims_archived == 0

    @pytest.mark.asyncio
    async def test_dream_report_has_step_results_field(self):
        """DreamReport dataclass includes step_results."""
        report = DreamReport(owner_id="test")
        assert hasattr(report, "step_results")
        assert report.step_results == []
