"""
tests/test_nap_api.py — Tests for Nap API endpoints (#418) and
sleep() consolidate flag (#416).

Covers:
    POST /api/v1/session/nap           - set session to 'napping'
    POST /api/v1/session/nap/wake      - resume session to 'active'
    POST /api/v1/session/sleep         - consolidate=True path
"""
from __future__ import annotations

from contextlib import contextmanager
from dataclasses import dataclass, field
from typing import Any, List, Optional
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

pytest.importorskip("fastapi", reason="fastapi not installed")
pytest.importorskip("httpx", reason="httpx not installed")

from fastapi.testclient import TestClient


@dataclass
class _SleepReport:
    session_id: str = "sess-sleep-nap"
    memories_summarised: int = 0
    errors: List[str] = field(default_factory=list)


def _make_mock_plugin(**overrides) -> MagicMock:
    plugin = MagicMock()
    plugin.config = MagicMock()
    plugin.config.owner_id = "owner-test"
    plugin.storage = MagicMock()
    plugin.storage.initialize = AsyncMock()
    plugin.storage.close = AsyncMock()
    plugin.storage.update_session = AsyncMock(
        return_value=overrides.get("update_session_return", None)
    )
    plugin.wake = AsyncMock(return_value=MagicMock(session_id="s1", cornerstones_loaded=0, errors=[]))
    plugin.sleep = AsyncMock(return_value=overrides.get("sleep_report", _SleepReport()))
    plugin.dream = AsyncMock(return_value=MagicMock(
        steps_completed=1, curiosity_seeds=0, claims_consolidated=0, journal="", errors=[],
    ))
    plugin.llm = MagicMock()
    return plugin


PREFIX = "/api/v1"


@contextmanager
def _make_client(api_key: Optional[str] = None, **plugin_overrides):
    from cortex.api.http_server import create_app
    mock_plugin = _make_mock_plugin(**plugin_overrides)
    app = create_app(api_key=api_key)
    with patch("cortex.api.plugin.CortexPlugin") as MockClass:
        MockClass.return_value = mock_plugin
        with TestClient(app, raise_server_exceptions=False) as tc:
            tc._mock_plugin = mock_plugin
            yield tc, mock_plugin


class TestSessionNap:
    def test_nap_returns_200(self):
        with _make_client() as (tc, _):
            resp = tc.post(f"{PREFIX}/session/nap", json={"session_id": "sess-001"})
        assert resp.status_code == 200

    def test_nap_response_shape(self):
        with _make_client() as (tc, _):
            resp = tc.post(f"{PREFIX}/session/nap", json={"session_id": "sess-002"})
        assert resp.status_code == 200
        data = resp.json()
        assert data["status"] == "napping"
        assert data["session_id"] == "sess-002"
        assert "errors" in data
        assert data["errors"] == []

    def test_nap_calls_update_session(self):
        with _make_client() as (tc, mock_plugin):
            tc.post(f"{PREFIX}/session/nap", json={"session_id": "sess-003"})
        mock_plugin.storage.update_session.assert_called_once_with(
            "sess-003", status="napping"
        )

    def test_nap_missing_session_id_returns_422(self):
        with _make_client() as (tc, _):
            resp = tc.post(f"{PREFIX}/session/nap", json={})
        assert resp.status_code == 422

    def test_nap_storage_error_returns_500(self):
        with _make_client() as (tc, mock_plugin):
            mock_plugin.storage.update_session = AsyncMock(
                side_effect=RuntimeError("DB locked")
            )
            resp = tc.post(f"{PREFIX}/session/nap", json={"session_id": "sess-err"})
        assert resp.status_code == 500

    def test_nap_no_api_key_required_when_none_set(self):
        with _make_client(api_key=None) as (tc, _):
            resp = tc.post(f"{PREFIX}/session/nap", json={"session_id": "sess-open"})
        assert resp.status_code == 200

    def test_nap_requires_api_key_when_set(self):
        with _make_client(api_key="secret") as (tc, _):
            resp = tc.post(f"{PREFIX}/session/nap", json={"session_id": "sess-auth"})
        assert resp.status_code in (401, 403)

    def test_nap_accepts_api_key_when_set(self):
        with _make_client(api_key="secret") as (tc, _):
            resp = tc.post(
                f"{PREFIX}/session/nap",
                json={"session_id": "sess-auth-ok"},
                headers={"X-API-Key": "secret"},
            )
        assert resp.status_code == 200


class TestSessionNapWake:
    def test_nap_wake_returns_200(self):
        with _make_client() as (tc, _):
            resp = tc.post(f"{PREFIX}/session/nap/wake", json={"session_id": "sess-nw-001"})
        assert resp.status_code == 200

    def test_nap_wake_response_shape(self):
        with _make_client() as (tc, _):
            resp = tc.post(f"{PREFIX}/session/nap/wake", json={"session_id": "sess-nw-002"})
        assert resp.status_code == 200
        data = resp.json()
        assert data["status"] == "active"
        assert data["session_id"] == "sess-nw-002"
        assert data["errors"] == []

    def test_nap_wake_calls_update_session(self):
        with _make_client() as (tc, mock_plugin):
            tc.post(f"{PREFIX}/session/nap/wake", json={"session_id": "sess-nw-003"})
        mock_plugin.storage.update_session.assert_called_once_with(
            "sess-nw-003", status="active"
        )

    def test_nap_wake_missing_session_id_returns_422(self):
        with _make_client() as (tc, _):
            resp = tc.post(f"{PREFIX}/session/nap/wake", json={})
        assert resp.status_code == 422

    def test_nap_wake_storage_error_returns_500(self):
        with _make_client() as (tc, mock_plugin):
            mock_plugin.storage.update_session = AsyncMock(
                side_effect=RuntimeError("timeout")
            )
            resp = tc.post(f"{PREFIX}/session/nap/wake", json={"session_id": "sess-nw-err"})
        assert resp.status_code == 500

    def test_nap_wake_no_api_key_required_when_none_set(self):
        with _make_client(api_key=None) as (tc, _):
            resp = tc.post(f"{PREFIX}/session/nap/wake", json={"session_id": "sess-open"})
        assert resp.status_code == 200


class TestNapRoundTrip:
    def test_nap_then_wake_sequence(self):
        with _make_client() as (tc, mock_plugin):
            nap_resp = tc.post(f"{PREFIX}/session/nap", json={"session_id": "rt-sess"})
            assert nap_resp.status_code == 200
            assert nap_resp.json()["status"] == "napping"

            wake_resp = tc.post(f"{PREFIX}/session/nap/wake", json={"session_id": "rt-sess"})
            assert wake_resp.status_code == 200
            assert wake_resp.json()["status"] == "active"

            assert mock_plugin.storage.update_session.call_count == 2
            calls = mock_plugin.storage.update_session.call_args_list
            assert calls[0][0] == ("rt-sess",)
            assert calls[0][1] == {"status": "napping"}
            assert calls[1][0] == ("rt-sess",)
            assert calls[1][1] == {"status": "active"}


class TestSleepConsolidateFlag:
    def test_sleep_consolidate_false_default(self):
        with _make_client() as (tc, mock_plugin):
            resp = tc.post(
                f"{PREFIX}/session/sleep",
                json={"session_id": "sess-sleep-1"},
            )
        assert resp.status_code == 200
        mock_plugin.sleep.assert_called_once_with(
            session_id="sess-sleep-1", consolidate=False
        )

    def test_sleep_consolidate_true_passed_through(self):
        with _make_client() as (tc, mock_plugin):
            resp = tc.post(
                f"{PREFIX}/session/sleep",
                json={"session_id": "sess-sleep-2", "consolidate": True},
            )
        assert resp.status_code == 200
        mock_plugin.sleep.assert_called_once_with(
            session_id="sess-sleep-2", consolidate=True
        )

    def test_sleep_returns_200_with_consolidate(self):
        with _make_client() as (tc, _):
            resp = tc.post(
                f"{PREFIX}/session/sleep",
                json={"session_id": "sess-sleep-3", "consolidate": True},
            )
        assert resp.status_code == 200
        data = resp.json()
        assert "session_id" in data
        assert "claims_consolidated" in data
        assert "duration_ms" in data
        assert "errors" in data


class TestPluginSleepConsolidate:
    """Unit tests for plugin.sleep(consolidate=True/False)."""

    @staticmethod
    def _make_plugin():
        from cortex.api.plugin import CortexPlugin
        p = object.__new__(CortexPlugin)
        p._init_errors = []
        p._session_id = "default-sess"
        p._llm_call_count = 0
        cfg = MagicMock()
        cfg.owner_id = "test-owner"
        p.config = cfg
        storage = MagicMock()
        storage.close_session = AsyncMock()
        storage.update_session = AsyncMock()
        p.storage = storage
        p.llm = MagicMock()
        p.cornerstone_guardian = MagicMock()
        p.working_memory = MagicMock()
        p.working_memory.clear_session = AsyncMock(return_value=0)
        p.event_emitter = None
        p._webhook_dispatcher = None
        p._metrics = None
        p._flags = None
        return p

    @pytest.mark.asyncio
    async def test_sleep_consolidate_false_skips_consolidator(self):
        plugin = self._make_plugin()
        with patch("cortex.circadian.sleep.SleepConsolidator") as MockConsolidator:
            report = await plugin.sleep(session_id="s1", consolidate=False)
        MockConsolidator.assert_not_called()
        assert report.session_id == "s1"
        plugin.storage.close_session.assert_called_once()

    @pytest.mark.asyncio
    async def test_sleep_consolidate_true_runs_consolidator(self):
        from cortex.circadian.sleep import SleepReport as CircadianSleepReport
        plugin = self._make_plugin()

        mock_consolidation_report = MagicMock(spec=CircadianSleepReport)
        mock_consolidation_report.claims_promoted = 3
        mock_consolidation_report.claims_pruned = 1
        mock_consolidation_report.drift_checks = []

        mock_consolidator_instance = MagicMock()
        mock_consolidator_instance.sleep = AsyncMock(
            return_value=mock_consolidation_report
        )

        with patch(
            "cortex.circadian.sleep.SleepConsolidator",
            return_value=mock_consolidator_instance,
        ):
            report = await plugin.sleep(session_id="s2", consolidate=True)

        mock_consolidator_instance.sleep.assert_called_once_with("s2")
        assert report.session_id == "s2"
        plugin.storage.close_session.assert_not_called()

    @pytest.mark.asyncio
    async def test_sleep_consolidate_true_fallback_on_error(self):
        plugin = self._make_plugin()
        mock_consolidator_instance = MagicMock()
        mock_consolidator_instance.sleep = AsyncMock(
            side_effect=RuntimeError("consolidation exploded")
        )
        with patch(
            "cortex.circadian.sleep.SleepConsolidator",
            return_value=mock_consolidator_instance,
        ):
            report = await plugin.sleep(session_id="s3", consolidate=True)
        assert any("SleepConsolidator failed" in e for e in report.errors)
        plugin.storage.close_session.assert_called_once()
        assert report.session_id == "s3"

    @pytest.mark.asyncio
    async def test_sleep_consolidate_true_no_storage_skips_consolidation(self):
        plugin = self._make_plugin()
        plugin.storage = None
        with patch("cortex.circadian.sleep.SleepConsolidator") as MockConsolidator:
            report = await plugin.sleep(session_id="s4", consolidate=True)
        MockConsolidator.assert_not_called()
        assert report.session_id == "s4"
