"""Tests for CortexService — real ChromaDB + mocked HTTP externals."""

import json
from unittest.mock import AsyncMock

import pytest
from pytest_httpx import HTTPXMock

from neo_cortex.classifier import GroqClassifier
from neo_cortex.cortex import CortexService, NOISE_PATTERNS
from neo_cortex.embedder import JinaEmbedder
from neo_cortex.memory_index import MemoryIndex
from neo_cortex.models import (
    Activity,
    ClassificationResult,
    IngestRequest,
    QueryAnalysis,
)
from neo_cortex.store import MemoryStore


FAKE_EMBEDDING = [0.1] * 1024
FAKE_EMBEDDING_2 = [0.2] * 1024


def _make_classifier_mock():
    classifier = AsyncMock(spec=GroqClassifier)
    classifier.classify = AsyncMock(return_value=ClassificationResult(
        project="neo-cortex", topic="test", activity=Activity.FEATURE,
        title="Test title", summary="Test summary",
        facts=["fact1"], concepts=["concept1"], files_touched=["test.py"],
    ))
    classifier.is_noise = AsyncMock(return_value=False)
    classifier.analyze_query = AsyncMock(return_value=QueryAnalysis(
        project="neo-cortex", refined_query="refined test query",
    ))
    classifier.aggregate_to_mbel = AsyncMock(return_value="@test::mbel")
    return classifier


@pytest.fixture
def cortex(tmp_path) -> CortexService:
    """CortexService with real store + mocked embedder/classifier."""
    store = MemoryStore(str(tmp_path), collection_name="test_cortex")
    embedder = AsyncMock(spec=JinaEmbedder)
    embedder.embed_passage = AsyncMock(return_value=FAKE_EMBEDDING)
    embedder.embed_query = AsyncMock(return_value=FAKE_EMBEDDING)
    classifier = _make_classifier_mock()
    return CortexService(store, embedder, classifier)


@pytest.fixture
def cortex_with_index(tmp_path) -> CortexService:
    """CortexService with real store + index + mocked embedder/classifier."""
    store = MemoryStore(str(tmp_path / "chroma"), collection_name="test_cortex_idx")
    index = MemoryIndex(str(tmp_path / "index.db"))
    embedder = AsyncMock(spec=JinaEmbedder)
    embedder.embed_passage = AsyncMock(return_value=FAKE_EMBEDDING)
    embedder.embed_query = AsyncMock(return_value=FAKE_EMBEDDING)
    classifier = _make_classifier_mock()
    return CortexService(store, embedder, classifier, index=index)


class TestCortexIngest:
    @pytest.mark.asyncio
    async def test_ingest_stores_memory(self, cortex):
        req = IngestRequest(
            session_id="test-session",
            question="How do I fix the bug?",
            answer="Check the stack trace.",
        )
        memory_id = await cortex.ingest(req)
        assert memory_id is not None
        assert memory_id.startswith("v3_")
        assert cortex._store.count() == 1

    @pytest.mark.asyncio
    async def test_ingest_static_noise_filtered(self, cortex):
        for noise in ["ciao", "ok", "si", "/clear"]:
            req = IngestRequest(session_id="s1", question=noise, answer="whatever")
            result = await cortex.ingest(req)
            assert result is None
        assert cortex._store.count() == 0

    @pytest.mark.asyncio
    async def test_ingest_system_prefix_filtered(self, cortex):
        req = IngestRequest(
            session_id="s1",
            question="[system] internal message",
            answer="ack",
        )
        assert await cortex.ingest(req) is None

    @pytest.mark.asyncio
    async def test_ingest_groq_noise_filter_short_message(self, cortex):
        cortex._classifier.is_noise = AsyncMock(return_value=True)
        req = IngestRequest(session_id="s1", question="lol wut", answer="idk")
        assert await cortex.ingest(req) is None

    @pytest.mark.asyncio
    async def test_ingest_long_message_skips_groq_noise(self, cortex):
        long_q = "x" * 100
        req = IngestRequest(session_id="s1", question=long_q, answer="answer")
        result = await cortex.ingest(req)
        assert result is not None
        # is_noise should NOT have been called for long messages
        cortex._classifier.is_noise.assert_not_called()


class TestCortexRecall:
    @pytest.mark.asyncio
    async def test_basic_recall_empty(self, cortex):
        result = await cortex.recall("anything", n=5, smart=False)
        assert result.count == 0
        assert result.memories == []

    @pytest.mark.asyncio
    async def test_basic_recall_with_data(self, cortex):
        req = IngestRequest(session_id="s1", question="Fix the crash", answer="Stack trace fix")
        await cortex.ingest(req)
        result = await cortex.recall("crash fix", n=5, smart=False)
        assert result.count == 1
        assert result.memories[0].record.question == "Fix the crash"[:500]

    @pytest.mark.asyncio
    async def test_smart_recall_calls_analyzer(self, cortex):
        req = IngestRequest(session_id="s1", question="Engine bug", answer="Fixed it")
        await cortex.ingest(req)
        result = await cortex.recall("engine bugs", n=5, smart=True)
        cortex._classifier.analyze_query.assert_called_once_with("engine bugs")
        assert result.mbel == "@test::mbel"


class TestCortexDream:
    @pytest.mark.asyncio
    async def test_dream_skips_with_few_memories(self, cortex):
        result = await cortex.dream()
        assert result.status == "skipped"
        assert result.dream_count == 0

    @pytest.mark.asyncio
    async def test_dream_runs_with_enough_memories(self, cortex):
        for i in range(3):
            req = IngestRequest(session_id=f"s{i}", question=f"Q{i}", answer=f"A{i}")
            await cortex.ingest(req)
        result = await cortex.dream()
        assert result.status == "completed"
        assert result.dream_count == 1
        assert len(result.cluster) >= 1

    @pytest.mark.asyncio
    async def test_dream_increments_count(self, cortex):
        for i in range(3):
            req = IngestRequest(session_id=f"s{i}", question=f"Q{i}", answer=f"A{i}")
            await cortex.ingest(req)
        await cortex.dream()
        await cortex.dream()
        result = await cortex.dream()
        assert result.dream_count == 3


class TestCortexStats:
    def test_stats_empty(self, cortex):
        stats = cortex.stats()
        assert stats.total_memories == 0

    @pytest.mark.asyncio
    async def test_stats_with_data(self, cortex):
        req = IngestRequest(session_id="s1", question="Test", answer="Answer")
        await cortex.ingest(req)
        stats = cortex.stats()
        assert stats.total_memories == 1


class TestCortexTimeline:
    @pytest.mark.asyncio
    async def test_timeline_returns_memories(self, cortex):
        req = IngestRequest(session_id="s1", question="Timeline test", answer="Works")
        await cortex.ingest(req)
        result = await cortex.timeline(n=10)
        assert result.count == 1


class TestCortexDualWrite:
    @pytest.mark.asyncio
    async def test_ingest_writes_to_both_stores(self, cortex_with_index):
        req = IngestRequest(session_id="s1", question="Dual write test", answer="Both stores")
        memory_id = await cortex_with_index.ingest(req)
        assert memory_id is not None
        # ChromaDB
        assert cortex_with_index._store.count() == 1
        # SQLite
        assert cortex_with_index._index.count() == 1
        record = cortex_with_index._index.get_by_id(memory_id)
        assert record is not None
        assert record.project == "neo-cortex"

    @pytest.mark.asyncio
    async def test_ingest_works_without_index(self, cortex):
        req = IngestRequest(session_id="s1", question="No index", answer="Still works")
        memory_id = await cortex.ingest(req)
        assert memory_id is not None
        assert cortex._store.count() == 1

    @pytest.mark.asyncio
    async def test_ingest_structured_fields_propagated(self, cortex_with_index):
        req = IngestRequest(session_id="s1", question="Structured test", answer="With fields")
        memory_id = await cortex_with_index.ingest(req)
        structured = cortex_with_index._index.get_structured(memory_id)
        assert structured is not None
        assert structured.title == "Test title"
        assert "fact1" in structured.facts
        assert "concept1" in structured.concepts


class TestCortexFallbackSearch:
    @pytest.mark.asyncio
    async def test_recall_fallback_to_fts_when_embed_fails(self, cortex_with_index):
        # Ingest a memory
        req = IngestRequest(session_id="s1", question="Fix the crash bug", answer="Stack trace analysis")
        await cortex_with_index.ingest(req)
        # Make embedder fail
        cortex_with_index._embedder.embed_query = AsyncMock(side_effect=Exception("Jina down"))
        result = await cortex_with_index.recall("crash", n=5, smart=False)
        assert result.count >= 1
        assert result.memories[0].record.question.startswith("Fix the crash")

    @pytest.mark.asyncio
    async def test_recall_fts_returns_results(self, cortex_with_index):
        req = IngestRequest(session_id="s1", question="Deploy systemd service", answer="Created unit file")
        await cortex_with_index.ingest(req)
        cortex_with_index._embedder.embed_query = AsyncMock(side_effect=Exception("Jina down"))
        result = await cortex_with_index.recall("deploy", n=5, smart=False)
        assert result.count >= 1

    @pytest.mark.asyncio
    async def test_recall_fts_works_without_index(self, cortex):
        cortex._embedder.embed_query = AsyncMock(side_effect=Exception("Jina down"))
        result = await cortex.recall("anything", n=5, smart=False)
        assert result.count == 0
        assert result.memories == []
