"""
Tests for pulse.brain.index.search_index — verifies rebuild covers all brain tables.

Confirmed brain tables indexed:
  lessons, failures, patterns, decisions (JSONL)
  knowledge / Semantic/*.json
  schemas (Evidence/Metrics/schemas.json)
  procedures (Evidence/Metrics/procedural_log.json)
  evidence (Evidence/**/*.json)

Dynamic-only sources (NOT indexed):
  predictions, anti_patterns, graph, private, episodic, working_memory
"""
from __future__ import annotations

import json
import sys
from pathlib import Path
from unittest.mock import patch

import pytest

PROJECT_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(PROJECT_ROOT))


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _write_jsonl(path: Path, entries: list[dict]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    with open(path, "w", encoding="utf-8") as f:
        for e in entries:
            f.write(json.dumps(e) + "\n")


def _write_json(path: Path, data) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(data), encoding="utf-8")


def _make_jsonl_entry(content: str, title: str = "") -> dict:
    return {
        "content": content,
        "title": title or content[:60],
        "timestamp": "2026-03-01T00:00:00Z",
        "confidence": 0.8,
    }


def _build_index_with_tmp(tmp_path: Path) -> dict:
    """Build index using tmp_path as the brain root. Returns the index dict."""
    from pulse.brain.index.search_index import build_index

    # --- JSONL files ---
    lessons_file = tmp_path / "Lessons" / "auto_lessons.jsonl"
    fails_file = tmp_path / "Failures" / "auto_fails.jsonl"
    patterns_file = tmp_path / "Patterns" / "auto_patterns.jsonl"
    decisions_file = tmp_path / "Decisions" / "auto_decisions.jsonl"

    _write_jsonl(lessons_file, [_make_jsonl_entry("use filelock for atomic writes to prevent corruption")])
    _write_jsonl(fails_file, [_make_jsonl_entry("subprocess encoding crash on windows cp1252 default")])
    _write_jsonl(patterns_file, [_make_jsonl_entry("always validate jsonl schema before brain write")])
    _write_jsonl(decisions_file, [_make_jsonl_entry("decision to remove knowledge count cap for brain growth")])

    # --- Semantic/knowledge JSON ---
    semantic_dir = tmp_path / "Semantic"
    semantic_dir.mkdir(parents=True, exist_ok=True)
    _write_json(semantic_dir / "auto_facts.json", [
        {"description": "temporal decay reduces confidence score for stale knowledge items", "timestamp": "2026-03-01"},
    ])

    # --- Schemas JSON ---
    schemas_file = tmp_path / "Evidence" / "Metrics" / "schemas.json"
    _write_json(schemas_file, [
        {"name": "lesson_schema", "description": "schema for brain lesson entries validation", "promoted_at": "2026-03-01"},
    ])

    # --- Procedures JSON ---
    proc_file = tmp_path / "Evidence" / "Metrics" / "procedural_log.json"
    _write_json(proc_file, [
        {"procedure": "brain consolidation dedup stage removes duplicate lessons", "success_rate": 0.95, "total_runs": 10, "last_run": "2026-03-01"},
    ])

    # --- Evidence sessions ---
    evidence_sessions_dir = tmp_path / "Evidence" / "Sessions"
    evidence_sessions_dir.mkdir(parents=True, exist_ok=True)
    _write_json(evidence_sessions_dir / "session_01.json", [
        {"evidence": "agent claimed task atomically using filelock mechanism", "timestamp": "2026-03-01"},
    ])

    index_file = tmp_path / "search_index.json"

    with (
        patch("pulse.brain.index.search_index.LESSONS_JSONL", lessons_file),
        patch("pulse.brain.index.search_index.FAILS_JSONL", fails_file),
        patch("pulse.brain.index.search_index.PATTERNS_JSONL", patterns_file),
        patch("pulse.brain.index.search_index.DECISIONS_JSONL", decisions_file),
        patch("pulse.brain.index.search_index.HIPPOCAMPUS_DIR", tmp_path),
        patch("pulse.brain.index.search_index.EVIDENCE_DIR", tmp_path / "Evidence"),
        patch("pulse.brain.index.search_index.INDEX_FILE", index_file),
    ):
        result = build_index(verbose=False)

    index_data = json.loads(index_file.read_text(encoding="utf-8"))
    return result, index_data


# ---------------------------------------------------------------------------
# build_index: all sources produce items
# ---------------------------------------------------------------------------

class TestBuildIndexCoversAllTables:

    def test_build_index_returns_ok_status(self, tmp_path):
        result, _ = _build_index_with_tmp(tmp_path)
        assert result["status"] == "ok"
        assert result["items"] > 0

    def test_lessons_table_indexed(self, tmp_path):
        _, index_data = _build_index_with_tmp(tmp_path)
        sources = [item["source"] for item in index_data["items"]]
        assert "lessons" in sources, "Lessons JSONL must be indexed"

    def test_failures_table_indexed(self, tmp_path):
        _, index_data = _build_index_with_tmp(tmp_path)
        sources = [item["source"] for item in index_data["items"]]
        assert "failures" in sources, "Failures JSONL must be indexed"

    def test_patterns_table_indexed(self, tmp_path):
        _, index_data = _build_index_with_tmp(tmp_path)
        sources = [item["source"] for item in index_data["items"]]
        assert "patterns" in sources, "Patterns JSONL must be indexed"

    def test_decisions_table_indexed(self, tmp_path):
        _, index_data = _build_index_with_tmp(tmp_path)
        sources = [item["source"] for item in index_data["items"]]
        assert "decisions" in sources, "Decisions JSONL must be indexed"

    def test_semantic_knowledge_table_indexed(self, tmp_path):
        _, index_data = _build_index_with_tmp(tmp_path)
        sources = [item["source"] for item in index_data["items"]]
        assert "knowledge" in sources, "Semantic/*.json knowledge must be indexed"

    def test_schemas_table_indexed(self, tmp_path):
        _, index_data = _build_index_with_tmp(tmp_path)
        sources = [item["source"] for item in index_data["items"]]
        assert "schemas" in sources, "Evidence/Metrics/schemas.json must be indexed"

    def test_procedures_table_indexed(self, tmp_path):
        _, index_data = _build_index_with_tmp(tmp_path)
        sources = [item["source"] for item in index_data["items"]]
        assert "procedures" in sources, "Evidence/Metrics/procedural_log.json must be indexed"

    def test_evidence_sessions_indexed(self, tmp_path):
        _, index_data = _build_index_with_tmp(tmp_path)
        sources = [item["source"] for item in index_data["items"]]
        assert "evidence" in sources, "Evidence/**/*.json sessions must be indexed"

    def test_all_eight_source_types_present(self, tmp_path):
        """All static brain tables must appear in a single index build."""
        _, index_data = _build_index_with_tmp(tmp_path)
        sources = {item["source"] for item in index_data["items"]}
        expected = {"lessons", "failures", "patterns", "decisions", "knowledge", "schemas", "procedures", "evidence"}
        missing = expected - sources
        assert not missing, f"These sources missing from index: {missing}"

    def test_index_structure_is_valid(self, tmp_path):
        result, index_data = _build_index_with_tmp(tmp_path)
        assert index_data.get("version") == 1
        assert "built_at" in index_data
        assert "items" in index_data
        assert "index" in index_data
        assert isinstance(index_data["items"], list)
        assert isinstance(index_data["index"], dict)
        assert index_data["item_count"] == len(index_data["items"])

    def test_inverted_index_maps_keywords_to_item_ids(self, tmp_path):
        """Every item ID in the inverted index must be a valid index into items[]."""
        _, index_data = _build_index_with_tmp(tmp_path)
        items = index_data["items"]
        for keyword, ids in index_data["index"].items():
            for item_id in ids:
                assert 0 <= item_id < len(items), (
                    f"Keyword '{keyword}' maps to invalid item_id {item_id} "
                    f"(only {len(items)} items exist)"
                )

    def test_item_count_matches_result_metadata(self, tmp_path):
        result, _ = _build_index_with_tmp(tmp_path)
        assert result["items"] >= 8, "At least one item per source type expected"


# ---------------------------------------------------------------------------
# build_index: dynamic sources are NOT included
# ---------------------------------------------------------------------------

class TestDynamicSourcesNotIndexed:

    def test_predictions_not_indexed(self, tmp_path):
        _, index_data = _build_index_with_tmp(tmp_path)
        sources = {item["source"] for item in index_data["items"]}
        assert "predictions" not in sources, "predictions is dynamic — must not be indexed"

    def test_anti_patterns_not_indexed(self, tmp_path):
        _, index_data = _build_index_with_tmp(tmp_path)
        sources = {item["source"] for item in index_data["items"]}
        assert "anti_patterns" not in sources, "anti_patterns is dynamic — must not be indexed"

    def test_graph_not_indexed(self, tmp_path):
        """knowledge_graph.json is explicitly skipped in build_index."""
        _, index_data = _build_index_with_tmp(tmp_path)
        sources = {item["source"] for item in index_data["items"]}
        assert "graph" not in sources, "knowledge_graph is large/dynamic — must not be indexed"


# ---------------------------------------------------------------------------
# build_index: graceful handling of missing/empty files
# ---------------------------------------------------------------------------

class TestBuildIndexGracefulDegradation:

    def test_empty_jsonl_files_do_not_crash(self, tmp_path):
        """Empty JSONL files for any source must not raise exceptions."""
        from pulse.brain.index.search_index import build_index

        lessons_file = tmp_path / "Lessons" / "auto_lessons.jsonl"
        fails_file = tmp_path / "Failures" / "auto_fails.jsonl"
        patterns_file = tmp_path / "Patterns" / "auto_patterns.jsonl"
        decisions_file = tmp_path / "Decisions" / "auto_decisions.jsonl"

        for f in [lessons_file, fails_file, patterns_file, decisions_file]:
            f.parent.mkdir(parents=True, exist_ok=True)
            f.write_text("", encoding="utf-8")

        index_file = tmp_path / "search_index.json"
        with (
            patch("pulse.brain.index.search_index.LESSONS_JSONL", lessons_file),
            patch("pulse.brain.index.search_index.FAILS_JSONL", fails_file),
            patch("pulse.brain.index.search_index.PATTERNS_JSONL", patterns_file),
            patch("pulse.brain.index.search_index.DECISIONS_JSONL", decisions_file),
            patch("pulse.brain.index.search_index.HIPPOCAMPUS_DIR", tmp_path),
            patch("pulse.brain.index.search_index.EVIDENCE_DIR", tmp_path / "Evidence"),
            patch("pulse.brain.index.search_index.INDEX_FILE", index_file),
        ):
            result = build_index()
        assert result["status"] == "ok"
        assert result["items"] == 0

    def test_missing_optional_files_do_not_crash(self, tmp_path):
        """Schemas and procedures files that don't exist are safely skipped."""
        from pulse.brain.index.search_index import build_index

        lessons_file = tmp_path / "Lessons" / "auto_lessons.jsonl"
        _write_jsonl(lessons_file, [_make_jsonl_entry("atomic write strategy with filelock")])
        missing = tmp_path / "nonexistent.jsonl"
        index_file = tmp_path / "search_index.json"

        with (
            patch("pulse.brain.index.search_index.LESSONS_JSONL", lessons_file),
            patch("pulse.brain.index.search_index.FAILS_JSONL", missing),
            patch("pulse.brain.index.search_index.PATTERNS_JSONL", missing),
            patch("pulse.brain.index.search_index.DECISIONS_JSONL", missing),
            patch("pulse.brain.index.search_index.HIPPOCAMPUS_DIR", tmp_path),
            patch("pulse.brain.index.search_index.EVIDENCE_DIR", tmp_path / "Evidence"),
            patch("pulse.brain.index.search_index.INDEX_FILE", index_file),
        ):
            result = build_index()
        assert result["status"] == "ok"
        assert result["items"] >= 1


# ---------------------------------------------------------------------------
# query_index: finds items from each source type
# ---------------------------------------------------------------------------

class TestQueryIndexFindsAllSources:

    def test_query_finds_lesson_items(self, tmp_path):
        from pulse.brain.index.search_index import query_index

        _, index_data = _build_index_with_tmp(tmp_path)
        index_file = tmp_path / "search_index.json"
        _write_json(index_file, index_data)

        with patch("pulse.brain.index.search_index.INDEX_FILE", index_file):
            results = query_index("filelock atomic write", {}, max_results=10)

        sources = {r["source"] for r in results}
        assert "lessons" in sources or len(results) > 0, "Expected lesson hit for 'filelock atomic write'"

    def test_query_finds_failure_items(self, tmp_path):
        from pulse.brain.index.search_index import query_index

        _, index_data = _build_index_with_tmp(tmp_path)
        index_file = tmp_path / "search_index.json"
        _write_json(index_file, index_data)

        with patch("pulse.brain.index.search_index.INDEX_FILE", index_file):
            results = query_index("subprocess encoding windows crash", {}, max_results=10)

        sources = {r["source"] for r in results}
        assert "failures" in sources or len(results) > 0

    def test_query_finds_pattern_items(self, tmp_path):
        from pulse.brain.index.search_index import query_index

        _, index_data = _build_index_with_tmp(tmp_path)
        index_file = tmp_path / "search_index.json"
        _write_json(index_file, index_data)

        with patch("pulse.brain.index.search_index.INDEX_FILE", index_file):
            results = query_index("validate jsonl schema brain write", {}, max_results=10)

        sources = {r["source"] for r in results}
        assert "patterns" in sources or len(results) > 0

    def test_query_returns_from_index_flag(self, tmp_path):
        """All results from query_index must have from_index=True."""
        from pulse.brain.index.search_index import query_index

        _, index_data = _build_index_with_tmp(tmp_path)
        index_file = tmp_path / "search_index.json"
        _write_json(index_file, index_data)

        with patch("pulse.brain.index.search_index.INDEX_FILE", index_file):
            results = query_index("filelock atomic write brain", {}, max_results=10)

        for r in results:
            assert r.get("from_index") is True, "Results from query_index must be tagged from_index=True"

    def test_query_returns_empty_for_missing_index(self, tmp_path):
        from pulse.brain.index.search_index import query_index

        missing = tmp_path / "nonexistent_index.json"
        with patch("pulse.brain.index.search_index.INDEX_FILE", missing):
            results = query_index("anything", {})
        assert results == []

    def test_query_max_results_respected(self, tmp_path):
        from pulse.brain.index.search_index import query_index

        _, index_data = _build_index_with_tmp(tmp_path)
        index_file = tmp_path / "search_index.json"
        _write_json(index_file, index_data)

        with patch("pulse.brain.index.search_index.INDEX_FILE", index_file):
            results = query_index("brain atomic write filelock schema evidence", {}, max_results=3)

        assert len(results) <= 3
