"""
tests/test_lifecycle_triggers.py — Session lifecycle manual trigger tests (Issue #457).

Validates that session lifecycle endpoints can be triggered manually for ops/debugging:
  - POST /api/v1/sessions/nap   → puts session into napping state
  - POST /api/v1/sessions/nap/wake → resumes session from nap
  - POST /api/v1/sessions/wake  → starts a new session
  - POST /api/v1/sessions/sleep → ends a session (optionally consolidates)
  - POST /api/v1/sessions/dream → triggers a full dream cycle
  - Full sleep → wake → sleep lifecycle sequence
  - Consolidation processes pending claims when consolidate=True

All tests use FastAPI TestClient with a mocked CortexPlugin.
"""
from __future__ import annotations

import sys
from dataclasses import dataclass, field
from typing import Any, Dict, 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  # noqa: E402


# ---------------------------------------------------------------------------
# Stub dataclasses mirroring CortexPlugin returns
# ---------------------------------------------------------------------------

@dataclass
class _WakeReport:
    session_id: str = "sess-lifecycle-1"
    cornerstones_loaded: int = 2
    errors: List[str] = field(default_factory=list)


@dataclass
class _SleepReport:
    session_id: str = "sess-lifecycle-1"
    memories_summarised: int = 4
    claims_consolidated: int = 4
    errors: List[str] = field(default_factory=list)


@dataclass
class _DreamReport:
    steps_completed: int = 7
    curiosity_seeds: int = 3
    claims_consolidated: int = 12
    journal: str = "Nightly consolidation complete."
    errors: List[str] = field(default_factory=list)


@dataclass
class _FakeHealthReport:
    ok: bool = True
    storage: str = "ok"
    modules: Dict[str, str] = field(default_factory=lambda: {"storage": "ok"})
    errors: List[str] = field(default_factory=list)
    timestamp: str = "2025-01-01T00:00:00+00:00"


# ---------------------------------------------------------------------------
# Plugin factory
# ---------------------------------------------------------------------------

def _make_plugin(storage_ok: bool = True) -> MagicMock:
    plugin = MagicMock()
    plugin._init_errors = []
    plugin.health = AsyncMock(return_value=_FakeHealthReport())
    plugin.wake = AsyncMock(return_value=_WakeReport())
    plugin.sleep = AsyncMock(return_value=_SleepReport())
    plugin.dream = AsyncMock(return_value=_DreamReport())

    cfg = MagicMock()
    cfg.owner_id = "default"
    cfg.quality_gates_enabled = False
    cfg.metrics_enabled = False
    plugin.config = cfg
    plugin._metrics = None

    if storage_ok:
        storage = MagicMock()
        storage.ping = AsyncMock(return_value=None)
        storage.update_session = AsyncMock(return_value=None)
        storage.initialize = AsyncMock(return_value=None)
        storage.close = AsyncMock(return_value=None)
        storage._conn = MagicMock()
        plugin.storage = storage
    else:
        plugin.storage = None

    return plugin


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
def lifecycle_client():
    from cortex.api.http_server import create_app
    plugin_mock = _make_plugin(storage_ok=True)
    app = create_app()
    with patch("cortex.api.plugin.CortexPlugin") as MockPlugin:
        MockPlugin.return_value = plugin_mock
        with TestClient(app, raise_server_exceptions=True) as c:
            yield c, plugin_mock


@pytest.fixture
def no_storage_client():
    from cortex.api.http_server import create_app
    plugin_mock = _make_plugin(storage_ok=False)
    app = create_app()
    with patch("cortex.api.plugin.CortexPlugin") as MockPlugin:
        MockPlugin.return_value = plugin_mock
        with TestClient(app, raise_server_exceptions=True) as c:
            yield c, plugin_mock


# ---------------------------------------------------------------------------
# NAP endpoint tests
# ---------------------------------------------------------------------------

class TestNapEndpoint:
    """POST /api/v1/sessions/nap — puts session into napping state."""

    def test_nap_triggers_storage_update(self, lifecycle_client):
        """Nap endpoint calls storage.update_session with 'napping' status."""
        client, plugin = lifecycle_client
        resp = client.post(
            "/api/v1/sessions/nap",
            json={"session_id": "sess-nap-1"},
        )
        assert resp.status_code == 200
        plugin.storage.update_session.assert_called_once_with("sess-nap-1", status="napping")

    def test_nap_returns_session_id(self, lifecycle_client):
        """Nap response includes the session_id."""
        client, _ = lifecycle_client
        resp = client.post(
            "/api/v1/sessions/nap",
            json={"session_id": "sess-nap-2"},
        )
        data = resp.json()
        assert data.get("session_id") == "sess-nap-2"

    def test_nap_returns_503_without_storage(self, no_storage_client):
        """Nap endpoint returns 503 when storage is unavailable."""
        client, _ = no_storage_client
        resp = client.post(
            "/api/v1/sessions/nap",
            json={"session_id": "sess-nap-nostorage"},
        )
        assert resp.status_code == 503


# ---------------------------------------------------------------------------
# NAP/WAKE endpoint tests
# ---------------------------------------------------------------------------

class TestNapWakeEndpoint:
    """POST /api/v1/sessions/nap/wake — resumes a napping session."""

    def test_nap_wake_sets_active_status(self, lifecycle_client):
        """Nap-wake calls storage.update_session with 'active' status."""
        client, plugin = lifecycle_client
        resp = client.post(
            "/api/v1/sessions/nap/wake",
            json={"session_id": "sess-napwake-1"},
        )
        assert resp.status_code == 200
        plugin.storage.update_session.assert_called_once_with(
            "sess-napwake-1", status="active"
        )

    def test_nap_wake_returns_session_id(self, lifecycle_client):
        """Nap-wake response includes the session_id."""
        client, _ = lifecycle_client
        resp = client.post(
            "/api/v1/sessions/nap/wake",
            json={"session_id": "sess-napwake-2"},
        )
        data = resp.json()
        assert data.get("session_id") == "sess-napwake-2"


# ---------------------------------------------------------------------------
# WAKE endpoint tests
# ---------------------------------------------------------------------------

class TestWakeEndpoint:
    """POST /api/v1/sessions/wake — starts a new session."""

    def test_wake_returns_200(self, lifecycle_client):
        """Wake endpoint returns 200."""
        client, _ = lifecycle_client
        resp = client.post("/api/v1/sessions/wake", json={})
        assert resp.status_code == 200

    def test_wake_calls_plugin_wake(self, lifecycle_client):
        """Wake endpoint calls plugin.wake()."""
        client, plugin = lifecycle_client
        client.post("/api/v1/sessions/wake", json={"session_id": "new-sess-1"})
        plugin.wake.assert_called_once()

    def test_wake_response_has_session_id(self, lifecycle_client):
        """Wake response includes session_id."""
        client, _ = lifecycle_client
        resp = client.post("/api/v1/sessions/wake", json={})
        data = resp.json()
        assert "session_id" in data
        assert data["session_id"] == "sess-lifecycle-1"  # from stub

    def test_wake_response_has_cornerstones_loaded(self, lifecycle_client):
        """Wake response includes cornerstones_loaded count."""
        client, _ = lifecycle_client
        resp = client.post("/api/v1/sessions/wake", json={})
        data = resp.json()
        assert "cornerstones_loaded" in data
        assert isinstance(data["cornerstones_loaded"], int)


# ---------------------------------------------------------------------------
# SLEEP endpoint tests
# ---------------------------------------------------------------------------

class TestSleepEndpoint:
    """POST /api/v1/sessions/sleep — ends a session."""

    def test_sleep_returns_200(self, lifecycle_client):
        """Sleep endpoint returns 200."""
        client, _ = lifecycle_client
        resp = client.post(
            "/api/v1/sessions/sleep",
            json={"session_id": "sess-sleep-1"},
        )
        assert resp.status_code == 200

    def test_sleep_calls_plugin_sleep(self, lifecycle_client):
        """Sleep endpoint calls plugin.sleep()."""
        client, plugin = lifecycle_client
        client.post("/api/v1/sessions/sleep", json={"session_id": "sess-sleep-2"})
        plugin.sleep.assert_called_once()

    def test_sleep_with_consolidate_true(self, lifecycle_client):
        """Sleep with consolidate=True triggers consolidation path."""
        client, plugin = lifecycle_client
        resp = client.post(
            "/api/v1/sessions/sleep",
            json={"session_id": "sess-sleep-3", "consolidate": True},
        )
        assert resp.status_code == 200
        # plugin.sleep called with consolidate=True
        call_kwargs = plugin.sleep.call_args
        assert call_kwargs is not None
        # positional or keyword argument check
        args, kwargs = call_kwargs
        consolidate_val = kwargs.get("consolidate", args[1] if len(args) > 1 else None)
        assert consolidate_val is True

    def test_sleep_response_has_claims_consolidated(self, lifecycle_client):
        """Sleep response includes claims_consolidated count."""
        client, _ = lifecycle_client
        resp = client.post(
            "/api/v1/sessions/sleep",
            json={"session_id": "sess-sleep-4"},
        )
        data = resp.json()
        assert "claims_consolidated" in data
        assert isinstance(data["claims_consolidated"], int)


# ---------------------------------------------------------------------------
# DREAM endpoint tests
# ---------------------------------------------------------------------------

class TestDreamEndpoint:
    """POST /api/v1/sessions/dream — triggers a full dream cycle."""

    def test_dream_returns_200(self, lifecycle_client):
        """Dream endpoint returns 200."""
        client, _ = lifecycle_client
        resp = client.post("/api/v1/sessions/dream", json={})
        assert resp.status_code == 200

    def test_dream_calls_plugin_dream(self, lifecycle_client):
        """Dream endpoint calls plugin.dream()."""
        client, plugin = lifecycle_client
        client.post("/api/v1/sessions/dream", json={})
        plugin.dream.assert_called_once()

    def test_dream_response_structure(self, lifecycle_client):
        """Dream response has expected fields."""
        client, _ = lifecycle_client
        resp = client.post("/api/v1/sessions/dream", json={})
        data = resp.json()
        assert "steps_completed" in data
        assert "claims_consolidated" in data
        assert "duration_ms" in data
        assert isinstance(data["steps_completed"], int)
        assert isinstance(data["duration_ms"], int)


# ---------------------------------------------------------------------------
# Full lifecycle sequence test
# ---------------------------------------------------------------------------

class TestLifecycleSequence:
    """End-to-end: sleep → wake → sleep sequence."""

    def test_full_sleep_wake_sleep_cycle(self, lifecycle_client):
        """Validates the full sleep → wake → sleep lifecycle sequence."""
        client, plugin = lifecycle_client

        # Step 1: Wake a new session
        r1 = client.post("/api/v1/sessions/wake", json={"session_id": "cycle-sess-1"})
        assert r1.status_code == 200
        assert "session_id" in r1.json()

        # Step 2: Sleep the session
        r2 = client.post(
            "/api/v1/sessions/sleep",
            json={"session_id": "cycle-sess-1"},
        )
        assert r2.status_code == 200

        # Step 3: Wake a new session again
        r3 = client.post("/api/v1/sessions/wake", json={"session_id": "cycle-sess-2"})
        assert r3.status_code == 200

        # Verify plugin methods were called in sequence
        assert plugin.wake.call_count == 2
        assert plugin.sleep.call_count == 1
