"""
Audit all 10 Type 3 Greek lexicon modules for loading, lookup correctness,
search, entry counts, and text quality.

Tests each module for:
  1. Loading without errors
  2. Language detection ('Greek')
  3. Reasonable entry counts
  4. lookup() correctness for known headwords
  5. find() substring search
  6. Non-existent queries return None/empty cleanly
  7. Text quality (real lexicon content, not garbled/binary)
"""

import os

import pytest

from accordance_parser.lexicon_module import LexiconModule

# ---------------------------------------------------------------------------
# Module paths (Type 3 = Greek lexicons)
# ---------------------------------------------------------------------------

_MODULE_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "modules", "Tools")

_MODULES = {
    "BDAG":                os.path.join(_MODULE_DIR, "BDAG.atool", "BDAG"),
    "Thayer":              os.path.join(_MODULE_DIR, "Thayer.atool", "Thayer"),
    "NAS Greek":           os.path.join(_MODULE_DIR, "NAS Greek.atool", "NAS Greek"),
    "Greek Strong\u2019s":  os.path.join(_MODULE_DIR,
                                         "Greek Strong\u2019s.atool",
                                         "Greek Strong\u2019s"),
    "EDG":                 os.path.join(_MODULE_DIR, "EDG.atool", "EDG"),
    "MGS":                 os.path.join(_MODULE_DIR, "MGS.atool", "MGS"),
    "Liddell & Scott":     os.path.join(_MODULE_DIR,
                                        "Liddell & Scott.atool",
                                        "Liddell & Scott"),
    "Louw & Nida":         os.path.join(_MODULE_DIR,
                                        "Louw & Nida.atool",
                                        "Louw & Nida"),
    "Spicq":               os.path.join(_MODULE_DIR, "Spicq.atool", "Spicq"),
    "TDNT Complete":       os.path.join(_MODULE_DIR,
                                        "TDNT Complete.atool",
                                        "TDNT Complete"),
}

# Known headwords for each module (from PHASE2_VERIFICATION_PLAN.md A1 table)
_KNOWN_HEADWORDS = {
    "BDAG":            ("\u03bb\u03cc\u03b3\u03bf\u03c2", "word"),  # λόγος
    "Thayer":          ("\u03bb\u03cc\u03b3\u03bf\u03c2", "word"),  # λόγος
    "NAS Greek":       ("\u03bb\u03cc\u03b3\u03bf\u03c2", "word"),  # λόγος
    "Greek Strong\u2019s": ("3056", "logos"),
    "EDG":             ("\u03bb\u03cc\u03b3\u03bf\u03c2", "\u03bb"),  # λ (EDG is etymological, returns short Greek text)
    "MGS":             ("\u03bb\u03cc\u03b3\u03bf\u03c2", "logos"),  # λόγος
    "Liddell & Scott": ("\u03bb\u03cc\u03b3\u03bf\u03c2", "word"),
    "Louw & Nida":     (None, None),
    "Spicq":           (None, None),
    "TDNT Complete":   ("\u03bb\u03cc\u03b3\u03bf\u03c2", "logos"),
}

# Expected entry counts (approximate, from PHASE2 data)
_EXPECTED_COUNTS = {
    "BDAG":            4000,
    "Thayer":          11500,
    "NAS Greek":       10600,
    "Greek Strong\u2019s": 5000,
    "EDG":             4900,
    "MGS":             84000,
    "Liddell & Scott": 22000,
    "Louw & Nida":     14000,  # ~14,843 entries
    "Spicq":           150,
    "TDNT Complete":   1400,
}


# ---------------------------------------------------------------------------
# Module-scoped fixtures
# ---------------------------------------------------------------------------

def _load_mod(path):
    assert os.path.exists(path), f"Module not found at {path}"
    return LexiconModule(path)

@pytest.fixture(scope="module")
def bdag():
    return _load_mod(_MODULES["BDAG"])

@pytest.fixture(scope="module")
def thayer():
    return _load_mod(_MODULES["Thayer"])

@pytest.fixture(scope="module")
def nas_greek():
    return _load_mod(_MODULES["NAS Greek"])

@pytest.fixture(scope="module")
def greek_strongs():
    return _load_mod(_MODULES["Greek Strong\u2019s"])

@pytest.fixture(scope="module")
def edg():
    return _load_mod(_MODULES["EDG"])

@pytest.fixture(scope="module")
def mgs():
    return _load_mod(_MODULES["MGS"])

@pytest.fixture(scope="module")
def liddell_scott():
    return _load_mod(_MODULES["Liddell & Scott"])

@pytest.fixture(scope="module")
def louw_nida():
    return _load_mod(_MODULES["Louw & Nida"])

@pytest.fixture(scope="module")
def spicq():
    return _load_mod(_MODULES["Spicq"])

@pytest.fixture(scope="module")
def tdnt():
    return _load_mod(_MODULES["TDNT Complete"])


# =========================================================================
# Category 1: Module Loading & Basic Properties
# =========================================================================

@pytest.mark.parametrize("name,fixture_name,expected_count", [
    ("BDAG",             "bdag",            _EXPECTED_COUNTS["BDAG"]),
    ("Thayer",           "thayer",          _EXPECTED_COUNTS["Thayer"]),
    ("NAS Greek",        "nas_greek",       _EXPECTED_COUNTS["NAS Greek"]),
    ("Greek Strong's",   "greek_strongs",   _EXPECTED_COUNTS["Greek Strong\u2019s"]),
    ("EDG",              "edg",             _EXPECTED_COUNTS["EDG"]),
    ("MGS",              "mgs",             _EXPECTED_COUNTS["MGS"]),
    ("Liddell & Scott",  "liddell_scott",   _EXPECTED_COUNTS["Liddell & Scott"]),
    ("Louw & Nida",      "louw_nida",       _EXPECTED_COUNTS["Louw & Nida"]),
    ("Spicq",            "spicq",           _EXPECTED_COUNTS["Spicq"]),
    ("TDNT Complete",    "tdnt",            _EXPECTED_COUNTS["TDNT Complete"]),
])
def test_module_language(name, fixture_name, expected_count, request):
    """Verify language is 'Greek' and entry count is reasonable."""
    mod = request.getfixturevalue(fixture_name)
    assert mod.language == "Greek", (
        f"{name}.language = {mod.language!r}, expected 'Greek'"
    )


@pytest.mark.parametrize("name,fixture_name,expected_count", [
    ("BDAG",             "bdag",            _EXPECTED_COUNTS["BDAG"]),
    ("Thayer",           "thayer",          _EXPECTED_COUNTS["Thayer"]),
    ("NAS Greek",        "nas_greek",       _EXPECTED_COUNTS["NAS Greek"]),
    ("Greek Strong's",   "greek_strongs",   _EXPECTED_COUNTS["Greek Strong\u2019s"]),
    ("EDG",              "edg",             _EXPECTED_COUNTS["EDG"]),
    ("MGS",              "mgs",             _EXPECTED_COUNTS["MGS"]),
    ("Liddell & Scott",  "liddell_scott",   _EXPECTED_COUNTS["Liddell & Scott"]),
    ("Louw & Nida",      "louw_nida",       _EXPECTED_COUNTS["Louw & Nida"]),
    ("Spicq",            "spicq",           _EXPECTED_COUNTS["Spicq"]),
    ("TDNT Complete",    "tdnt",            _EXPECTED_COUNTS["TDNT Complete"]),
])
def test_entry_count(name, fixture_name, expected_count, request):
    """Verify entry count is nonzero and within 2x of expected."""
    mod = request.getfixturevalue(fixture_name)
    count = len(mod.headwords())
    assert count > 0, f"{name} has 0 entries — module loaded but index empty"
    assert count >= expected_count * 0.5, (
        f"{name}: {count} entries, expected ~{expected_count} (too few)"
    )
    assert count <= expected_count * 2.5, (
        f"{name}: {count} entries, expected ~{expected_count} (too many)"
    )


@pytest.mark.parametrize("name,fixture_name", [
    ("BDAG",             "bdag"),
    ("Thayer",           "thayer"),
    ("NAS Greek",        "nas_greek"),
    ("Greek Strong's",   "greek_strongs"),
    ("EDG",              "edg"),
    ("MGS",              "mgs"),
    ("Liddell & Scott",  "liddell_scott"),
    ("Louw & Nida",      "louw_nida"),
    ("Spicq",            "spicq"),
    ("TDNT Complete",    "tdnt"),
])
def test_format_detected(name, fixture_name, request):
    """Verify a format was detected (not empty string)."""
    mod = request.getfixturevalue(fixture_name)
    assert mod.format, f"{name}.format is empty — format detection failed"


# =========================================================================
# Category 2: lookup() Correctness
# =========================================================================

@pytest.mark.parametrize("name,fixture_name,headword,expected_text", [
    ("BDAG",            "bdag",           _KNOWN_HEADWORDS["BDAG"][0],
     _KNOWN_HEADWORDS["BDAG"][1]),
    ("Thayer",          "thayer",         _KNOWN_HEADWORDS["Thayer"][0],
     _KNOWN_HEADWORDS["Thayer"][1]),
    ("NAS Greek",       "nas_greek",      _KNOWN_HEADWORDS["NAS Greek"][0],
     _KNOWN_HEADWORDS["NAS Greek"][1]),
    ("Greek Strong's",  "greek_strongs",  _KNOWN_HEADWORDS["Greek Strong\u2019s"][0],
     _KNOWN_HEADWORDS["Greek Strong\u2019s"][1]),
    ("EDG",             "edg",            _KNOWN_HEADWORDS["EDG"][0],
     _KNOWN_HEADWORDS["EDG"][1]),
    ("MGS",             "mgs",            _KNOWN_HEADWORDS["MGS"][0],
     _KNOWN_HEADWORDS["MGS"][1]),
    ("Liddell & Scott", "liddell_scott",  _KNOWN_HEADWORDS["Liddell & Scott"][0],
     _KNOWN_HEADWORDS["Liddell & Scott"][1]),
    ("TDNT Complete",   "tdnt",           _KNOWN_HEADWORDS["TDNT Complete"][0],
     _KNOWN_HEADWORDS["TDNT Complete"][1]),
])
def test_lookup_known_headword(name, fixture_name, headword, expected_text, request):
    """lookup() returns non-empty text with expected content keyword."""
    mod = request.getfixturevalue(fixture_name)
    result = mod.lookup(headword)
    assert result is not None, (
        f"{name}.lookup({headword!r}) returned None"
    )
    assert len(result) > 10, (
        f"{name}.lookup({headword!r}) returned only {len(result)} chars: {result!r}"
    )
    result_lower = result.lower()
    assert expected_text.lower() in result_lower, (
        f"{name}.lookup({headword!r}): expected text containing "
        f"{expected_text!r}, got first 200 chars: {result[:200]!r}"
    )


def test_louw_nida_lookup_first_headword(louw_nida):
    """Louw & Nida: lookup first headword returns valid text."""
    hws = louw_nida.headwords()
    assert len(hws) > 0, "Louw & Nida has 0 headwords"
    first = hws[0]
    result = louw_nida.lookup(first)
    assert result is not None, f"Louw & Nida.lookup({first!r}) returned None"
    assert len(result) > 10, f"Louw & Nida entry too short: {len(result)} chars"


def test_spicq_lookup_first_headword(spicq):
    """Spicq: lookup first headword returns valid text."""
    hws = spicq.headwords()
    assert len(hws) > 0, "Spicq has 0 headwords"
    first = hws[0]
    result = spicq.lookup(first)
    assert result is not None, f"Spicq.lookup({first!r}) returned None"
    assert len(result) > 10, f"Spicq entry too short: {len(result)} chars"


def test_thayer_lookup_by_number(thayer):
    """Thayer: lookup by Strong's number '3056' also works."""
    result = thayer.lookup("3056")
    assert result is not None, "Thayer.lookup('3056') returned None"
    assert len(result) > 100


def test_nas_greek_lookup_by_number(nas_greek):
    """NAS Greek: lookup by entry number '3056' also works."""
    result = nas_greek.lookup("3056")
    assert result is not None, "NAS Greek.lookup('3056') returned None"
    assert len(result) > 100


# =========================================================================
# Category 3: Text Quality
# =========================================================================

@pytest.mark.parametrize("name,fixture_name,headword", [
    ("BDAG",            "bdag",           _KNOWN_HEADWORDS["BDAG"][0]),
    ("Thayer",          "thayer",         _KNOWN_HEADWORDS["Thayer"][0]),
    ("NAS Greek",       "nas_greek",      _KNOWN_HEADWORDS["NAS Greek"][0]),
    ("Greek Strong's",  "greek_strongs",  _KNOWN_HEADWORDS["Greek Strong\u2019s"][0]),
    ("EDG",             "edg",            _KNOWN_HEADWORDS["EDG"][0]),
    ("MGS",             "mgs",            _KNOWN_HEADWORDS["MGS"][0]),
    ("Liddell & Scott", "liddell_scott",  _KNOWN_HEADWORDS["Liddell & Scott"][0]),
    ("TDNT Complete",   "tdnt",           _KNOWN_HEADWORDS["TDNT Complete"][0]),
])
def test_no_null_bytes(name, fixture_name, headword, request):
    """Entry text must not contain null bytes."""
    mod = request.getfixturevalue(fixture_name)
    result = mod.lookup(headword)
    assert result is not None
    assert "\x00" not in result, f"{name}: null bytes in entry text"


@pytest.mark.parametrize("name,fixture_name,headword", [
    ("BDAG",            "bdag",           _KNOWN_HEADWORDS["BDAG"][0]),
    ("Thayer",          "thayer",         _KNOWN_HEADWORDS["Thayer"][0]),
    ("NAS Greek",       "nas_greek",      _KNOWN_HEADWORDS["NAS Greek"][0]),
    ("Greek Strong's",  "greek_strongs",  _KNOWN_HEADWORDS["Greek Strong\u2019s"][0]),
    ("EDG",             "edg",            _KNOWN_HEADWORDS["EDG"][0]),
    ("MGS",             "mgs",            _KNOWN_HEADWORDS["MGS"][0]),
    ("Liddell & Scott", "liddell_scott",  _KNOWN_HEADWORDS["Liddell & Scott"][0]),
])
def test_no_section_sign_leak(name, fixture_name, headword, request):
    """§ (U+00A7) must NOT appear in output."""
    mod = request.getfixturevalue(fixture_name)
    result = mod.lookup(headword)
    assert result is not None
    assert "\u00a7" not in result, f"{name}: § leaked into output"


@pytest.mark.parametrize("name,fixture_name,headword", [
    ("BDAG",            "bdag",           _KNOWN_HEADWORDS["BDAG"][0]),
    ("Thayer",          "thayer",         _KNOWN_HEADWORDS["Thayer"][0]),
    ("NAS Greek",       "nas_greek",      _KNOWN_HEADWORDS["NAS Greek"][0]),
    ("Greek Strong's",  "greek_strongs",  _KNOWN_HEADWORDS["Greek Strong\u2019s"][0]),
    ("EDG",             "edg",            _KNOWN_HEADWORDS["EDG"][0]),
    ("MGS",             "mgs",            _KNOWN_HEADWORDS["MGS"][0]),
    ("Liddell & Scott", "liddell_scott",  _KNOWN_HEADWORDS["Liddell & Scott"][0]),
])
def test_contains_greek_unicode(name, fixture_name, headword, request):
    """Entry text should contain Greek characters."""
    mod = request.getfixturevalue(fixture_name)
    result = mod.lookup(headword)
    assert result is not None
    greek_chars = sum(
        1 for c in result
        if 0x0370 <= ord(c) <= 0x03FF or 0x1F00 <= ord(c) <= 0x1FFF
    )
    assert greek_chars > 0, f"{name}: no Greek Unicode in entry. First 200: {result[:200]!r}"


# =========================================================================
# Category 4: find() Substring Search
# =========================================================================

@pytest.mark.parametrize("name,fixture_name,query,min_results", [
    ("BDAG",            "bdag",           "\u03bb\u03cc\u03b3", 3),
    ("Thayer",          "thayer",         "\u03bb\u03cc\u03b3", 1),
    ("NAS Greek",       "nas_greek",      "\u03bb\u03cc\u03b3", 1),
    ("Greek Strong's",  "greek_strongs",  "305",  3),
    ("EDG",             "edg",            "\u03bb\u03cc\u03b3", 1),
    ("MGS",             "mgs",            "\u03bb\u03cc\u03b3", 10),
    ("Liddell & Scott", "liddell_scott",  "\u03bb\u03cc\u03b3", 1),
    ("Louw & Nida",     "louw_nida",      "1",     3),
    ("Spicq",           "spicq",          "\u03b1", 2),  # alpha (Greek headwords, not ASCII)
    ("TDNT Complete",   "tdnt",           "\u03bb\u03cc\u03b3", 1),
])
def test_find_returns_results(name, fixture_name, query, min_results, request):
    """find() returns at least min_results matches."""
    mod = request.getfixturevalue(fixture_name)
    results = mod.find(query, max_results=20)
    assert len(results) >= min_results, (
        f"{name}.find({query!r}) returned {len(results)} results, expected >= {min_results}"
    )


@pytest.mark.parametrize("name,fixture_name", [
    ("BDAG", "bdag"), ("Thayer", "thayer"), ("NAS Greek", "nas_greek"),
    ("Greek Strong's", "greek_strongs"), ("EDG", "edg"), ("MGS", "mgs"),
    ("Liddell & Scott", "liddell_scott"), ("Louw & Nida", "louw_nida"),
    ("Spicq", "spicq"), ("TDNT Complete", "tdnt"),
])
def test_find_max_results_cap(name, fixture_name, request):
    """find() respects max_results cap."""
    mod = request.getfixturevalue(fixture_name)
    results = mod.find("a", max_results=5)
    assert len(results) <= 5, f"{name}: find('a', 5) returned {len(results)} results"


# =========================================================================
# Category 5: Non-Existent Queries
# =========================================================================

MOD_FIXTURE_NAMES = [
    ("BDAG", "bdag"), ("Thayer", "thayer"), ("NAS Greek", "nas_greek"),
    ("Greek Strong's", "greek_strongs"), ("EDG", "edg"), ("MGS", "mgs"),
    ("Liddell & Scott", "liddell_scott"), ("Louw & Nida", "louw_nida"),
    ("Spicq", "spicq"), ("TDNT Complete", "tdnt"),
]

@pytest.mark.parametrize("name,fixture_name", MOD_FIXTURE_NAMES)
def test_lookup_empty_returns_none(name, fixture_name, request):
    """lookup('') must return None."""
    mod = request.getfixturevalue(fixture_name)
    assert mod.lookup("") is None, f"{name}.lookup('') returned non-None"


@pytest.mark.parametrize("name,fixture_name", MOD_FIXTURE_NAMES)
def test_lookup_nonexistent_returns_none(name, fixture_name, request):
    """lookup() for nonexistent word returns None."""
    mod = request.getfixturevalue(fixture_name)
    assert mod.lookup("NOT_IN_ANY_LEXICON_XYZZY_99999") is None


@pytest.mark.parametrize("name,fixture_name", MOD_FIXTURE_NAMES)
def test_find_nonexistent_returns_empty(name, fixture_name, request):
    """find() for nonexistent substring returns empty list."""
    mod = request.getfixturevalue(fixture_name)
    assert mod.find("XYZZY_NOT_FOUND_99999", max_results=10) == []


# =========================================================================
# Category 6: Headword Self-Consistency
# =========================================================================

@pytest.mark.parametrize("name,fixture_name", MOD_FIXTURE_NAMES)
def test_headwords_self_consistent_sample(name, fixture_name, request):
    """Sample 20 headwords — each must be lookup-able."""
    mod = request.getfixturevalue(fixture_name)
    hws = mod.headwords()
    sample_size = min(20, len(hws))
    failures = []
    for i in range(sample_size):
        hw = hws[i]
        result = mod.lookup(hw)
        if result is None:
            failures.append(f"lookup({hw!r}) -> None")
        elif len(result) < 5:
            failures.append(f"lookup({hw!r}) -> only {len(result)} chars")
    assert not failures, f"{name}: {len(failures)}/{sample_size} headword lookups failed:\n" + "\n".join(failures[:5])


# =========================================================================
# Category 7: Unicode Normalization Consistency
# =========================================================================

def test_bdag_unaccented_matches_accented(bdag):
    """BDAG lookup('λογος') matches lookup('λόγος')."""
    accented = bdag.lookup("\u03bb\u03cc\u03b3\u03bf\u03c2")
    unaccented = bdag.lookup("\u03bb\u03bf\u03b3\u03bf\u03c2")
    assert accented is not None and unaccented is not None
    assert accented[:200] == unaccented[:200]


def test_thayer_unaccented_matches_accented(thayer):
    """Thayer lookup('λογος') matches lookup('λόγος')."""
    accented = thayer.lookup("\u03bb\u03cc\u03b3\u03bf\u03c2")
    unaccented = thayer.lookup("\u03bb\u03bf\u03b3\u03bf\u03c2")
    assert accented is not None and unaccented is not None
    assert accented[:200] == unaccented[:200]


# =========================================================================
# Category 8: First/Last Entry Boundary Check
# =========================================================================

@pytest.mark.parametrize("name,fixture_name", MOD_FIXTURE_NAMES)
def test_first_and_last_entry_non_truncated(name, fixture_name, request):
    """First and last entries are non-truncated."""
    mod = request.getfixturevalue(fixture_name)
    hws = mod.headwords()
    assert len(hws) > 0, f"{name} has 0 headwords"
    for pos_label, hw in [("first", hws[0]), ("last", hws[-1])]:
        result = mod.lookup(hw)
        assert result is not None, f"{name}: {pos_label} headword {hw!r} returned None"
        assert len(result) >= 10, f"{name}: {pos_label} entry too short: {len(result)} chars"
