"""
Audit all 6 Type 2 English dictionary modules for loading, language detection,
lookup correctness, search, entry counts, and text quality.

Tests each module for:
  1. Loading without errors
  2. Language detection (must be 'English', not 'Greek' or 'Hebrew')
  3. Reasonable entry counts
  4. lookup() correctness for known headwords
  5. find() substring search
  6. Text quality (no null bytes, no garbage)
  7. Edge cases (empty queries, nonexistent queries, max_results=0)
"""

import os

import pytest

from accordance_parser.lexicon_module import LexiconModule

# ---------------------------------------------------------------------------
# Module paths
# ---------------------------------------------------------------------------

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

_MODULES = {
    "NI Bible Dictionary":  os.path.join(_MODULE_DIR, "NI Bible Dictionary.atool", "NI Bible Dictionary"),
    "PhotoGuide Sampler":   os.path.join(_MODULE_DIR, "PhotoGuide Sampler.atool", "PhotoGuide Sampler"),
    "Place Names":          os.path.join(_MODULE_DIR, "Place Names.atool", "Place Names"),
    "Read Me-Modules":      os.path.join(_MODULE_DIR, "Read Me-Modules.atool", "Read Me-Modules"),
    "Sites":                os.path.join(_MODULE_DIR, "Sites.atool", "Sites"),
    "Webster":              os.path.join(_MODULE_DIR, "Webster.atool", "Webster"),
}

# Known headwords: (headword_to_lookup, expected_substring_in_result)
# Using first-headword strategy for modules where we don't have specific
# knowledge.  The first headword in a dictionary is always a real entry.
#
# For modules where we know content: used expected English words likely
# to appear in their definitions.
_KNOWN_HEADWORDS = {
    "NI Bible Dictionary":  (None, None),   # use first headword
    "PhotoGuide Sampler":   (None, None),   # use first headword
    "Place Names":          (None, None),   # use first headword
    "Read Me-Modules":      (None, None),   # use first headword (may have very few entries)
    "Sites":                (None, None),   # use first headword
    "Webster":              ("abandon", "forsake"),
}

# Expected entry count ranges (min, max).
# Read Me-Modules is documentation, not a real dictionary — may have 0 entries.
# PhotoGuide Sampler is image-heavy with very few text entries.
_EXPECTED_COUNTS = {
    "NI Bible Dictionary":  (1000, 30000),
    "PhotoGuide Sampler":   (1, 500),
    "Place Names":          (50, 5000),
    "Read Me-Modules":      (0, 1000),
    "Sites":                (10, 2000),
    "Webster":              (50000, 150000),
}


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

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


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

@pytest.fixture(scope="module")
def ni_bible_dict():
    return _load_mod(_MODULES["NI Bible Dictionary"])

@pytest.fixture(scope="module")
def photoguide():
    return _load_mod(_MODULES["PhotoGuide Sampler"])

@pytest.fixture(scope="module")
def place_names():
    return _load_mod(_MODULES["Place Names"])

@pytest.fixture(scope="module")
def read_me():
    return _load_mod(_MODULES["Read Me-Modules"])

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

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


# ---------------------------------------------------------------------------
# All-module parametrized lists
# ---------------------------------------------------------------------------

MOD_PARAMS = [
    ("NI Bible Dictionary",  "ni_bible_dict",  _EXPECTED_COUNTS["NI Bible Dictionary"]),
    ("PhotoGuide Sampler",   "photoguide",     _EXPECTED_COUNTS["PhotoGuide Sampler"]),
    ("Place Names",          "place_names",    _EXPECTED_COUNTS["Place Names"]),
    ("Read Me-Modules",      "read_me",        _EXPECTED_COUNTS["Read Me-Modules"]),
    ("Sites",                "sites",          _EXPECTED_COUNTS["Sites"]),
    ("Webster",              "webster",        _EXPECTED_COUNTS["Webster"]),
]


# =========================================================================
# Category 1: Module Loading & Language Detection
# =========================================================================

@pytest.mark.parametrize("name,fixture_name,count_range", MOD_PARAMS)
def test_language_is_english(name, fixture_name, count_range, request):
    """Every Type 2 module must report language='English'."""
    mod = request.getfixturevalue(fixture_name)
    assert mod.language == "English", (
        f"{name}.language = {mod.language!r}, expected 'English'"
    )
    assert mod.language not in ("Greek", "Hebrew"), (
        f"{name}: language is {mod.language!r} — should be English for type 2 modules"
    )


@pytest.mark.parametrize("name,fixture_name,count_range", MOD_PARAMS)
def test_entry_count_reasonable(name, fixture_name, count_range, request):
    """Entry count must be within expected range."""
    mod = request.getfixturevalue(fixture_name)
    count = len(mod.headwords())
    lo, hi = count_range
    assert lo <= count <= hi, (
        f"{name}: {count} entries, expected between {lo} and {hi}"
    )


@pytest.mark.parametrize("name,fixture_name", [(n, f) for n, f, _ in MOD_PARAMS])
def test_format_detected(name, fixture_name, request):
    """Every module must have a detected format (not empty string)."""
    mod = request.getfixturevalue(fixture_name)
    assert mod.format, f"{name}.format is empty — format detection failed"


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

def _get_lookup_params():
    """Build lookup test params: use known headword if available, else first headword."""
    params = []
    for name, fixture_name, _ in MOD_PARAMS:
        kw = _KNOWN_HEADWORDS[name]
        if kw[0] is not None:
            params.append((name, fixture_name, kw[0], kw[1]))
        else:
            params.append((name, fixture_name, None, None))
    return params


@pytest.mark.parametrize("name,fixture_name,headword,expected_text", _get_lookup_params())
def test_lookup_known_or_first_headword(name, fixture_name, headword, expected_text, request):
    """lookup() returns non-empty, readable English text."""
    mod = request.getfixturevalue(fixture_name)

    # If no known headword specified, use the first headword in the index
    hw = headword
    if hw is None:
        hws = mod.headwords()
        if not hws:
            pytest.skip(f"{name}: has 0 entries — skipping lookup test")
        hw = hws[0]

    result = mod.lookup(hw)
    assert result is not None, (
        f"{name}.lookup({hw!r}) returned None"
    )
    assert len(result) > 10, (
        f"{name}.lookup({hw!r}) returned only {len(result)} chars: {result[:100]!r}"
    )

    # If expected_text was provided, verify it's present
    if expected_text:
        result_lower = result.lower()
        assert expected_text.lower() in result_lower, (
            f"{name}.lookup({hw!r}): expected text containing "
            f"{expected_text!r}, got first 200 chars: {result[:200]!r}"
        )


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

@pytest.mark.parametrize("name,fixture_name", [(n, f) for n, f, _ in MOD_PARAMS])
def test_no_null_bytes_in_lookup(name, fixture_name, request):
    """Entry text must not contain null bytes."""
    mod = request.getfixturevalue(fixture_name)
    hws = mod.headwords()
    if not hws:
        pytest.skip(f"{name}: has 0 entries")
    # Test a sample of entries (first, last, middle)
    indices = [0]
    if len(hws) > 1:
        indices.append(len(hws) - 1)
    if len(hws) > 2:
        indices.append(len(hws) // 2)
    for idx in indices:
        result = mod.lookup(hws[idx])
        assert result is not None, f"{name}: lookup({hws[idx]!r}) returned None"
        assert "\x00" not in result, (
            f"{name}: null bytes in entry text for headword {hws[idx]!r}"
        )


@pytest.mark.parametrize("name,fixture_name", [(n, f) for n, f, _ in MOD_PARAMS])
def test_lookup_returns_english_text(name, fixture_name, request):
    """Entry text must contain printable ASCII English (or reasonable Latin-1)."""
    mod = request.getfixturevalue(fixture_name)
    hws = mod.headwords()
    if not hws:
        pytest.skip(f"{name}: has 0 entries")
    result = mod.lookup(hws[0])
    assert result is not None
    # Count printable characters (ASCII + Latin-1 supplement)
    printable = sum(1 for c in result if c.isprintable() and c != ' ')
    total = len(result)
    # At least 50% of the text should be printable characters
    assert printable / max(total, 1) > 0.5, (
        f"{name}: only {printable}/{total} printable chars — "
        f"first 200: {result[:200]!r}"
    )


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

@pytest.mark.parametrize("name,fixture_name", [(n, f) for n, f, _ in MOD_PARAMS])
def test_find_returns_results(name, fixture_name, request):
    """find() returns results for a common English substring."""
    mod = request.getfixturevalue(fixture_name)
    hws = mod.headwords()
    if not hws:
        pytest.skip(f"{name}: has 0 entries")
    # Use the first letter of the first headword as a query
    query = hws[0][:2] if len(hws[0]) >= 2 else hws[0][0]
    results = mod.find(query, max_results=20)
    assert len(results) > 0, (
        f"{name}.find({query!r}) returned 0 results"
    )
    # All results should contain the query substring (case-insensitive)
    query_lower = query.lower()
    for r in results:
        assert query_lower in r.lower(), (
            f"{name}.find({query!r}): result {r!r} does not contain {query!r}"
        )


@pytest.mark.parametrize("name,fixture_name", [(n, f) for n, f, _ in MOD_PARAMS])
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"


@pytest.mark.parametrize("name,fixture_name", [(n, f) for n, f, _ in MOD_PARAMS])
@pytest.mark.xfail(reason="BUG: find() does not respect max_results=0 — returns 1 result before checking limit")
def test_find_max_results_zero(name, fixture_name, request):
    """find() with max_results=0 should return empty list (known bug)."""
    mod = request.getfixturevalue(fixture_name)
    results = mod.find("a", max_results=0)
    assert results == [], f"{name}: find('a', 0) returned {results!r}"


# =========================================================================
# Category 5: Edge Cases
# =========================================================================

@pytest.mark.parametrize("name,fixture_name", [(n, f) for n, f, _ in MOD_PARAMS])
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", [(n, f) for n, f, _ in MOD_PARAMS])
def test_lookup_nonexistent_returns_none(name, fixture_name, request):
    """lookup() for nonexistent word returns None."""
    mod = request.getfixturevalue(fixture_name)
    assert mod.lookup("ZZ_XYZZY_NOT_IN_ANY_DICT_99999") is None, (
        f"{name}.lookup(nonexistent) returned non-None"
    )


@pytest.mark.parametrize("name,fixture_name", [(n, f) for n, f, _ in MOD_PARAMS])
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_12345", max_results=10) == [], (
        f"{name}.find(nonexistent) returned non-empty"
    )


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

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


# =========================================================================
# Category 7: First and Last Entry Boundary Checks
# =========================================================================

@pytest.mark.parametrize("name,fixture_name", [(n, f) for n, f, _ in MOD_PARAMS])
def test_first_and_last_entry_non_truncated(name, fixture_name, request):
    """First and last entries are non-truncated (reasonable length)."""
    mod = request.getfixturevalue(fixture_name)
    hws = mod.headwords()
    if not hws:
        pytest.skip(f"{name}: has 0 entries")
    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"
        )
        # Entries should be at least 10 chars (real definitions, not stubs)
        assert len(result) >= 3, (
            f"{name}: {pos_label} entry too short: {len(result)} chars"
        )


# =========================================================================
# Category 8: Module Metadata
# =========================================================================

@pytest.mark.parametrize("name,fixture_name", [(n, f) for n, f, _ in MOD_PARAMS])
def test_module_has_title(name, fixture_name, request):
    """Every module should have a nonzero title string."""
    mod = request.getfixturevalue(fixture_name)
    assert mod.title, f"{name}.title is empty"
    assert len(mod.title) > 1, f"{name}.title too short: {mod.title!r}"


@pytest.mark.parametrize("name,fixture_name", [(n, f) for n, f, _ in MOD_PARAMS])
def test_module_has_abbrev(name, fixture_name, request):
    """Every module should have a nonzero abbreviation."""
    mod = request.getfixturevalue(fixture_name)
    assert mod.abbrev, f"{name}.abbrev is empty"
    assert len(mod.abbrev) > 1, f"{name}.abbrev too short: {mod.abbrev!r}"
