"""
Audit FTS5 search index coverage and cross-module consistency for all
lexicon modules (types 3 and 4).

Tests: index coverage, Strong's consistency, language filtering,
search_all integration, index robustness, empty-module safety.

Usage:
    cd "/Users/jacobgruber/Projects/Accordance Port"
    .venv/bin/python3 -m pytest test_audit_lexicon_search.py -v
"""

import os
import sys
import tempfile
import shutil
from pathlib import Path

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

import pytest

from accordance_parser.discovery import discover_modules
from accordance_parser.lexicon_module import LexiconModule
from accordance_parser.search_index import SearchIndex


# ── Helpers ───────────────────────────────────────────────────────────

def _find_index_dir() -> str | None:
    """Find the data/ directory or return None."""
    candidate = PROJECT_ROOT / "data"
    if candidate.exists() and candidate.is_dir():
        return str(candidate)
    return None

def _temp_search_index() -> tuple[SearchIndex, str]:
    """Create a SearchIndex in a temp directory. Returns (si, tmpdir)."""
    tmpdir = tempfile.mkdtemp()
    db_path = os.path.join(tmpdir, "test_index.db")
    return SearchIndex(db_path), tmpdir


# ── Fixtures ──────────────────────────────────────────────────────────

@pytest.fixture(scope="module")
def all_modules():
    return discover_modules()


@pytest.fixture(scope="module")
def lexicon_info(all_modules):
    info = {}
    for abbr, v in all_modules.items():
        if v.get("type") in (3, 4):
            info[abbr] = v
    return info


@pytest.fixture(scope="module")
def lexicon_modules(lexicon_info) -> dict:
    mods = {}
    for abbr, info in sorted(lexicon_info.items()):
        try:
            mods[abbr] = LexiconModule(info["path"])
        except Exception:
            pass  # skip modules that fail to load
    return mods


# Cache the index dir once per session
_index_dir = _find_index_dir()

@pytest.fixture(scope="module")
def search_index():
    if _index_dir is None:
        pytest.skip("No data/ directory with search index found")
    db_path = os.path.join(_index_dir, "search_index.db")
    if not os.path.exists(db_path):
        pytest.skip(f"No search_index.db at {db_path}")
    si = SearchIndex(db_path)
    return si


# ── Test 1: Discovery & Loading ───────────────────────────────────────

class TestDiscovery:
    def test_discovery_finds_lexicon_modules(self, lexicon_info):
        count = len(lexicon_info)
        assert count >= 17, f"Expected >=17 lexicon modules (types 3/4), found {count}"

    def test_all_modules_loadable(self, lexicon_modules):
        loaded = len(lexicon_modules)
        assert loaded >= 17, f"Expected >=17 loadable modules, got {loaded}"

    def test_modules_have_headwords(self, lexicon_modules):
        empty = []
        for abbr, mod in sorted(lexicon_modules.items()):
            hw = mod.headwords()
            if isinstance(hw, list) and len(hw) == 0:
                # Allow zero-entry modules (Jenni-Westermann, etc.)
                if abbr not in {"Jenni-Westermann", "KM Hebrew Dictionary",
                                "Trench-Synonyms", "Mounce Greek Dictionary"}:
                    empty.append(f"{abbr}: 0 headwords (unexpected)")
        assert not empty, "Modules unexpectedly empty:\n" + "\n".join(empty)


# ── Test 2: Strong's Cross-Module Consistency ──────────────────────────

class TestStrongsConsistency:
    GOD_SIGNALS = {"god", "elohim", "mighty", "divine", "deity", "el", "אלהים", "אֱלֹהִים"}

    def test_bdb_h430(self, lexicon_modules):
        mod = lexicon_modules.get("BDB Complete")
        if mod is None:
            pytest.skip("BDB Complete not installed")
        result = mod.lookup("H430")
        assert result is not None
        assert len(result) > 200
        assert any(w in result.lower() for w in self.GOD_SIGNALS), f"BDB H430 not God: {result[:200]}"

    def test_nas_hebrew_h430(self, lexicon_modules):
        mod = lexicon_modules.get("NAS Hebrew")
        if mod is None:
            pytest.skip("NAS Hebrew not installed")
        result = mod.lookup("H430")
        if result is None:
            result = mod.lookup("430")
        assert result is not None
        assert any(w in result.lower() for w in self.GOD_SIGNALS), f"NAS Hebrew H430: {result[:200]}"

    def test_hebrew_strongs_h430(self, lexicon_modules):
        mod = lexicon_modules.get("Hebrew Strong's")
        if mod is None:
            pytest.skip("Hebrew Strong's not installed")
        result = mod.lookup("H430")
        if result is None:
            result = mod.lookup("430")
        assert result is not None, "Hebrew Strong's: H430/430 not found"


# ── Test 3: Index Coverage (Production DB) ─────────────────────────────

class TestIndexCoverage:
    @pytest.fixture(scope="class")
    def has_lexicon_data(self, search_index):
        status = search_index.get_status()
        sources = status.get("sources", {})
        return sources.get("lexicon", 0) > 0

    def test_index_has_lexicon_source(self, search_index, has_lexicon_data):
        status = search_index.get_status()
        sources = status.get("sources", {})
        if not has_lexicon_data and "lexicon" not in sources:
            pytest.skip("Search index not yet built — no lexicon data")
        assert "lexicon" in sources, f"No 'lexicon' in sources: {list(sources.keys())}"

    def test_index_has_data(self, search_index, has_lexicon_data):
        status = search_index.get_status()
        if not has_lexicon_data:
            pytest.skip("Search index not yet built — rebuilding is needed")
        assert status["total_rows"] > 0

    def test_search_lexicons_logos(self, search_index, has_lexicon_data):
        if not has_lexicon_data:
            pytest.skip("No index data")
        results = search_index.search_lexicons("logos", language="el", max_results=5)
        assert len(results) > 0

    def test_search_lexicons_covenant(self, search_index, has_lexicon_data):
        if not has_lexicon_data:
            pytest.skip("No index data")
        results = search_index.search_lexicons("covenant", max_results=5)
        assert len(results) > 0

    def test_search_lexicons_grace(self, search_index, has_lexicon_data):
        if not has_lexicon_data:
            pytest.skip("No index data")
        results = search_index.search_lexicons("grace", max_results=5)
        assert len(results) > 0

    def test_search_all_logos(self, search_index, has_lexicon_data):
        if not has_lexicon_data:
            pytest.skip("No index data")
        results = search_index.search("logos", source_type="all", max_results=10)
        source_types = {r[0] for r in results if r[0]}
        assert "lexicon" in source_types, f"search_all missing lexicon; got: {source_types}"

    def test_index_status_keys(self, search_index):
        status = search_index.get_status()
        for key in ("db_path", "total_rows", "sources", "last_build"):
            assert key in status, f"missing key: {key}"


# ── Test 4: Rebuild Robustness (Temp DB) ───────────────────────────────

class TestRebuild:
    def test_build_all_lexicon_only(self, lexicon_modules):
        si, tmpdir = _temp_search_index()
        try:
            total = si.build_all(lexicon_modules=lexicon_modules)
            assert isinstance(total, int)
            assert total >= 0
        finally:
            si.close()
            shutil.rmtree(tmpdir, ignore_errors=True)

    def test_index_single_lexicon_no_crash(self, lexicon_modules):
        """_index_single_lexicon must not raise on any module."""
        si, tmpdir = _temp_search_index()
        try:
            failures = []
            for abbr, mod in sorted(lexicon_modules.items()):
                try:
                    si._index_single_lexicon(abbr, mod, 0.0)
                except Exception as e:
                    failures.append(f"{abbr}: {e}")
            assert not failures, "Index crashes:\n" + "\n".join(failures)
        finally:
            si.close()
            shutil.rmtree(tmpdir, ignore_errors=True)

    def test_rebuild_then_search(self, lexicon_modules):
        """After rebuild, basic search must work."""
        si, tmpdir = _temp_search_index()
        try:
            si.build_all(lexicon_modules=lexicon_modules)
            results = si.search("god", source_type="lexicon", max_results=3)
            assert isinstance(results, list)
        finally:
            si.close()
            shutil.rmtree(tmpdir, ignore_errors=True)


# ── Test 5: Zero-entry module safety ───────────────────────────────────

class TestEmptyModules:
    ZERO_ENTRY = {"Jenni-Westermann", "KM Hebrew Dictionary",
                  "Trench-Synonyms", "Mounce Greek Dictionary"}

    def test_zero_entry_loadable(self, lexicon_modules):
        for abbr in self.ZERO_ENTRY:
            if abbr in lexicon_modules:
                mod = lexicon_modules[abbr]
                assert isinstance(mod.headwords(), list)

    def test_zero_entry_index_no_crash(self, lexicon_modules):
        si, tmpdir = _temp_search_index()
        try:
            for abbr, mod in lexicon_modules.items():
                if abbr not in self.ZERO_ENTRY:
                    continue
                try:
                    si._index_single_lexicon(abbr, mod, 0.0)
                except Exception as e:
                    pytest.fail(f"{abbr}: _index_single_lexicon raised {e}")
        finally:
            si.close()
            shutil.rmtree(tmpdir, ignore_errors=True)


# ── Test 6: Search with language filtering ─────────────────────────────

class TestLanguageFiltering:
    @pytest.fixture(scope="class")
    def has_data(self, search_index):
        return search_index.get_status()["total_rows"] > 0

    def test_en_search(self, search_index, has_data):
        if not has_data:
            pytest.skip("No index data")
        results = search_index.search("baptism", source_type="lexicon", language="en", max_results=10)
        assert isinstance(results, list)

    def test_el_search(self, search_index, has_data):
        if not has_data:
            pytest.skip("No index data")
        results = search_index.search("logos", source_type="lexicon", language="el", max_results=10)
        assert isinstance(results, list)

    def test_search_lexicons_en(self, search_index, has_data):
        if not has_data:
            pytest.skip("No index data")
        results = search_index.search_lexicons("covenant", language="en", max_results=5)
        assert len(results) > 0

    def test_search_lexicons_el(self, search_index, has_data):
        if not has_data:
            pytest.skip("No index data")
        results = search_index.search_lexicons("logos", language="el", max_results=5)
        assert len(results) > 0
