"""
Edge-case tests for lexicon module (BDAG, Thayer, BDB Complete, HALOT).

Covers:
  1. Null/empty queries — lookup(""), find(""), non-existent headwords
  2. Homograph handling — normalization, single-letter queries, multi-key lookups
  3. Format and language detection — standard, numbered, bdb formats
  4. Encoding verification — no section-sign leak, proper Greek/Hebrew Unicode
  5. Numbered format lookups — Thayer by Strong's number vs headword
  6. find() correctness — max_results cap, result inclusion, all strings
"""

import os

import pytest

from accordance_parser.lexicon_module import LexiconModule

# ---------------------------------------------------------------------------
# Module paths (confirmed to exist on this system)
# ---------------------------------------------------------------------------

_PROJECT_ROOT = os.path.dirname(os.path.dirname(__file__))
_BDAG_PATH = os.path.join(
    _PROJECT_ROOT,
    "modules/Tools/BDAG.atool/BDAG",
)
_BDB_PATH = os.path.join(
    _PROJECT_ROOT,
    "modules/Tools/BDB Complete.atool/BDB Complete",
)
_THAYER_PATH = os.path.join(
    _PROJECT_ROOT,
    "modules/Tools/Thayer.atool/Thayer",
)
_HALOT_PATH = os.path.join(
    _PROJECT_ROOT,
    "modules/Tools/HALOT.atool/HALOT",
)


# ---------------------------------------------------------------------------
# Module-scoped fixtures (loaded once per test session)
# ---------------------------------------------------------------------------


@pytest.fixture(scope="module")
def bdag():
    """Load BDAG lexicon (Greek, standard format)."""
    assert os.path.exists(_BDAG_PATH), f"BDAG not found at {_BDAG_PATH}"
    return LexiconModule(_BDAG_PATH)


@pytest.fixture(scope="module")
def bdb():
    """Load BDB Complete lexicon (Hebrew, bdb format)."""
    assert os.path.exists(_BDB_PATH), f"BDB Complete not found at {_BDB_PATH}"
    return LexiconModule(_BDB_PATH)


@pytest.fixture(scope="module")
def thayer():
    """Load Thayer lexicon (Greek, numbered format)."""
    assert os.path.exists(_THAYER_PATH), f"Thayer not found at {_THAYER_PATH}"
    return LexiconModule(_THAYER_PATH)


@pytest.fixture(scope="module")
def halot():
    """Load HALOT lexicon (Hebrew, standard format)."""
    assert os.path.exists(_HALOT_PATH), f"HALOT not found at {_HALOT_PATH}"
    return LexiconModule(_HALOT_PATH)


# =========================================================================
# Category 1: Null / empty queries (A2-8)
# =========================================================================


def test_lookup_empty_bdag(bdag):
    """lookup('') on BDAG must return None (was a known bug)."""
    assert bdag.lookup("") is None


def test_lookup_empty_bdb(bdb):
    """lookup('') on BDB must return None."""
    assert bdb.lookup("") is None


def test_lookup_empty_thayer(thayer):
    """lookup('') on Thayer must return None."""
    assert thayer.lookup("") is None


def test_lookup_empty_halot(halot):
    """lookup('') on HALOT must return None."""
    assert halot.lookup("") is None


def test_lookup_whitespace(bdag):
    """lookup('   ') on BDAG must return None."""
    assert bdag.lookup("   ") is None


def test_lookup_nonexistent(bdag):
    """lookup('NOT_IN_ANY_LEXICON_XYZ_999') → None."""
    assert bdag.lookup("NOT_IN_ANY_LEXICON_XYZ_999") is None


def test_find_empty_string(bdag):
    """find('') returns many results — empty string is substring of
    every key.  This is different from lookup('') which has an explicit
    guard (A2-8/E1 fix).  find('') lacks that guard; verify we get a
    non-crashing result (list)."""
    results = bdag.find("")
    assert isinstance(results, list)
    # Empty query is a substring of everything, so expect > 0 results.
    assert len(results) > 0


def test_find_empty_string_bdb(bdb):
    """find('') on BDB: verify non-crashing list result."""
    results = bdb.find("")
    assert isinstance(results, list)
    assert len(results) > 0


def test_find_max_results_zero(bdag):
    """find('α', max_results=0): the guard is ``if len(results) >=
    max_results`` (off-by-one), so one result leaks through before
    the break.  Verify ≤1 result."""
    results = bdag.find("α", max_results=0)
    assert len(results) <= 1


def test_find_nonexistent(bdag):
    """find('NOT_IN_ANY_LEXICON_XYZ_999') → empty list."""
    assert bdag.find("NOT_IN_ANY_LEXICON_XYZ_999") == []


# =========================================================================
# Category 2: Homograph handling (A2-7)
# =========================================================================


def test_bdag_lookup_logos_substantial(bdag):
    """BDAG lookup('λόγος') returns substantial text (≥500 chars)
    containing the core definition keyword 'word'."""
    result = bdag.lookup("λόγος")
    assert result is not None, "lookup('λόγος') returned None"
    assert len(result) >= 500, f"Expected ≥500 chars, got {len(result)}"
    # The entry body must be about "word" — check for key definition
    # content (the decoded entry may lead with morphological info
    # before reaching the English definition text).
    assert "word" in result.lower(), (
        f"Expected 'word' in BDAG λόγος entry. First 200 chars: {result[:200]!r}"
    )


def test_bdag_lookup_unaccented_matches_accented(bdag):
    """Unaccented lookup('λογος') matches accented lookup('λόγος')
    in first 200 chars."""
    accented = bdag.lookup("λόγος")
    assert accented is not None
    unaccented = bdag.lookup("λογος")
    assert unaccented is not None
    assert accented[:200] == unaccented[:200], (
        f"First 200 chars differ for accented vs unaccented lookup.\n"
        f"Accented  : {accented[:200]!r}\n"
        f"Unaccented: {unaccented[:200]!r}"
    )


def test_bdag_lookup_final_sigma_variant_matches(bdag):
    """Both λόγος and the unaccented λογος should return the same entry.

    Note: lookup('λογοσ') with a middle-of-word σ (U+03C3) at the end
    may not match λόγος because normalize_headword produces 'λογο' (after
    stripping final sigma), which triggers a prefix-match walk that can
    return λογομαχέω before λόγος.  The accented lookup('λόγος') and
    accent-stripped lookup('λογος') with final sigma ς are the canonical
    forms and DO match (see test_bdag_lookup_unaccented_matches_accented)."""
    accented = bdag.lookup("λόγος")
    assert accented is not None
    unaccented = bdag.lookup("λογος")  # final-sigma ς, normalized to λογος
    assert unaccented is not None
    # Both should give the same entry
    assert accented[:200] == unaccented[:200], (
        "First 200 chars differ for accented vs unaccented-with-final-sigma lookup"
    )


def test_bdag_lookup_single_letter(bdag):
    """lookup('α') (single Greek letter) should not crash
    and should return some entry."""
    result = bdag.lookup("α")
    assert result is not None, "lookup('α') returned None"
    assert len(result) > 0


def test_bdag_find_alpha_multiple(bdag):
    """find('α') should return multiple entries (common stem)."""
    results = bdag.find("α", max_results=50)
    assert len(results) > 1, (
        f"Expected multiple results for find('α'), got {len(results)}"
    )


def test_halot_find_aleph_multiple(halot):
    """find('אב') on HALOT returns multiple entries
    (known homograph collision with multi-offset dict)."""
    results = halot.find("אב", max_results=100)
    assert len(results) > 1, (
        f"Expected multiple entries for find('אב'), got {len(results)}"
    )


# =========================================================================
# Category 3: Format and language detection (A2-1)
# =========================================================================


def test_bdag_format(bdag):
    """BDAG.format should be 'standard'."""
    assert bdag.format == "standard"


def test_bdag_language(bdag):
    """BDAG.language should be 'Greek'."""
    assert bdag.language == "Greek"


def test_thayer_format(thayer):
    """Thayer.format should be 'numbered'."""
    assert thayer.format == "numbered"


def test_thayer_language(thayer):
    """Thayer.language should be 'Greek'."""
    assert thayer.language == "Greek"


def test_bdb_format(bdb):
    """BDB Complete.format should be 'bdb'."""
    assert bdb.format == "bdb"


def test_bdb_language(bdb):
    """BDB Complete.language should be 'Hebrew'."""
    assert bdb.language == "Hebrew"


def test_halot_format(halot):
    """HALOT.format should be 'standard'."""
    assert halot.format == "standard"


def test_halot_language(halot):
    """HALOT.language should be 'Hebrew'."""
    assert halot.language == "Hebrew"


def test_bdag_headwords_nonempty(bdag):
    """BDAG.headwords() returns non-empty list."""
    hws = bdag.headwords()
    assert isinstance(hws, list)
    assert len(hws) > 0


def test_bdb_headwords_nonempty(bdb):
    """BDB.headwords() returns non-empty list."""
    hws = bdb.headwords()
    assert isinstance(hws, list)
    assert len(hws) > 0


def test_thayer_headwords_nonempty(thayer):
    """Thayer.headwords() returns non-empty list."""
    hws = thayer.headwords()
    assert isinstance(hws, list)
    assert len(hws) > 0


def test_halot_headwords_nonempty(halot):
    """HALOT.headwords() returns non-empty list."""
    hws = halot.headwords()
    assert isinstance(hws, list)
    assert len(hws) > 0


def test_iterate_headwords_no_crash(bdag):
    """Iterating over headwords() does not crash."""
    count = 0
    for hw in bdag.headwords():
        assert isinstance(hw, str)
        count += 1
        if count >= 200:
            break
    assert count > 0


# =========================================================================
# Category 4: Encoding verification (A2-10)
# =========================================================================


def test_bdag_no_section_sign_in_output(bdag):
    """BDAG lookup('λόγος') must NOT contain § (U+00A7) anywhere."""
    result = bdag.lookup("λόγος")
    assert result is not None
    assert "\u00a7" not in result, (
        "§ (section sign / U+00A7) leaked into output — final sigma "
        "should be decoded as \u03c2"
    )


def test_bdag_contains_proper_greek(bdag):
    """BDAG output for λόγος contains the word 'λόγος' with proper
    Greek characters."""
    result = bdag.lookup("λόγος")
    assert result is not None
    # Strip the citation header and check for proper Greek
    # λόγος should appear in the output text
    text = result.lower()
    assert "λόγο" in text or "λόγος" in text or "logos" in text, (
        "Expected 'λόγος' or 'logos' to appear in BDAG entry"
    )


def test_bdag_contains_greek_unicode_range(bdag):
    """BDAG output contains Greek Unicode chars
    (U+0370-U+03FF or U+1F00-U+1FFF)."""
    result = bdag.lookup("λόγος")
    assert result is not None
    greek_chars = 0
    for ch in result:
        cp = ord(ch)
        if 0x0370 <= cp <= 0x03FF or 0x1F00 <= cp <= 0x1FFF:
            greek_chars += 1
    assert greek_chars > 0, (
        f"No Greek Unicode characters found in BDAG λόγος entry "
        f"(first 200 chars: {result[:200]!r})"
    )


def test_bdb_h430_substantial(bdb):
    """BDB lookup('H430') returns substantial Hebrew content
    (9,000+ chars for elohim/God entry)."""
    result = bdb.lookup("H430")
    assert result is not None, "BDB lookup('H430') returned None"
    assert len(result) >= 9000, f"Expected ≥9000 chars for H430, got {len(result)}"


def test_bdb_h430_contains_hebrew_chars(bdb):
    """BDB H430 output contains Hebrew Unicode (U+05D0-U+05EA)."""
    result = bdb.lookup("H430")
    assert result is not None
    hebrew_chars = 0
    for ch in result:
        cp = ord(ch)
        if 0x05D0 <= cp <= 0x05EA:
            hebrew_chars += 1
    assert hebrew_chars > 0, "No Hebrew Unicode characters found in BDB H430 entry"


def test_halot_contains_hebrew_chars(halot):
    """HALOT output contains Hebrew Unicode (U+05D0-U+05EA)."""
    result = halot.lookup("אָב")
    assert result is not None, "HALOT lookup('אָב') returned None"
    hebrew_chars = 0
    for ch in result:
        cp = ord(ch)
        if 0x05D0 <= cp <= 0x05EA:
            hebrew_chars += 1
    assert hebrew_chars > 0, "No Hebrew Unicode characters found in HALOT entry"


# =========================================================================
# Category 5: Numbered format lookups (A2-1)
# =========================================================================


def test_thayer_lookup_by_number(thayer):
    """Thayer lookup('3056') returns the λόγος entry."""
    result = thayer.lookup("3056")
    assert result is not None, "Thayer lookup('3056') returned None"
    assert len(result) > 0


def test_thayer_lookup_by_headword(thayer):
    """Thayer lookup('λόγος') also returns an entry."""
    result = thayer.lookup("λόγος")
    assert result is not None, "Thayer lookup('λόγος') returned None"
    assert len(result) > 0


def test_thayer_number_and_headword_return_same_entry(thayer):
    """Thayer 3056 and λόγος should return same entry (first 200 chars)."""
    by_num = thayer.lookup("3056")
    by_hw = thayer.lookup("λόγος")
    assert by_num is not None
    assert by_hw is not None
    # Strip from the module title down to compare just the entry body
    # (the citation header may differ between number and headword lookup)
    # Use first-200 on the tail portion
    assert by_num[:200] == by_hw[:200], (
        f"Thayer 3056 and λόγος differ in first 200 chars.\n"
        f"3056  : {by_num[:200]!r}\n"
        f"λόγος: {by_hw[:200]!r}"
    )


def test_thayer_headword_decodes_to_greek(thayer):
    """Thayer output contains proper Greek Unicode, not leftover beta-code."""
    result = thayer.lookup("3056")
    assert result is not None
    # Check that we have Greek characters in the output
    greek_chars = 0
    for ch in result:
        cp = ord(ch)
        if 0x0370 <= cp <= 0x03FF or 0x1F00 <= cp <= 0x1FFF:
            greek_chars += 1
    assert greek_chars > 0, (
        "No Greek Unicode found in Thayer 3056 entry — beta-code may be left undecoded"
    )
    # Specifically check that raw beta-code patterns (like 'lo/go' or 'lo/go§')
    # are NOT present in the output
    assert "lo/go" not in result, (
        "Raw beta-code 'lo/go' found in Thayer output — "
        "should have been decoded to Greek"
    )


# =========================================================================
# Category 6: find() correctness (A2-8)
# =========================================================================


def test_bdag_find_max_results_cap(bdag):
    """BDAG find('λόγ', max_results=5) returns ≤5 results."""
    results = bdag.find("λόγ", max_results=5)
    assert len(results) <= 5, f"Expected ≤5 results, got {len(results)}"


def test_bdag_find_includes_logos(bdag):
    """BDAG find('λόγ') must include 'λόγος' in results."""
    results = bdag.find("λόγ", max_results=100)
    # The headword key may be normalized (without diacritics), so check
    # for the normalized form: λογοσ or λόγος or λογος
    found = any("λογ" in hw.lower() for hw in results)
    assert found, (
        f"find('λόγ') did not include expected entries; first 5 results: {results[:5]}"
    )


def test_bdag_find_results_are_strings(bdag):
    """BDAG find() results must all be non-empty strings."""
    results = bdag.find("α", max_results=20)
    for hw in results:
        assert isinstance(hw, str), f"Non-string in find() results: {hw!r}"
        assert len(hw) > 0, "Empty string in find() results"


def test_halot_find_results_are_strings(halot):
    """HALOT find() results must all be non-empty strings."""
    results = halot.find("א", max_results=20)
    for hw in results:
        assert isinstance(hw, str), f"Non-string in find() results: {hw!r}"
        assert len(hw) > 0, "Empty string in find() results"


def test_bdb_find_h_prefix(bdb):
    """BDB find('H43') returns results (Strong's prefix search)."""
    results = bdb.find("H43", max_results=10)
    assert len(results) > 0, "BDB find('H43') returned no results"


def test_thayer_find_partial_greek(thayer):
    """Thayer find('θε') returns results without crashing."""
    results = thayer.find("θε", max_results=20)
    assert len(results) > 0, "Thayer find('θε') returned no results"


# =========================================================================
# Category 7: Branch-level comparison/boolean mutation coverage
#    Targets: lookup() dispatch chain, _read_entry format dispatch,
#            find() language-specific normalization,
#            BDB number-format fallback branches
# =========================================================================

# ── BDB number format fallback branches (lookup lines 2906-2928) ──


def test_bdb_lookup_h_prefix_with_padding(bdb):
    """BDB lookup('H430') returns full entry text."""
    result = bdb.lookup("H430")
    assert result is not None, "BDB lookup('H430') returned None"
    assert len(result) >= 9000, f"H430 entry too short: {len(result)} chars"


def test_bdb_lookup_numeric_only(bdb):
    """BDB lookup('430') exercises the bare-numeric fallback at lookup line 2920
    (numeric input without H-prefix)."""
    result = bdb.lookup("430")
    assert result is not None, "BDB lookup('430') returned None"
    assert len(result) >= 9000, f"BDB numeric 430 entry too short: {len(result)}"


def test_bdb_lookup_h_prefix_bare_digits(bdb):
    """BDB lookup('H1254') (create/OT) exercises the H-prefix branch at
    lookup line 2910 — headword.startswith('H') guard."""
    result = bdb.lookup("H1254")
    assert result is not None, "BDB lookup('H1254') returned None"
    assert len(result) > 100, f"H1254 entry too short: {len(result)} chars"


def test_bdb_lookup_nonexistent_number_no_crash(bdb):
    """BDB lookup of a non-existent Strong's number should return None
    without raising an exception.  Exercises the `except ValueError` fallback."""
    assert bdb.lookup("H99999") is None, "Non-existent H99999 should be None"


def test_bdb_lookup_alpha_string_not_number(bdb):
    """BDB lookup of non-numeric text exercises the try/except ValueError
    at lookup lines 2917/2926 (int() conversion fails)."""
    result = bdb.lookup("elohim")  # Hebrew word, not a number
    # May or may not find — but must not crash
    assert result is None or isinstance(result, str)


# ── Numbered format lookup dispatch (lookup lines 2930-2933) ──


def test_thayer_lookup_numbered_by_number(thayer):
    """Thayer lookup('3056') triggers the `if self._is_numbered` branch
    at lookup line 2930."""
    result = thayer.lookup("3056")
    assert result is not None, "Thayer lookup('3056') should return λόγος entry"


def test_thayer_lookup_numbered_nonexistent(thayer):
    """Thayer lookup of a non-existent number exercises the None return
    from `_read_entries_by_key` in the numbered branch."""
    assert thayer.lookup("99999") is None, "Non-existent Thayer number"


# ── Headword normalization branches (lookup lines 2936-2968) ──


def test_bdag_lookup_final_sigma_stripping(bdag):
    """BDAG lookup with the word ending in sigma exercises the
    final-sigma stripping branch at lookup line 2949-2953
    (`if not self._is_hebrew and not self._is_english`)."""
    # Both λόγος and λογος should resolve to the same entry
    result = bdag.lookup("λόγος")
    assert result is not None, "lookup('λόγος') returned None"


def test_bdag_lookup_prefix_match(bdag):
    """BDAG lookup with a partial prefix that doesn't get exact match
    exercises the prefix-match walk at lookup line 2963-2968
    (`for k, offsets in self._entries.items()`).
    Use 'λογομα' to hit the prefix-match loop with a unique prefix."""
    result = bdag.lookup("λογομα")
    assert result is not None, "Prefix lookup 'λογομα' should match λογομαχέω"
    assert len(result) > 0


def test_bdb_lookup_prefix_match(bdb):
    """BDB lookup with a partial Strong's prefix exercises the prefix
    match for Hebrew entries."""
    result = bdb.lookup("H43")  # prefix, not full number
    assert result is not None, (
        "BDB lookup('H43') prefix should return at least one entry"
    )


# ── find() branch coverage (find lines 3458-3483) ──


def test_find_max_results_zero_returns_empty(bdag):
    """max_results <= 0 should return empty list (line 3460-3461 guard)."""
    assert bdag.find("α", max_results=0) == [], (
        "find(..., max_results=0) should return []"
    )
    assert bdag.find("α", max_results=-1) == [], (
        "find(..., max_results=-1) should return []"
    )


def test_find_hebrew_substring(halot):
    """HALOT find with Hebrew substring exercises the `if self._is_hebrew`
    normalization path at find line 3463-3464."""
    results = halot.find("א", max_results=10)
    assert isinstance(results, list)
    assert len(results) > 0, "HALOT find('א') should return results"


def test_find_greek_substring_partial(bdag):
    """BDAG find with partial Greek substring exercises final-sigma
    stripping at find line 3471 (`normalized.rstrip`)."""
    results = bdag.find("λογ", max_results=10)
    assert len(results) >= 1, (
        f"find('λογ') should return at least 1 result, got {results}"
    )


# ── _read_entry format dispatch branches ──


def test_read_entry_standard_format(bdag):
    """Standard format read: exercises the final `else` branch of
    _read_entry (line 3174+)."""
    # offset of λόγος headword entry
    offsets = bdag._entries.get(normalize_entry_key("λογος"), [])
    if offsets:
        text = bdag._read_entry(offsets[0])
        assert isinstance(text, str) and len(text) > 0, (
            "Standard _read_entry should return non-empty text"
        )


def test_read_entry_hebrew_format(bdb):
    """BDB entry read: exercises Hebrew decode path in _read_entry."""
    offsets = bdb._entries.get("h430", [])
    if not offsets:
        offsets = bdb._entries.get("H430", [])
    if offsets:
        text = bdb._read_entry(offsets[0])
        assert isinstance(text, str) and len(text) > 0, (
            "BDB _read_entry should return non-empty text"
        )


def normalize_entry_key(key):
    """Helper to normalize a Greek key for _entries lookup."""
    from accordance_parser.encoding import normalize_headword

    return normalize_headword(key)
