"""Tests for MemoryIndex — SQLite + FTS5 structured memory store."""

import json
import pytest

from neo_cortex.memory_index import MemoryIndex
from neo_cortex.models import (
    Activity,
    CompactMemory,
    MemoryRecord,
    MemoryType,
    StructuredFields,
)


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


@pytest.fixture
def record_a():
    return MemoryRecord(
        id="v3_aaa_0_10001",
        session_id="sess-aaa",
        timestamp=1707300000.0,
        turn_number=0,
        question="How to fix the crash in ProcessIO?",
        answer_preview="The crash was caused by InvalidOperationException...",
        document="Q: crash\nA: fix",
        project="neo-reloaded",
        topic="crash fix",
        activity=Activity.BUGFIX,
        energy=0.8,
    )


@pytest.fixture
def record_b():
    return MemoryRecord(
        id="v3_bbb_1_10002",
        session_id="sess-bbb",
        timestamp=1707400000.0,
        turn_number=1,
        question="Deploy neo-cortex to systemd",
        answer_preview="Created neo-cortex.service with Restart=on-failure...",
        document="Q: deploy\nA: systemd",
        project="neo-cortex",
        topic="deploy",
        activity=Activity.DEPLOY,
        energy=1.0,
    )


@pytest.fixture
def structured_a():
    return StructuredFields(
        title="Fix ProcessIO crash on dispose",
        summary="InvalidOperationException after process dispose",
        facts=["Process.HasExited throws after dispose", "Added try-catch guard"],
        concepts=["process-lifecycle", "exception-handling"],
        files_touched=["src/IO/ProcessIO.cs"],
    )


@pytest.fixture
def structured_b():
    return StructuredFields(
        title="Deploy neo-cortex systemd service",
        summary="Created systemd unit file with on-failure restart",
        facts=["Port 5074", "Restart=on-failure"],
        concepts=["systemd", "deployment"],
        files_touched=["deploy/services/neo-cortex.service"],
    )


class TestCreateAndInit:
    def test_creates_db_on_init(self, index):
        assert index.count() == 0

    def test_creates_tables(self, index):
        # Should not raise
        assert index.count() == 0


class TestInsertAndGet:
    def test_insert_and_get_by_id(self, index, record_a, structured_a):
        index.insert(record_a, structured_a)
        result = index.get_by_id("v3_aaa_0_10001")
        assert result is not None
        assert result.id == "v3_aaa_0_10001"
        assert result.project == "neo-reloaded"
        assert result.question == record_a.question

    def test_get_by_id_not_found(self, index):
        assert index.get_by_id("nonexistent") is None

    def test_insert_and_get_by_ids(self, index, record_a, record_b, structured_a, structured_b):
        index.insert(record_a, structured_a)
        index.insert(record_b, structured_b)
        results = index.get_by_ids(["v3_aaa_0_10001", "v3_bbb_1_10002"])
        assert len(results) == 2
        ids = {r.id for r in results}
        assert "v3_aaa_0_10001" in ids
        assert "v3_bbb_1_10002" in ids

    def test_get_by_ids_partial(self, index, record_a, structured_a):
        index.insert(record_a, structured_a)
        results = index.get_by_ids(["v3_aaa_0_10001", "nonexistent"])
        assert len(results) == 1

    def test_insert_without_structured(self, index, record_a):
        index.insert(record_a)
        result = index.get_by_id("v3_aaa_0_10001")
        assert result is not None
        assert result.project == "neo-reloaded"

    def test_count(self, index, record_a, record_b, structured_a, structured_b):
        assert index.count() == 0
        index.insert(record_a, structured_a)
        assert index.count() == 1
        index.insert(record_b, structured_b)
        assert index.count() == 2

    def test_insert_duplicate_upserts(self, index, record_a, structured_a):
        index.insert(record_a, structured_a)
        # Update energy
        record_a_updated = record_a.model_copy(update={"energy": 0.5})
        index.insert(record_a_updated, structured_a)
        assert index.count() == 1
        result = index.get_by_id("v3_aaa_0_10001")
        assert result.energy == 0.5


class TestSearchFTS:
    def test_search_basic(self, index, record_a, record_b, structured_a, structured_b):
        index.insert(record_a, structured_a)
        index.insert(record_b, structured_b)
        results = index.search_fts("crash")
        assert len(results) >= 1
        assert any(r.id == "v3_aaa_0_10001" for r in results)

    def test_search_with_project_filter(self, index, record_a, record_b, structured_a, structured_b):
        index.insert(record_a, structured_a)
        index.insert(record_b, structured_b)
        results = index.search_fts("", project="neo-cortex")
        assert all(r.project == "neo-cortex" for r in results)

    def test_search_with_activity_filter(self, index, record_a, record_b, structured_a, structured_b):
        index.insert(record_a, structured_a)
        index.insert(record_b, structured_b)
        results = index.search_fts("", activity="deploy")
        assert all(r.activity == "deploy" for r in results)

    def test_search_empty_query_returns_recent(self, index, record_a, record_b, structured_a, structured_b):
        index.insert(record_a, structured_a)
        index.insert(record_b, structured_b)
        results = index.search_fts("")
        assert len(results) == 2
        # Newest first
        assert results[0].timestamp >= results[1].timestamp

    def test_search_no_results(self, index, record_a, structured_a):
        index.insert(record_a, structured_a)
        results = index.search_fts("kubernetes")
        assert len(results) == 0


class TestTimeline:
    def test_timeline_newest_first(self, index, record_a, record_b, structured_a, structured_b):
        index.insert(record_a, structured_a)
        index.insert(record_b, structured_b)
        results = index.timeline(n=10)
        assert len(results) == 2
        assert results[0].timestamp > results[1].timestamp

    def test_timeline_filter_project(self, index, record_a, record_b, structured_a, structured_b):
        index.insert(record_a, structured_a)
        index.insert(record_b, structured_b)
        results = index.timeline(n=10, project="neo-cortex")
        assert len(results) == 1
        assert results[0].project == "neo-cortex"

    def test_timeline_limit(self, index, record_a, record_b, structured_a, structured_b):
        index.insert(record_a, structured_a)
        index.insert(record_b, structured_b)
        results = index.timeline(n=1)
        assert len(results) == 1


class TestStatsAndEnergy:
    def test_stats(self, index, record_a, record_b, structured_a, structured_b):
        index.insert(record_a, structured_a)
        index.insert(record_b, structured_b)
        s = index.stats(dream_count=3)
        assert s["total_memories"] == 2
        assert s["sessions"] == 2
        assert s["dream_count"] == 3
        assert s["avg_energy"] > 0

    def test_stats_empty(self, index):
        s = index.stats(dream_count=0)
        assert s["total_memories"] == 0
        assert s["sessions"] == 0

    def test_update_energy(self, index, record_a, structured_a):
        index.insert(record_a, structured_a)
        index.update_energy("v3_aaa_0_10001", 0.3)
        result = index.get_by_id("v3_aaa_0_10001")
        assert abs(result.energy - 0.3) < 0.001


class TestStructuredFields:
    def test_get_structured(self, index, record_a, structured_a):
        index.insert(record_a, structured_a)
        s = index.get_structured("v3_aaa_0_10001")
        assert s is not None
        assert s.title == "Fix ProcessIO crash on dispose"
        assert "Process.HasExited throws after dispose" in s.facts
        assert "process-lifecycle" in s.concepts
        assert "src/IO/ProcessIO.cs" in s.files_touched

    def test_get_structured_not_found(self, index):
        assert index.get_structured("nonexistent") is None
