"""Tests for FastAPI server — TestClient with mocked externals."""

import json
from unittest.mock import AsyncMock, patch, MagicMock

import pytest
from fastapi.testclient import TestClient

from neo_cortex.memory_index import MemoryIndex
from neo_cortex.models import (
    Activity,
    ClassificationResult,
    CortexStats,
    MemoryRecord,
    QueryAnalysis,
    RecalledMemory,
)


FAKE_EMBEDDING = [0.1] * 1024


@pytest.fixture
def mock_embedder():
    m = AsyncMock()
    m.embed_passage = AsyncMock(return_value=FAKE_EMBEDDING)
    m.embed_query = AsyncMock(return_value=FAKE_EMBEDDING)
    m.close = AsyncMock()
    return m


@pytest.fixture
def mock_classifier():
    m = AsyncMock()
    m.classify = AsyncMock(return_value=ClassificationResult(
        project="test", topic="test", activity=Activity.FEATURE,
    ))
    m.is_noise = AsyncMock(return_value=False)
    m.analyze_query = AsyncMock(return_value=QueryAnalysis(refined_query="test"))
    m.aggregate_to_mbel = AsyncMock(return_value="@test::mbel")
    m.close = AsyncMock()
    return m


@pytest.fixture
def client(tmp_path, mock_embedder, mock_classifier, monkeypatch):
    """TestClient with real ChromaDB store + index + mocked HTTP externals."""
    from neo_cortex.store import MemoryStore
    from neo_cortex.cortex import CortexService
    import neo_cortex.server as server_module

    # Provide fake API keys so lifespan doesn't crash
    monkeypatch.setenv("JINA_API_KEY", "test-jina-key")
    monkeypatch.setenv("GEMINI_API_KEY", "test-gemini-key")
    monkeypatch.setenv("CORTEX_DB_PATH", str(tmp_path / "chroma"))
    monkeypatch.setenv("MEMORY_INDEX_DB_PATH", str(tmp_path / "index.db"))

    store = MemoryStore(str(tmp_path / "chroma"), collection_name="test_server")
    index = MemoryIndex(str(tmp_path / "index.db"))
    cortex = CortexService(store, mock_embedder, mock_classifier, index=index)

    # Enter TestClient (lifespan runs, creates real cortex from env vars)
    with TestClient(server_module.app, raise_server_exceptions=False) as c:
        # Override AFTER lifespan — inject test cortex over real one
        server_module._cortex = cortex
        server_module._embedder = mock_embedder
        server_module._classifier = mock_classifier
        server_module._index = index
        yield c

    # Cleanup
    server_module._cortex = None
    server_module._embedder = None
    server_module._classifier = None
    server_module._index = None


class TestServerHealth:
    def test_health_returns_ok(self, client):
        resp = client.get("/health")
        assert resp.status_code == 200
        data = resp.json()
        assert data["status"] == "ok"
        assert "stats" in data


class TestServerIngest:
    def test_ingest_returns_id(self, client):
        resp = client.post("/api/ingest", json={
            "session_id": "test-sess",
            "question": "How do I fix the crash?",
            "answer": "Check the stack trace.",
        })
        assert resp.status_code == 200
        data = resp.json()
        assert data["status"] == "ingested"
        assert data["id"].startswith("v3_")

    def test_ingest_noise_filtered(self, client):
        resp = client.post("/api/ingest", json={
            "session_id": "test-sess",
            "question": "ciao",
            "answer": "hello",
        })
        assert resp.status_code == 200
        data = resp.json()
        assert data["status"] == "skipped"


class TestServerQuery:
    def test_query_empty(self, client):
        resp = client.get("/api/query", params={"q": "test", "smart": "false"})
        assert resp.status_code == 200
        data = resp.json()
        assert data["count"] == 0

    def test_query_after_ingest(self, client):
        client.post("/api/ingest", json={
            "session_id": "s1",
            "question": "Engine architecture",
            "answer": "Four black boxes.",
        })
        resp = client.get("/api/query", params={"q": "engine", "n": "5", "smart": "false"})
        assert resp.status_code == 200
        data = resp.json()
        assert data["count"] == 1


class TestServerDream:
    def test_dream_skips_empty(self, client):
        resp = client.post("/api/dream")
        assert resp.status_code == 200
        assert resp.json()["status"] == "skipped"


class TestServerStats:
    def test_stats_empty(self, client):
        resp = client.get("/api/stats")
        assert resp.status_code == 200
        assert resp.json()["total_memories"] == 0


class TestServerTimeline:
    def test_timeline_empty(self, client):
        resp = client.get("/api/timeline")
        assert resp.status_code == 200
        data = resp.json()
        assert data["count"] == 0

    def test_timeline_with_data(self, client):
        client.post("/api/ingest", json={
            "session_id": "s1",
            "question": "Timeline test",
            "answer": "Works",
        })
        resp = client.get("/api/timeline", params={"n": "10"})
        assert resp.status_code == 200
        assert resp.json()["count"] == 1


class TestServerSearch:
    def test_search_returns_compact(self, client):
        client.post("/api/ingest", json={
            "session_id": "s1",
            "question": "Fix the crash bug",
            "answer": "Stack trace analysis done",
        })
        resp = client.get("/api/search", params={"q": "crash"})
        assert resp.status_code == 200
        data = resp.json()
        assert data["count"] >= 1
        assert "id" in data["results"][0]
        assert "title" in data["results"][0]

    def test_search_with_project_filter(self, client):
        client.post("/api/ingest", json={
            "session_id": "s1",
            "question": "Test search",
            "answer": "With filter",
        })
        resp = client.get("/api/search", params={"q": "", "project": "nonexistent"})
        assert resp.status_code == 200
        assert resp.json()["count"] == 0


class TestServerMemory:
    def test_get_memory_by_id(self, client):
        ingest_resp = client.post("/api/ingest", json={
            "session_id": "s1",
            "question": "Get memory test",
            "answer": "Should be retrievable",
        })
        memory_id = ingest_resp.json()["id"]
        resp = client.get(f"/api/memory/{memory_id}")
        assert resp.status_code == 200
        assert resp.json()["id"] == memory_id

    def test_get_memory_not_found_404(self, client):
        resp = client.get("/api/memory/nonexistent")
        assert resp.status_code == 404

    def test_get_memories_batch(self, client):
        id1 = client.post("/api/ingest", json={
            "session_id": "s1", "question": "Batch Q1", "answer": "A1",
        }).json()["id"]
        id2 = client.post("/api/ingest", json={
            "session_id": "s2", "question": "Batch Q2", "answer": "A2",
        }).json()["id"]
        resp = client.get("/api/memories", params={"ids": f"{id1},{id2}"})
        assert resp.status_code == 200
        data = resp.json()
        assert data["count"] == 2
