"""Tests for MBConsolidator — pattern extraction, MB generation, idempotent append."""

import json
from unittest.mock import AsyncMock

import pytest

from neo_cortex.consolidator import MBConsolidator
from neo_cortex.memory_index import MemoryIndex
from neo_cortex.models import (
    Activity,
    MemoryRecord,
    StructuredFields,
)


def _insert_memory(index: MemoryIndex, mid: str, project: str = "neo-cortex",
                    concepts: list[str] | None = None, energy: float = 0.8,
                    topic: str = "test"):
    """Helper to insert a memory with structured fields."""
    record = MemoryRecord(
        id=mid, session_id="s1", timestamp=1700000000.0,
        turn_number=1, question="test q", answer_preview="test a",
        document="test doc", project=project, topic=topic,
        activity=Activity.FEATURE, energy=energy,
    )
    structured = StructuredFields(
        title=f"Title for {mid}",
        summary=f"Summary for {mid}",
        facts=[f"fact from {mid}"],
        concepts=concepts or [],
        files_touched=[],
    )
    index.insert(record, structured)
    # Set energy explicitly (insert defaults to record.energy)
    index.update_energy(mid, energy)


@pytest.fixture
def index(tmp_path) -> MemoryIndex:
    return MemoryIndex(str(tmp_path / "index.db"))


@pytest.fixture
def consolidator(tmp_path, index) -> MBConsolidator:
    """Consolidator with real index, mocked classifier, temp MB path."""
    classifier = AsyncMock()
    classifier.summarize_pattern = AsyncMock(return_value="## Encoding\nUTF8Encoding(false) prevents BOM")
    classifier.classify_mb_target = AsyncMock(return_value="techContext.md")
    mb_path = str(tmp_path / "memory_bank")
    return MBConsolidator(index=index, classifier=classifier, mb_path=mb_path)


@pytest.fixture
def consolidator_with_data(tmp_path) -> MBConsolidator:
    """Consolidator with index populated with recurring concepts."""
    index = MemoryIndex(str(tmp_path / "index.db"))

    # "encoding" concept appears in 4 memories at high energy
    for i in range(4):
        _insert_memory(index, f"enc_{i}", concepts=["encoding", "utf8"], energy=0.8)

    # "deploy" concept appears in 3 memories at high energy
    for i in range(3):
        _insert_memory(index, f"dep_{i}", concepts=["deploy", "systemd"], energy=0.7)

    # "random" concept appears in only 1 memory — won't be a pattern
    _insert_memory(index, "rnd_0", concepts=["random"], energy=0.9)

    # "stale" concept appears in 3 memories but low energy
    for i in range(3):
        _insert_memory(index, f"stale_{i}", concepts=["stale-thing"], energy=0.2)

    classifier = AsyncMock()
    classifier.summarize_pattern = AsyncMock(return_value="## Pattern\nSome knowledge")
    classifier.classify_mb_target = AsyncMock(return_value="techContext.md")
    mb_path = str(tmp_path / "memory_bank")
    return MBConsolidator(index=index, classifier=classifier, mb_path=mb_path)


class TestPatternExtraction:
    def test_find_recurring_concepts(self, consolidator_with_data):
        """Concepts in 3+ memories with high energy → pattern candidates."""
        patterns = consolidator_with_data.find_recurring_patterns()
        assert len(patterns) > 0
        for p in patterns:
            assert p["count"] >= 3
            assert p["avg_energy"] >= 0.5

    def test_no_patterns_from_low_energy(self, index, tmp_path):
        """Low energy memories don't produce patterns."""
        for i in range(5):
            _insert_memory(index, f"low_{i}", concepts=["lowconcept"], energy=0.2)
        classifier = AsyncMock()
        mb_path = str(tmp_path / "mb")
        c = MBConsolidator(index=index, classifier=classifier, mb_path=mb_path)
        patterns = c.find_recurring_patterns()
        assert len(patterns) == 0

    def test_pattern_includes_source_memories(self, consolidator_with_data):
        """Each pattern has a list of source memory_ids."""
        patterns = consolidator_with_data.find_recurring_patterns()
        for p in patterns:
            assert "memory_ids" in p
            assert len(p["memory_ids"]) >= 3

    def test_single_occurrence_not_a_pattern(self, consolidator_with_data):
        """Concept in only 1 memory is not a pattern."""
        patterns = consolidator_with_data.find_recurring_patterns()
        concepts = [p["concept"] for p in patterns]
        assert "random" not in concepts


class TestMBGeneration:
    @pytest.mark.asyncio
    async def test_generate_mb_entry(self, consolidator_with_data):
        """Generates MB entry from a pattern (via classifier)."""
        patterns = consolidator_with_data.find_recurring_patterns()
        entry = await consolidator_with_data.generate_mb_entry(patterns[0])
        assert isinstance(entry, str)
        assert len(entry) > 10

    @pytest.mark.asyncio
    async def test_classify_mb_target(self, consolidator):
        """Classifies a pattern → target MB file."""
        pattern = {"concept": "encoding", "count": 4, "avg_energy": 0.8, "memory_ids": ["a", "b", "c"]}
        target = await consolidator.classify_mb_target(pattern)
        assert target.endswith(".md")


class TestMBWrite:
    def test_append_to_mb_file(self, tmp_path, consolidator):
        """Appends entry to MB file without overwriting."""
        mb_file = tmp_path / "memory_bank" / "techContext.md"
        mb_file.parent.mkdir(parents=True, exist_ok=True)
        mb_file.write_text("# TechContext\n\nexisting content\n")
        consolidator.append_to_mb(str(mb_file), "## New Pattern\nnew knowledge")
        content = mb_file.read_text()
        assert "existing content" in content
        assert "New Pattern" in content
        assert "new knowledge" in content

    def test_append_idempotent(self, tmp_path, consolidator):
        """Doesn't append the same content twice."""
        mb_file = tmp_path / "memory_bank" / "techContext.md"
        mb_file.parent.mkdir(parents=True, exist_ok=True)
        mb_file.write_text("# TechContext\n")
        consolidator.append_to_mb(str(mb_file), "## Encoding\nUTF8Encoding(false)")
        consolidator.append_to_mb(str(mb_file), "## Encoding\nUTF8Encoding(false)")
        content = mb_file.read_text()
        assert content.count("UTF8Encoding(false)") == 1

    def test_create_mb_file_if_missing(self, tmp_path, consolidator):
        """Creates MB file if it doesn't exist."""
        mb_file = tmp_path / "memory_bank" / "new_topic.md"
        consolidator.append_to_mb(str(mb_file), "## New Knowledge\nSomething learned")
        assert mb_file.exists()
        assert "New Knowledge" in mb_file.read_text()
