"""
Audit Type 2 English dictionary modules (Bible Words, Easton, ISBE,
NAS Topical Index, Names, Nave's).

Tests: loading, language detection, entry counts, lookup(), find(),
text quality (null bytes, C1 control chars), cross-module consistency.
"""

import pytest
import time
from pathlib import Path

from accordance_parser.lexicon_module import LexiconModule

# ---------------------------------------------------------------------------
# Module paths (smart quote in Nave's)
# ---------------------------------------------------------------------------
BASE = Path(__file__).resolve().parent.parent

MODULE_PATHS = {
    "Bible Words": BASE / "modules/Tools/Bible Words.atool/Bible Words",
    "Easton":      BASE / "modules/Tools/Easton.atool/Easton",
    "ISBE":        BASE / "modules/Tools/ISBE.atool/ISBE",
    "NAS Topical": BASE / "modules/Tools/NAS Topical Index.atool/NAS Topical Index",
    "Names":       BASE / "modules/Tools/Names.atool/Names",
    "Nave's":      BASE / "modules/Tools/Nave\u2019s.atool/Nave\u2019s",
}

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

def _load(name: str) -> LexiconModule:
    """Load a module and return it.  Fails the test if it crashes."""
    path = MODULE_PATHS[name]
    assert path.exists(), f"Module file not found: {path}"
    t0 = time.time()
    mod = LexiconModule(str(path))
    print(f"  Loaded {name} in {time.time() - t0:.2f}s  "
          f"[lang={mod.language}, fmt={mod.format}, keys={len(mod.headwords())}]")
    return mod


def _find_working_key(mod: LexiconModule, *candidates: str) -> str:
    """Return the first candidate that has a non-None lookup result."""
    for c in candidates:
        if mod.lookup(c) is not None:
            return c
    # Fallback: use the first indexed headword
    for hw in mod.headwords():
        if mod.lookup(hw) is not None:
            return hw
    raise pytest.fail(f"No working lookup key found for {mod.abbrev}")


# ---------------------------------------------------------------------------
# 1. Loading  --  no crashes
# ---------------------------------------------------------------------------

@pytest.fixture(scope="module", params=list(MODULE_PATHS))
def mod(request):
    """Load each module once per test-module scope."""
    return _load(request.param)


def test_module_data_files_exist():
    """All 6 data files exist on disk."""
    missing = [name for name, path in MODULE_PATHS.items() if not path.exists()]
    assert not missing, f"Missing data files: {missing}"


def test_loading_no_crash(mod):
    """Loading via LexiconModule should not raise any exception."""
    assert mod is not None
    assert mod.abbrev


# ---------------------------------------------------------------------------
# 2. Language detection  --  should be English (not Greek / Hebrew)
# ---------------------------------------------------------------------------

def test_language_is_english(mod):
    assert mod.language == "English", (
        f"{mod.abbrev}: expected language='English', got '{mod.language}'"
    )


# ---------------------------------------------------------------------------
# 3. Entry counts  --  reasonable, not zero
# ---------------------------------------------------------------------------

# Minimum expected counts.  Names (26) is exceptionally small and is
# flagged separately.
EXPECTED_MIN_COUNTS = {
    "Bible Words": 200,
    "Easton":      3000,
    "ISBE":        1500,
    "NAS Topical":  700,
    "Names":          20,  # only 26 letters; see note below
    "Nave's":      4000,
}


def test_entry_count_reasonable(mod):
    n = len(mod.headwords())
    min_ok = EXPECTED_MIN_COUNTS.get(mod.abbrev, 1)
    assert n >= min_ok, (
        f"{mod.abbrev}: only {n} headwords (expected >= {min_ok})"
    )


# ---------------------------------------------------------------------------
# 4. lookup()  --  known headword returns English text
# ---------------------------------------------------------------------------

# Fallback candidates checked in order; the test picks the first that works.
LOOKUP_CANDIDATES = {
    "Bible Words":  ("abandon", "love", "faith", "grace"),
    "Easton":       ("aaron", "moses", "jerusalem", "abaddon"),
    "ISBE":         ("moses", "jerusalem", "abraham", "abaddon"),
    "NAS Topical":  ("abide", "love", "faith", "grace"),
    "Names":        ("aaron", "moses", "abraham"),
    "Nave's":       ("aaron", "moses", "faith", "jerusalem"),
}


def test_lookup_returns_text(mod):
    candidates = LOOKUP_CANDIDATES.get(mod.abbrev, [])
    key = _find_working_key(mod, *candidates)
    text = mod.lookup(key)
    assert text is not None, f"{mod.abbrev}: lookup('{key}') returned None"
    assert isinstance(text, str), (
        f"{mod.abbrev}: lookup('{key}') returned {type(text)}, not str"
    )
    assert len(text) > 20, (
        f"{mod.abbrev}: lookup('{key}') is too short ({len(text)} chars)"
    )


def test_lookup_text_is_english(mod):
    """Verify the returned text is predominantly printable ASCII (English)."""
    candidates = LOOKUP_CANDIDATES.get(mod.abbrev, [])
    key = _find_working_key(mod, *candidates)
    text = mod.lookup(key)
    # Count printable ASCII characters
    ascii_printable = sum(1 for c in text if 0x20 <= ord(c) <= 0x7E)
    total = max(len(text), 1)
    ratio = ascii_printable / total
    assert ratio > 0.80, (
        f"{mod.abbrev}: lookup('{key}') is only {ratio:.1%} printable ASCII "
        f"(expected > 80%) — may contain garbled text"
    )


# ---------------------------------------------------------------------------
# 5. find()  --  headword substring search works
# ---------------------------------------------------------------------------

FIND_QUERIES = {
    "Bible Words":  "love",
    "Easton":       "aaron",
    "ISBE":         "abraham",
    "NAS Topical":  "faith",
    "Names":        "aaron",
    "Nave's":       "aaron",
}


def test_find_returns_results(mod):
    query = FIND_QUERIES.get(mod.abbrev, "a")
    results = mod.find(query, max_results=20)
    assert isinstance(results, list), (
        f"{mod.abbrev}: find('{query}') returned {type(results)}, not list"
    )
    assert len(results) >= 1, (
        f"{mod.abbrev}: find('{query}') returned 0 results"
    )


def test_find_results_contain_query(mod):
    query = FIND_QUERIES.get(mod.abbrev, "a")
    results = mod.find(query, max_results=20)
    query_lower = query.lower()
    for r in results[:10]:
        assert query_lower in r.lower(), (
            f"{mod.abbrev}: find('{query}') returned '{r}' "
            f"which does not contain the query"
        )


# ---------------------------------------------------------------------------
# 6. Text quality  --  no null bytes, no C1 control characters
# ---------------------------------------------------------------------------

def test_no_null_bytes_in_lookup(mod):
    """All lookup results must be free of null bytes (U+0000)."""
    for hw in mod.headwords()[:50]:  # sample first 50 entries
        text = mod.lookup(hw)
        if text and "\0" in text:
            pos = text.index("\0")
            pytest.fail(
                f"{mod.abbrev}: null byte in lookup('{hw}') at position {pos}"
            )


def test_no_c1_control_chars_in_lookup(mod):
    """All lookup results must be free of C1 control chars (U+0080-U+009F)."""
    for hw in mod.headwords()[:50]:
        text = mod.lookup(hw)
        if text:
            # Per _postprocess_decoded and _read_entry, C1 bytes are already
            # replaced with spaces.  Double-check they're fully removed.
            for i, c in enumerate(text):
                if 0x80 <= ord(c) <= 0x9F:
                    pytest.fail(
                        f"{mod.abbrev}: C1 control char U+{ord(c):04X} "
                        f"in lookup('{hw}') at position {i}"
                    )


# ---------------------------------------------------------------------------
# 7. Cross-module consistency  --  same topic returns relevant content
# ---------------------------------------------------------------------------

# Topics known to exist in all/most English dictionaries.
CROSS_TOPICS_FALLBACK = {
    "abaddon": ["Easton", "ISBE", "Nave's"],
    "aaron":   ["Easton", "Nave's", "Names"],
    "moses":   ["Easton", "ISBE", "Nave's"],
    "love":    ["Bible Words", "NAS Topical"],
    "faith":   ["Bible Words", "NAS Topical", "Nave's"],
}


@pytest.mark.parametrize("topic,expected_modules", [
    ("aaron", ["Easton", "Nave's"]),
    ("moses", ["Easton", "ISBE", "Nave's"]),
    ("faith", ["Bible Words", "NAS Topical"]),
    ("love",  ["Bible Words", "NAS Topical"]),
])
def test_cross_module_topic(topic, expected_modules):
    """Each topic should be findable in the listed dictionaries and return
    non-trivial English text."""
    ok_count = 0
    for name in expected_modules:
        mod = _load(name)
        text = mod.lookup(topic)
        if text is None:
            print(f"  {name}: lookup('{topic}') returned None (may not be indexed)")
            continue
        assert len(text) > 20, (
            f"{name}: lookup('{topic}') too short ({len(text)} chars)"
        )
        # Quick sanity: text should contain the topic or a known variant
        ok_count += 1
    assert ok_count >= 1, (
        f"No module returned results for topic '{topic}'"
    )
