"""
tests/test_dream_manual.py — Manual dream cycle trigger tests (Issue #457).

Validates POST /api/v1/admin/dream-cycle endpoint:
  - Triggers a full dream cycle
  - Returns memories_processed count and duration_ms
  - Handles empty DB (0 memories processed, no error)
  - Propagates errors cleanly
  - Respects optional owner_id parameter
"""
from __future__ import annotations

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
# ---------------------------------------------------------------------------

@dataclass
class _DreamReport:
    steps_completed: int = 5
    curiosity_seeds: int = 2
    claims_consolidated: int = 8
    journal: str = "All memories processed."
    errors: List[str] = field(default_factory=list)


@dataclass
class _EmptyDreamReport:
    """Dream report when DB is empty — 0 everything, no errors."""
    steps_completed: int = 0
    curiosity_seeds: int = 0
    claims_consolidated: int = 0
    journal: str = "Nothing to consolidate."
    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(dream_report=None, dream_raises: Optional[Exception] = None) -> MagicMock:
    plugin = MagicMock()
    plugin._init_errors = []
    plugin.health = AsyncMock(return_value=_FakeHealthReport())

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

    storage = MagicMock()
    storage.ping = AsyncMock(return_value=None)
    storage.initialize = AsyncMock(return_value=None)
    storage.close = AsyncMock(return_value=None)
    storage._conn = MagicMock()
    plugin.storage = storage

    if dream_raises is not None:
        plugin.dream = AsyncMock(side_effect=dream_raises)
    elif dream_report is not None:
        plugin.dream = AsyncMock(return_value=dream_report)
    else:
        plugin.dream = AsyncMock(return_value=_DreamReport())

    return plugin


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

@pytest.fixture
def dream_client():
    """Client with a healthy plugin that returns a normal dream report."""
    from cortex.api.http_server import create_app
    plugin_mock = _make_plugin()
    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 empty_dream_client():
    """Client that returns a dream report with 0 memories."""
    from cortex.api.http_server import create_app
    plugin_mock = _make_plugin(dream_report=_EmptyDreamReport())
    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 failing_dream_client():
    """Client where dream raises an exception."""
    from cortex.api.http_server import create_app
    plugin_mock = _make_plugin(dream_raises=RuntimeError("dream engine crashed"))
    app = create_app()
    with patch("cortex.api.plugin.CortexPlugin") as MockPlugin:
        MockPlugin.return_value = plugin_mock
        with TestClient(app, raise_server_exceptions=False) as c:
            yield c, plugin_mock


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------

class TestAdminDreamCycleEndpoint:
    """POST /api/v1/admin/dream-cycle — manual dream trigger."""

    def test_dream_trigger_returns_200(self, dream_client):
        """Manual dream trigger returns 200 on success."""
        client, _ = dream_client
        resp = client.post("/api/v1/admin/dream-cycle")
        assert resp.status_code == 200

    def test_dream_trigger_processes_memories(self, dream_client):
        """Response includes memories_processed count > 0."""
        client, _ = dream_client
        resp = client.post("/api/v1/admin/dream-cycle")
        data = resp.json()
        assert "memories_processed" in data
        assert isinstance(data["memories_processed"], int)
        assert data["memories_processed"] >= 0

    def test_dream_trigger_includes_duration_ms(self, dream_client):
        """Response includes duration_ms as a non-negative integer."""
        client, _ = dream_client
        resp = client.post("/api/v1/admin/dream-cycle")
        data = resp.json()
        assert "duration_ms" in data
        assert isinstance(data["duration_ms"], int)
        assert data["duration_ms"] >= 0

    def test_dream_trigger_includes_status(self, dream_client):
        """Response includes a 'status' field."""
        client, _ = dream_client
        resp = client.post("/api/v1/admin/dream-cycle")
        data = resp.json()
        assert "status" in data
        assert data["status"] == "completed"

    def test_dream_trigger_calls_plugin_dream(self, dream_client):
        """Endpoint calls plugin.dream() exactly once."""
        client, plugin = dream_client
        client.post("/api/v1/admin/dream-cycle")
        plugin.dream.assert_called_once()

    def test_dream_trigger_empty_db_succeeds(self, empty_dream_client):
        """Dream cycle with empty DB returns success with 0 memories processed."""
        client, _ = empty_dream_client
        resp = client.post("/api/v1/admin/dream-cycle")
        assert resp.status_code == 200
        data = resp.json()
        assert data["status"] == "completed"
        assert data["memories_processed"] == 0
        assert data.get("errors", []) == []

    def test_dream_trigger_failure_returns_error_status(self, failing_dream_client):
        """When dream raises, response contains error info."""
        client, _ = failing_dream_client
        resp = client.post("/api/v1/admin/dream-cycle")
        # Should return 500 with error info
        assert resp.status_code == 500
        data = resp.json()
        assert data["status"] == "failed"
        assert len(data["errors"]) > 0
        assert "dream engine crashed" in data["errors"][0]

    def test_dream_trigger_owner_id_param(self, dream_client):
        """owner_id query param is forwarded to plugin.dream()."""
        client, plugin = dream_client
        client.post("/api/v1/admin/dream-cycle?owner_id=test-owner")
        call_kwargs = plugin.dream.call_args
        assert call_kwargs is not None
        args, kwargs = call_kwargs
        owner_arg = kwargs.get("owner_id") or (args[0] if args else None)
        assert owner_arg == "test-owner"
