"""Test MBEL coherence — verifica che l'output MBEL contenga i key terms dai dati strutturati."""

from unittest.mock import AsyncMock

import pytest

from neo_cortex.classifier import Classifier
from neo_cortex.cortex import CortexService
from neo_cortex.dedup import DedupStore
from neo_cortex.embedder import JinaEmbedder
from neo_cortex.graph import ConceptGraph
from neo_cortex.memory_index import MemoryIndex
from neo_cortex.models import (
    Activity,
    ClassificationResult,
    CompactMemory,
    IngestRequest,
    MemoryRecord,
    QueryAnalysis,
    RecalledMemory,
    StructuredFields,
)
from neo_cortex.store import MemoryStore


# === Fixtures ===

MEMORIES_DATASET = [
    {
        "question": "How do I fix the UTF-8 BOM encoding bug?",
        "answer": "Use new UTF8Encoding(false) instead of Encoding.UTF8. The default emits a BOM that corrupts stdin JSON.",
        "project": "neo-cortex",
        "activity": Activity.BUGFIX,
        "title": "Fix UTF8 BOM encoding bug",
        "summary": "Used UTF8Encoding(false) to prevent BOM corruption on stdin JSON pipe",
        "facts": ["Encoding.UTF8 emits BOM", "BOM corrupts stdin JSON", "UTF8Encoding(false) is the fix"],
        "concepts": ["encoding", "BOM", "utf8", "stdin"],
    },
    {
        "question": "Deploy neo-cortex to systemd",
        "answer": "Created systemd user service unit file, enabled with systemctl --user enable",
        "project": "neo-cortex",
        "activity": Activity.DEPLOY,
        "title": "Deploy neo-cortex as systemd service",
        "summary": "Created systemd unit file for neo-cortex MCP server",
        "facts": ["systemd user service", "systemctl --user enable", "runs on port 5074"],
        "concepts": ["deploy", "systemd", "service"],
    },
    {
        "question": "EngineHost.StartAsync blocks and prevents Kestrel from starting",
        "answer": "StartAsync must not block. Use Task.Run + TaskCompletionSource for background init.",
        "project": "neo-reloaded",
        "activity": Activity.BUGFIX,
        "title": "Fix EngineHost blocking startup",
        "summary": "EngineHost.StartAsync was blocking, preventing Kestrel startup. Fixed with Task.Run.",
        "facts": ["StartAsync must not block", "Kestrel starts AFTER hosted services", "Use TaskCompletionSource"],
        "concepts": ["hosted-services", "kestrel", "async", "startup"],
    },
    {
        "question": "Come funziona il concept graph?",
        "answer": "NetworkX graph dove i nodi sono concetti e gli archi pesati collegano concetti co-occorrenti. Spreading activation trova memorie correlate.",
        "project": "neo-cortex",
        "activity": Activity.FEATURE,
        "title": "Concept graph with spreading activation",
        "summary": "NetworkX-based concept graph with weighted edges for co-occurrence and spreading activation",
        "facts": ["NetworkX graph", "weighted edges", "spreading activation", "co-occurrence"],
        "concepts": ["graph", "networkx", "spreading-activation", "concepts"],
    },
    {
        "question": "SimHash dedup per filtrare duplicati",
        "answer": "64-bit SimHash con soglia hamming distance <= 3. Blocca near-duplicates prima dell'embedding.",
        "project": "neo-cortex",
        "activity": Activity.FEATURE,
        "title": "SimHash dedup filter",
        "summary": "64-bit SimHash with hamming distance threshold of 3 blocks near-duplicate ingestion",
        "facts": ["64-bit SimHash", "hamming distance <= 3", "pre-embedding filter"],
        "concepts": ["dedup", "simhash", "hamming"],
    },
]


def _make_recalled_memories(dataset: list[dict]) -> list[RecalledMemory]:
    """Build RecalledMemory objects from dataset."""
    memories = []
    for i, d in enumerate(dataset):
        record = MemoryRecord(
            id=f"v3_test_{i}_00000",
            session_id=f"sess_{i}",
            timestamp=1700000000.0 + i * 3600,
            turn_number=i,
            question=d["question"],
            answer_preview=d["answer"][:500],
            document=f"Q: {d['question']}\nA: {d['answer']}",
            project=d["project"],
            topic=d.get("topic", "test"),
            activity=d["activity"],
            energy=0.8 + i * 0.04,
        )
        memories.append(RecalledMemory(record=record, similarity=0.9 - i * 0.05))
    return memories


def _extract_key_terms(dataset: list[dict]) -> set[str]:
    """Extract terms that MUST appear in MBEL for it to be considered coherent."""
    terms = set()
    for d in dataset:
        terms.add(d["project"])
        for c in d["concepts"][:2]:
            terms.add(c.lower())
    return terms


# === Unit tests (mocked Groq) ===

class TestMBELOutputFormat:
    """Verify that MBEL from mocked classifier has expected structure."""

    def test_mbel_uses_operators(self):
        """MBEL should use MBEL operators (>, @, ::, +, {, })."""
        mbel = (
            "> EncodingFix :: UTF8Encoding(false)\n"
            "@ BOM :: corruptsStdinJSON\n"
            "> deploy :: systemdService{neo-cortex}\n"
            "@ conceptGraph :: NetworkX+spreadingActivation\n"
            "> dedup :: SimHash{64bit+hamming≤3}"
        )
        operators = [">", "@", "::", "+", "{", "}"]
        for op in operators[:3]:
            assert op in mbel, f"MBEL missing operator: {op}"

    def test_mbel_max_10_lines(self):
        """aggregate_to_mbel caps at 10 lines."""
        lines = "line\n" * 15
        capped = [line for line in lines.split("\n") if line.strip()][:10]
        assert len(capped) <= 10


class TestMBELCoherence:
    """Verify MBEL contains key terms from source memories."""

    @pytest.fixture
    def mock_mbel_good(self):
        return (
            "[neo-cortex]\n"
            "> encodingFix :: UTF8Encoding(false)→preventBOM\n"
            "@ BOM :: corruptsStdinJSON{Encoding.UTF8→emitsBOM}\n"
            "> deploy :: systemdService{userUnit+systemctl}\n"
            "@ conceptGraph :: NetworkX{spreadingActivation+weightedEdges}\n"
            "> dedup :: SimHash{64bit+hamming≤3}\n"
            "[neo-reloaded]\n"
            "> EngineHost :: StartAsync¬block{Task.Run+TCS}\n"
            "@ kestrel :: startsAfterHostedServices"
        )

    @pytest.fixture
    def mock_mbel_bad(self):
        return (
            "@recall::someGenericStuff\n"
            "@recall::moreGenericStuff"
        )

    def test_good_mbel_contains_projects(self, mock_mbel_good):
        mbel_lower = mock_mbel_good.lower()
        assert "neo-cortex" in mbel_lower
        assert "neo-reloaded" in mbel_lower

    def test_good_mbel_contains_key_concepts(self, mock_mbel_good):
        mbel_lower = mock_mbel_good.lower()
        key_terms = _extract_key_terms(MEMORIES_DATASET)
        found = sum(1 for t in key_terms if t in mbel_lower)
        coverage = found / len(key_terms)
        assert coverage >= 0.5, (
            f"MBEL covers only {coverage:.0%} of key terms. "
            f"Missing: {[t for t in key_terms if t not in mbel_lower]}"
        )

    def test_bad_mbel_fails_coherence(self, mock_mbel_bad):
        mbel_lower = mock_mbel_bad.lower()
        key_terms = _extract_key_terms(MEMORIES_DATASET)
        found = sum(1 for t in key_terms if t in mbel_lower)
        coverage = found / len(key_terms)
        assert coverage < 0.5, "Bad MBEL should fail coherence check"

    def test_mbel_not_empty_for_nonempty_input(self):
        memories = _make_recalled_memories(MEMORIES_DATASET)
        assert len(memories) > 0

    def test_coherence_check_function(self):
        mbel = "> encoding :: UTF8Encoding(false)\n@ deploy :: systemd\n@ graph :: NetworkX"
        terms = {"encoding", "deploy", "neo-cortex", "graph", "networkx", "dedup"}
        mbel_lower = mbel.lower()
        found = sum(1 for t in terms if t in mbel_lower)
        coverage = found / len(terms)
        assert coverage >= 0.5


class TestMBELCoherenceWithCortex:
    """End-to-end: ingest memories → recall → check MBEL coherence."""

    @pytest.fixture
    def cortex_full(self, tmp_path) -> CortexService:
        store = MemoryStore(str(tmp_path / "chroma"), collection_name="test_mbel")
        index = MemoryIndex(str(tmp_path / "index.db"))
        embedder = AsyncMock(spec=JinaEmbedder)
        embedder.embed_passage = AsyncMock(return_value=[0.1] * 1024)
        embedder.embed_query = AsyncMock(return_value=[0.1] * 1024)

        classifier = AsyncMock(spec=Classifier)
        classifier.is_noise = AsyncMock(return_value=False)
        classifier.analyze_query = AsyncMock(return_value=QueryAnalysis(refined_query="test"))

        call_count = {"n": 0}

        async def classify_side_effect(q, a):
            idx = min(call_count["n"], len(MEMORIES_DATASET) - 1)
            d = MEMORIES_DATASET[idx]
            call_count["n"] += 1
            return ClassificationResult(
                project=d["project"], topic="test", activity=d["activity"],
                title=d["title"], summary=d["summary"],
                facts=d["facts"], concepts=d["concepts"], files_touched=[],
            )

        classifier.classify = AsyncMock(side_effect=classify_side_effect)
        classifier.resolve_conflict = AsyncMock(return_value={"action": "add", "target_id": None, "reason": "no conflict"})

        classifier.aggregate_to_mbel = AsyncMock(return_value=(
            "[neo-cortex]\n"
            "> encodingFix :: UTF8Encoding(false)→preventBOM\n"
            "> deploy :: systemdService{systemctl}\n"
            "@ conceptGraph :: NetworkX{spreadingActivation}\n"
            "> dedup :: SimHash{64bit+hamming≤3}\n"
            "[neo-reloaded]\n"
            "> EngineHost :: ¬blockStartAsync{Task.Run}"
        ))

        return CortexService(store, embedder, classifier, index=index)

    @pytest.mark.asyncio
    async def test_ingest_then_recall_mbel_coherent(self, cortex_full):
        for d in MEMORIES_DATASET:
            req = IngestRequest(
                session_id="coherence-test",
                question=d["question"],
                answer=d["answer"],
            )
            mid = await cortex_full.ingest(req)
            assert mid is not None

        result = await cortex_full.recall("encoding deploy graph", n=5, smart=True)
        assert result.mbel is not None
        assert len(result.mbel) > 0

        mbel_lower = result.mbel.lower()
        key_terms = _extract_key_terms(MEMORIES_DATASET)
        found = sum(1 for t in key_terms if t in mbel_lower)
        coverage = found / len(key_terms)
        assert coverage >= 0.5, (
            f"MBEL coherence {coverage:.0%} < 50%. "
            f"Missing: {[t for t in key_terms if t not in mbel_lower]}"
        )

    @pytest.mark.asyncio
    async def test_recall_mbel_not_empty(self, cortex_full):
        req = IngestRequest(
            session_id="s1",
            question="Fix encoding BOM",
            answer="Use UTF8Encoding(false)",
        )
        await cortex_full.ingest(req)
        result = await cortex_full.recall("encoding", n=5, smart=True)
        assert result.mbel is not None
        assert len(result.mbel.strip()) > 0

    @pytest.mark.asyncio
    async def test_structured_fields_propagated_to_index(self, cortex_full):
        for d in MEMORIES_DATASET:
            req = IngestRequest(session_id="s", question=d["question"], answer=d["answer"])
            mid = await cortex_full.ingest(req)
            sf = cortex_full.index.get_structured(mid)
            assert sf is not None, f"No structured fields for {mid}"
            assert sf.title is not None, f"No title for {mid}"
            assert sf.summary is not None, f"No summary for {mid}"
            assert len(sf.facts) > 0, f"No facts for {mid}"
            assert len(sf.concepts) > 0, f"No concepts for {mid}"


class TestMCPOutputFormat:
    """Verify the MCP tool output shape (what actually goes to Claude)."""

    def test_memory_query_output_compact(self):
        output = {
            "query": "encoding bug",
            "count": 2,
            "mbel": "> encoding :: UTF8Encoding(false)",
            "refs": [
                {"id": "v3_abc", "title": "Fix encoding", "project": "neo-cortex", "activity": "bugfix", "similarity": 0.82},
                {"id": "v3_def", "title": "Deploy service", "project": "neo-cortex", "activity": "deploy", "similarity": 0.75},
            ],
        }
        assert "mbel" in output
        assert len(output["mbel"]) > 0
        assert "memories" not in output
        assert "document" not in str(output)
        for ref in output["refs"]:
            assert "id" in ref
            assert "title" in ref
            assert "document" not in ref

    def test_memory_timeline_output_compact(self):
        output = {
            "count": 3,
            "memories": [
                {"id": "v3_a", "title": "Fix X", "project": "p", "activity": "bugfix", "energy": 0.8, "timestamp": 1700000000, "topic": "t"},
            ],
        }
        for m in output["memories"]:
            assert "document" not in m
            assert "answer_preview" not in m
            assert "question" not in m

    def test_memory_get_output_structured(self):
        output = {
            "memories": [
                {"id": "v3_a", "title": "Fix encoding", "summary": "...", "facts": ["f1"], "concepts": ["c1"],
                 "project": "neo-cortex", "activity": "bugfix", "energy": 0.8},
            ],
        }
        for m in output["memories"]:
            assert "document" not in m
            assert "title" in m
            assert "summary" in m
            assert "facts" in m

    def test_memory_stats_output_slim(self):
        output = {"total_memories": 326, "sessions": 116, "avg_energy": 0.72, "dream_count": 5}
        assert "projects" not in output
        assert len(str(output)) < 200

    def test_memory_dream_output_no_ids(self):
        output = {"status": "completed", "dream_count": 5, "high_energy_count": 42}
        assert "cluster" not in output
        assert len(str(output)) < 200
