"""
tests/test_shadow_mode.py — Tests for shadow mode (dry-run) on remember & retrieve.

Shadow mode runs the extraction pipeline but does NOT persist to storage.
The response includes ``shadow: true`` and ``would_store`` (for remember).
"""
from __future__ import annotations

from contextlib import contextmanager
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 types
# ---------------------------------------------------------------------------

@dataclass
class _RememberResult:
    session_id: str = "sess-1"
    claims_extracted: int = 3
    claims_stored: int = 2
    entities_resolved: int = 1
    noise_filtered: int = 0
    errors: List[str] = field(default_factory=list)
    warnings: List[str] = field(default_factory=list)
    zero_claim_warning: bool = False
    ok: bool = True


@dataclass
class _RetrievalResult:
    query: str = "test query"
    memories: List[str] = field(default_factory=lambda: ["memory A"])
    total_tokens: int = 42


@dataclass
class _HealthReport:
    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"


@dataclass
class _EntityMention:
    name: str = "Eva"


@dataclass
class _ClaimExtraction:
    action: str = "ADD"
    subject: str = "Eva"
    predicate: str = "is"
    object_value: str = "an AI companion"
    confidence: float = 0.9
    temporal: str = "current"
    entities_mentioned: List[Any] = field(default_factory=lambda: [_EntityMention()])
    sensitivity: str = "normal"
    category: str = "identity"


@dataclass
class _ExtractionResult:
    claims: List[_ClaimExtraction] = field(default_factory=lambda: [_ClaimExtraction()])
    noise_filtered: int = 0
    raw_message_count: int = 1
    batches_processed: int = 1


# ---------------------------------------------------------------------------
# Mock plugin factory
# ---------------------------------------------------------------------------

def _make_mock_plugin(**overrides) -> MagicMock:
    """Return a mocked CortexPlugin with an extractor for shadow mode."""
    plugin = MagicMock()
    plugin.config = MagicMock()
    plugin.config.owner_id = "owner-1"

    plugin.storage = MagicMock()
    plugin.storage.initialize = AsyncMock()
    plugin.storage.count_claims = AsyncMock(return_value=10)
    plugin.storage.count_sessions = AsyncMock(return_value=5)
    plugin.storage.close = AsyncMock()
    plugin.storage.get_cornerstones = AsyncMock(return_value=[])

    plugin.remember = AsyncMock(
        return_value=overrides.get("remember_result", _RememberResult())
    )
    plugin.retrieve = AsyncMock(
        return_value=overrides.get("retrieval_result", _RetrievalResult())
    )
    plugin.health = AsyncMock(return_value=_HealthReport())

    # Extractor mock for shadow mode
    extractor = AsyncMock()
    extractor.extract = AsyncMock(
        return_value=overrides.get("extraction_result", _ExtractionResult())
    )
    plugin.extractor = extractor

    return plugin


@contextmanager
def _make_client(api_key: Optional[str] = None, **plugin_overrides):
    """Yield (TestClient, mock_plugin)."""
    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=True) as tc:
            yield tc, mock_plugin


# ---------------------------------------------------------------------------
# Shadow remember tests (v1)
# ---------------------------------------------------------------------------

class TestShadowRememberV1:
    """POST /api/v1/remember?shadow=true (deprecated v1)"""

    def test_shadow_remember_returns_would_store(self):
        """Shadow mode should extract claims but not persist."""
        with _make_client() as (client, plugin):
            resp = client.post(
                "/api/v1/remember?shadow=true",
                json={"conversation": "Eva is an AI companion"},
            )
            assert resp.status_code == 200
            data = resp.json()
            assert data["shadow"] is True
            assert data["claims_stored"] == 0
            assert data["claims_extracted"] >= 1
            assert isinstance(data["would_store"], list)
            assert len(data["would_store"]) >= 1
            # Verify plugin.remember was NOT called (shadow skips storage)
            plugin.remember.assert_not_called()

    def test_shadow_remember_calls_extractor(self):
        """Shadow mode should call the extractor."""
        with _make_client() as (client, plugin):
            resp = client.post(
                "/api/v1/remember?shadow=true",
                json={"conversation": "Andrew likes coffee"},
            )
            assert resp.status_code == 200
            plugin.extractor.extract.assert_called_once()

    def test_non_shadow_remember_calls_plugin(self):
        """Without shadow param, normal remember flow should run."""
        with _make_client() as (client, plugin):
            resp = client.post(
                "/api/v1/remember",
                json={"conversation": "Eva is an AI companion"},
            )
            assert resp.status_code == 200
            data = resp.json()
            assert "shadow" not in data
            plugin.remember.assert_called_once()

    def test_shadow_false_is_normal(self):
        """shadow=false should behave like normal remember."""
        with _make_client() as (client, plugin):
            resp = client.post(
                "/api/v1/remember?shadow=false",
                json={"conversation": "Eva is an AI companion"},
            )
            assert resp.status_code == 200
            plugin.remember.assert_called_once()


# ---------------------------------------------------------------------------
# Shadow remember tests (v2)
# ---------------------------------------------------------------------------

class TestShadowRememberV2:
    """POST /api/v1/remember/v2?shadow=true"""

    def test_shadow_remember_v2_returns_would_store(self):
        with _make_client() as (client, plugin):
            resp = client.post(
                "/api/v1/remember/v2?shadow=true",
                json={"conversation": "Eva is an AI companion"},
            )
            assert resp.status_code == 200
            data = resp.json()
            assert data["shadow"] is True
            assert data["claims_stored"] == 0
            assert data["claims_added"] == 0
            assert data["claims_updated"] == 0
            assert isinstance(data["would_store"], list)
            assert len(data["would_store"]) >= 1
            assert "processing_time_ms" in data
            plugin.remember.assert_not_called()

    def test_shadow_remember_v2_normal_flow(self):
        """Without shadow, v2 remember should work normally."""
        with _make_client() as (client, plugin):
            resp = client.post(
                "/api/v1/remember/v2",
                json={"conversation": "Eva is an AI companion"},
            )
            assert resp.status_code == 200
            data = resp.json()
            assert "shadow" not in data
            assert data["claims_stored"] == 2
            plugin.remember.assert_called_once()


# ---------------------------------------------------------------------------
# Shadow retrieve tests (v1)
# ---------------------------------------------------------------------------

class TestShadowRetrieveV1:
    """POST /api/v1/retrieve?shadow=true (deprecated v1)"""

    def test_shadow_retrieve_returns_shadow_flag(self):
        with _make_client() as (client, plugin):
            resp = client.post(
                "/api/v1/retrieve?shadow=true",
                json={"query": "What do you know about Eva?"},
            )
            assert resp.status_code == 200
            data = resp.json()
            assert data["shadow"] is True
            # Retrieval should still run (read-only anyway)
            plugin.retrieve.assert_called_once()

    def test_non_shadow_retrieve_has_no_flag(self):
        with _make_client() as (client, plugin):
            resp = client.post(
                "/api/v1/retrieve",
                json={"query": "What do you know about Eva?"},
            )
            assert resp.status_code == 200
            data = resp.json()
            assert "shadow" not in data


# ---------------------------------------------------------------------------
# Shadow retrieve tests (v2)
# ---------------------------------------------------------------------------

class TestShadowRetrieveV2:
    """POST /api/v1/retrieve/v2?shadow=true"""

    def test_shadow_retrieve_v2_returns_shadow_flag(self):
        with _make_client() as (client, plugin):
            resp = client.post(
                "/api/v1/retrieve/v2?shadow=true",
                json={"query": "What do you know about Eva?"},
            )
            assert resp.status_code == 200
            data = resp.json()
            assert data["shadow"] is True

    def test_non_shadow_retrieve_v2(self):
        with _make_client() as (client, plugin):
            resp = client.post(
                "/api/v1/retrieve/v2",
                json={"query": "What do you know about Eva?"},
            )
            assert resp.status_code == 200
            data = resp.json()
            assert "shadow" not in data


# ---------------------------------------------------------------------------
# Shadow mode — no DB writes verification
# ---------------------------------------------------------------------------

class TestShadowNoDBWrites:
    """Verify that shadow mode does not call any storage write methods."""

    def test_shadow_remember_no_storage_writes(self):
        with _make_client() as (client, plugin):
            resp = client.post(
                "/api/v1/remember?shadow=true",
                json={"conversation": "Important fact to remember"},
            )
            assert resp.status_code == 200
            # plugin.remember itself should not be called
            plugin.remember.assert_not_called()

    def test_shadow_remember_v2_no_storage_writes(self):
        with _make_client() as (client, plugin):
            resp = client.post(
                "/api/v1/remember/v2?shadow=true",
                json={"conversation": "Another important fact"},
            )
            assert resp.status_code == 200
            plugin.remember.assert_not_called()


# ---------------------------------------------------------------------------
# Shadow mode with conversation list format
# ---------------------------------------------------------------------------

class TestShadowWithMessageList:
    """Shadow mode with structured message list input."""

    def test_shadow_remember_with_messages(self):
        with _make_client() as (client, plugin):
            resp = client.post(
                "/api/v1/remember/v2?shadow=true",
                json={
                    "conversation": [
                        {"role": "user", "content": "My name is Andrew"},
                        {"role": "assistant", "content": "Nice to meet you, Andrew!"},
                    ]
                },
            )
            assert resp.status_code == 200
            data = resp.json()
            assert data["shadow"] is True
            assert data["claims_stored"] == 0
            assert isinstance(data["would_store"], list)


# ---------------------------------------------------------------------------
# Shadow mode with extractor unavailable
# ---------------------------------------------------------------------------

class TestShadowExtractorUnavailable:
    """Shadow mode when extractor is None."""

    def test_shadow_remember_no_extractor(self):
        with _make_client() as (client, plugin):
            plugin.extractor = None
            resp = client.post(
                "/api/v1/remember?shadow=true",
                json={"conversation": "Test conversation"},
            )
            assert resp.status_code == 200
            data = resp.json()
            assert data["shadow"] is True
            assert data["claims_extracted"] == 0
            assert data["would_store"] == []
            assert "Extraction pipeline not available" in data["errors"]
