"""Session 76 — Encoding integrity tests: Greek/Hebrew encoding, module
discovery, canon data integrity, and cross-module consistency in Catholic
contexts.

12 scenarios:
  S1  — Iota Subscript Stacked on Rough+Acute Breathing
  S2  — Final Sigma Rendering in Catholic Greek Lexicon Entries
  S3  — Dagesh Forte vs Dagesh Lene in BDB
  S4  — Hebrew Final Forms in Word Boundaries
  S5  — Form Marker Stripping in H834 (אשר)
  S6  — Module Discovery Completeness
  S7  — Catholic Canon Verse Totals (73 Books, ~35,606 Verses)
  S8  — Deuterocanonical Chapter Counts
  S9  — FTS5 Language Detection Correctness
  S10 — OSHB Hebrew Morphology for Miserere (Psalm 51)
  S11 — Strong's H430 Cross-Module Consistency
  S12 — Strong's G3056 (λόγος) Cross-Module Consistency
"""

from __future__ import annotations

import pytest
from pathlib import Path

# =============================================================================
# IMPORT GUARDS
# =============================================================================

# --- Encoding ---
try:
    from accordance_parser.encoding import (
        decode_always,
        decode_hebrew_always,
        _find_hebrew_spans,
        _HEBREW_FORM_MARKERS,
        _decode_hebrew_raw,
    )
    _has_encoding = True
except Exception:
    _has_encoding = False

# --- Canon ---
try:
    from accordance_parser.canon import (
        CANON_CATHOLIC,
        CANON_PROTESTANT,
        chapter_count,
    )
    _has_canon = True
except Exception:
    _has_canon = False

# --- Discovery ---
try:
    from accordance_parser.discovery import discover_modules
    _has_discovery = True
except Exception:
    _has_discovery = False

# --- Lexicon ---
try:
    from accordance_parser.lexicon_module import LexiconModule
    _has_lexicon = True
except Exception:
    _has_lexicon = False

# --- Morphology ---
try:
    from accordance_parser.morphology import describe_hebrew_morph
    _has_morphology = True
except Exception:
    _has_morphology = False

# --- Search index ---
try:
    from accordance_parser.search_index import (
        SearchIndex,
        detect_language,
    )
    _has_search_index = True
except Exception:
    _has_search_index = False


# =============================================================================
# Helper: load a specific lexicon module by abbreviation
# =============================================================================

_MODULE_CACHE: dict[str, LexiconModule | None] = {}


def _load_module(abbrev: str) -> LexiconModule | None:
    """Load a LexiconModule by its discovery abbreviation, with caching."""
    if abbrev in _MODULE_CACHE:
        return _MODULE_CACHE[abbrev]
    if not _has_discovery or not _has_lexicon:
        _MODULE_CACHE[abbrev] = None
        return None
    mods = discover_modules()
    info = mods.get(abbrev)
    if not info:
        _MODULE_CACHE[abbrev] = None
        return None
    try:
        lex = LexiconModule(info["path"])
        _MODULE_CACHE[abbrev] = lex
        return lex
    except Exception:
        _MODULE_CACHE[abbrev] = None
        return None


def _require_module(abbrev: str) -> LexiconModule:
    """Return lexicon module or skip the test."""
    lex = _load_module(abbrev)
    if lex is None:
        pytest.skip(f"Module '{abbrev}' not available")
    return lex


# =============================================================================
# S1 — Iota Subscript Stacked on Rough+Acute Breathing
# =============================================================================

@pytest.mark.skipif(not _has_encoding, reason="Encoding not available")
class TestS1IotaSubscriptBreathing:
    """0xF6 on α/η/ω with rough+acute (0xE2) produces multi-code-point
    output including combining iota subscript (U+0345)."""

    def test_alpha_rough_acute_iota_subscript(self):
        """a + 0xE2 (rough+acute) + 0xF6 (iota subscript) → U+0345 present."""
        result = decode_always(b"a\xe2\xf6")
        assert len(result) >= 2, (
            f"Expected ≥2 code points, got {len(result)}: {result!r}"
        )
        assert "\u0345" in result, (
            f"Missing iota subscript U+0345 in {result!r}"
        )

    def test_eta_rough_acute_iota_subscript(self):
        """h + 0xE2 + 0xF6 → eta with rough+acute+iota subscript."""
        result = decode_always(b"h\xe2\xf6")
        assert len(result) >= 2, (
            f"Expected ≥2 code points, got {len(result)}: {result!r}"
        )
        assert "\u0345" in result, (
            f"Missing iota subscript U+0345 in {result!r}"
        )

    def test_omega_rough_acute_iota_subscript(self):
        """w + 0xE2 + 0xF6 → omega with rough+acute+iota subscript."""
        result = decode_always(b"w\xe2\xf6")
        assert len(result) >= 2, (
            f"Expected ≥2 code points, got {len(result)}: {result!r}"
        )
        assert "\u0345" in result, (
            f"Missing iota subscript U+0345 in {result!r}"
        )

    def test_rough_acute_without_iota_subscript(self):
        """a + 0xE2 (rough+acute, NO iota) does NOT contain U+0345."""
        result = decode_always(b"a\xe2")
        assert "\u0345" not in result, (
            f"Unexpected iota subscript in {result!r} — 0xF6 is sole source"
        )
        # Should still have some Greek character(s)
        assert len(result) >= 1


# =============================================================================
# S2 — Final Sigma Rendering in Catholic Greek Lexicon Entries
# =============================================================================

@pytest.mark.skipif(not _has_lexicon, reason="Lexicon not available")
class TestS2FinalSigmaLexicon:
    """BDAG entries must have final sigma (U+03C2) and no raw § (U+00A7)."""

    def test_christos_final_sigma(self):
        """Lookup χριστός in BDAG — final sigma present, § absent."""
        lex = _require_module("BDAG")
        result = lex.lookup("\u03c7\u03c1\u03b9\u03c3\u03c4\u03cc\u03c2")  # χριστός
        assert result is not None, "χριστός not found in BDAG"
        assert "\u03c2" in result, "Missing final sigma (U+03C2) in χριστός entry"
        assert "\u00a7" not in result, (
            "Raw § (U+00A7) leaked into χριστός entry"
        )

    def test_christos_medial_sigma(self):
        """Medial sigma (U+03C3) also appears in χριστός body."""
        lex = _require_module("BDAG")
        result = lex.lookup("\u03c7\u03c1\u03b9\u03c3\u03c4\u03cc\u03c2")
        assert result is not None
        assert "\u03c3" in result, "Missing medial sigma (U+03C3) in χριστός entry"

    def test_theos_final_sigma_sanitized(self):
        """Lookup θεός in BDAG — no raw §, final sigma present."""
        lex = _require_module("BDAG")
        result = lex.lookup("\u03b8\u03b5\u03cc\u03c2")  # θεός
        assert result is not None, "θεός not found in BDAG"
        assert "\u00a7" not in result, "Raw § leaked into θεός entry"
        assert "\u03c2" in result, "Missing final sigma in θεός entry"

    def test_theos_medial_sigma(self):
        """Medial sigma (U+03C3) appears in θεός body."""
        lex = _require_module("BDAG")
        result = lex.lookup("\u03b8\u03b5\u03cc\u03c2")
        assert result is not None
        assert "\u03c3" in result, "Missing medial sigma (U+03C3) in θεός entry"


# =============================================================================
# S3 — Dagesh Forte vs Dagesh Lene in BDB
# =============================================================================

@pytest.mark.skipif(not _has_encoding, reason="Encoding not available")
class TestS3DageshEncoding:
    """Bet/kaf/pe + 0xC9 → dagesh (U+05BC)."""

    def test_bet_dagesh_raw(self):
        """b + 0xC9 → bet + dagesh (U+05D1 U+05BC)."""
        result = decode_hebrew_always(b"b\xc9")
        assert "\u05d1" in result, f"Missing bet in {result!r}"
        assert "\u05bc" in result, f"Missing dagesh in {result!r}"

    def test_kaf_dagesh_raw(self):
        """k + 0xC9 → kaf + dagesh."""
        result = decode_hebrew_always(b"k\xc9")
        assert "\u05db" in result, f"Missing kaf in {result!r}"
        assert "\u05bc" in result, f"Missing dagesh in {result!r}"

    def test_pe_dagesh_raw(self):
        """p + 0xC9 → pe + dagesh."""
        result = decode_hebrew_always(b"p\xc9")
        assert "\u05e4" in result, f"Missing pe in {result!r}"
        assert "\u05bc" in result, f"Missing dagesh in {result!r}"


@pytest.mark.skipif(not _has_lexicon, reason="Lexicon not available")
class TestS3DageshLexicon:
    """Dagesh present in BDB entries for bless (H1288) and curse (H7043)."""

    def test_bdb_bless_has_dagesh(self):
        """H1288 (bless) — Hebrew text with dagesh in BDB entry."""
        lex = _require_module("BDB Complete")
        result = lex.lookup("H1288")
        assert result is not None, "H1288 not found in BDB"
        assert "\u05bc" in result, (
            "No dagesh (U+05BC) found in H1288 entry"
        )

    def test_bdb_curse_has_dagesh(self):
        """H7043 (curse) — contrasting root with dagesh in BDB entry."""
        lex = _require_module("BDB Complete")
        result = lex.lookup("H7043")
        assert result is not None, "H7043 not found in BDB"
        assert "\u05bc" in result, (
            "No dagesh (U+05BC) found in H7043 entry"
        )


# =============================================================================
# S4 — Hebrew Final Forms in Word Boundaries
# =============================================================================

@pytest.mark.skipif(not _has_encoding, reason="Encoding not available")
class TestS4FinalFormsEncoding:
    """Uppercase K/M/N/P/C decode to final forms; lowercase to medial."""

    def test_final_kaf(self):
        assert _decode_hebrew_raw(b"K") == "\u05da", "K → final kaf"

    def test_final_mem(self):
        assert _decode_hebrew_raw(b"M") == "\u05dd", "M → final mem"

    def test_final_nun(self):
        assert _decode_hebrew_raw(b"N") == "\u05df", "N → final nun"

    def test_final_pe(self):
        assert _decode_hebrew_raw(b"P") == "\u05e3", "P → final pe"

    def test_final_tsadi(self):
        assert _decode_hebrew_raw(b"C") == "\u05e5", "C → final tsadi"

    def test_lowercase_k_is_medial_not_final(self):
        """Lowercase k (0x6B) → medial kaf (U+05DB), NOT final (U+05DA)."""
        result = _decode_hebrew_raw(b"k")
        assert result == "\u05db", (
            f"Lowercase k should be medial kaf (U+05DB), got {result!r}"
        )
        assert result != "\u05da", (
            "Lowercase k must NOT be final kaf (U+05DA)"
        )


@pytest.mark.skipif(not _has_lexicon, reason="Lexicon not available")
class TestS4FinalFormsLexicon:
    """BDB headword for melek (king, H4428) contains final kaf."""

    def test_melek_headword_has_final_kaf(self):
        """H4428 (king) — headword contains final kaf (U+05DA)."""
        lex = _require_module("BDB Complete")
        result = lex.lookup("H4428")
        assert result is not None, "H4428 not found in BDB"
        # The headword should contain final kaf at word end
        # Look in first 200 chars for the Hebrew headword
        assert "\u05da" in result, (
            "Final kaf (U+05DA) missing from H4428 entry"
        )


# =============================================================================
# S5 — Form Marker Stripping in H834 (אשר)
# =============================================================================

@pytest.mark.skipif(not _has_encoding, reason="Encoding not available")
class TestS5FormMarkerStripping:
    """Form markers D/F/J/Q/R/S/T/V/X/Z are stripped during Hebrew decode."""

    def test_asher_decodes_to_aleph_shin_resh(self):
        """aSvRr → aleph + shin + resh (form markers S and R stripped)."""
        result = _decode_hebrew_raw(b"aSvRr")
        assert "\u05d0" in result, f"Missing aleph in {result!r}"
        assert "\u05e9" in result, f"Missing shin in {result!r}"
        assert "\u05e8" in result, f"Missing resh in {result!r}"

    def test_no_uppercase_form_markers_in_result(self):
        """Decoded result must NOT contain raw uppercase S or R."""
        result = _decode_hebrew_raw(b"aSvRr")
        assert "S" not in result, f"Form marker 'S' leaked into output: {result!r}"
        assert "R" not in result, f"Form marker 'R' leaked into output: {result!r}"

    def test_find_hebrew_spans_detects_form_marker_word(self):
        """_find_hebrew_spans returns spans for form-marker Hebrew 'aSvRr'."""
        spans = _find_hebrew_spans(b"aSvRr")
        assert len(spans) >= 1, (
            f"_find_hebrew_spans should detect form-marker word, got: {spans}"
        )

    def test_all_form_markers_are_uppercase_ascii(self):
        """All form markers are uppercase ASCII letters."""
        for fm in _HEBREW_FORM_MARKERS:
            assert 0x41 <= fm <= 0x5A, (
                f"Form marker 0x{fm:02X} ({chr(fm)!r}) not uppercase ASCII"
            )

    def test_pure_form_markers_return_empty(self):
        """All form-marker bytes (no consonants) → empty string, no crash."""
        result = _decode_hebrew_raw(b"SRFT")
        assert isinstance(result, str), "Should return a string"
        assert result == "", (
            f"Pure form markers should decode to empty, got {result!r}"
        )


# =============================================================================
# S6 — Module Discovery Completeness
# =============================================================================

@pytest.mark.skipif(not _has_discovery, reason="Discovery not available")
class TestS6ModuleDiscovery:
    """discover_modules() returns ≥80 total modules with expected properties."""

    def test_total_module_count(self):
        """At least 80 total modules discovered."""
        mods = discover_modules()
        assert len(mods) >= 80, (
            f"Expected ≥80 modules, got {len(mods)}"
        )

    def test_text_module_count(self):
        """At least 1 Text module."""
        mods = discover_modules()
        text_count = sum(1 for m in mods.values() if m.get("kind") == "Text")
        assert text_count >= 1, f"Expected ≥1 Text module, got {text_count}"

    def test_tool_module_count(self):
        """At least 30 Tool modules."""
        mods = discover_modules()
        tool_count = sum(1 for m in mods.values() if m.get("kind") == "Tool")
        assert tool_count >= 30, f"Expected ≥30 Tool modules, got {tool_count}"

    def test_language_coverage(self):
        """el, he, en all present in module languages."""
        mods = discover_modules()
        langs: set[str] = set()
        for m in mods.values():
            lang = m.get("language", "")
            if lang:
                langs.add(lang)
        for expected in ("el", "he", "en"):
            assert expected in langs, (
                f"Language '{expected}' not found in module languages: {sorted(langs)}"
            )

    def test_all_abbreviations_unique(self):
        """No duplicate abbreviations across modules."""
        mods = discover_modules()
        abbrs = [m.get("abbr", "") for m in mods.values()]
        seen: set[str] = set()
        dups: list[str] = []
        for a in abbrs:
            if a in seen:
                dups.append(a)
            seen.add(a)
        assert not dups, f"Duplicate abbreviations: {dups}"

    def test_all_paths_exist_and_nonzero(self):
        """Every module's file path exists on disk with non-zero size."""
        mods = discover_modules()
        problems = []
        for abbr, m in mods.items():
            p = Path(m.get("path", ""))
            if not p.exists():
                problems.append(f"{abbr}: path {p} does not exist")
                continue
            if p.stat().st_size == 0:
                problems.append(f"{abbr}: zero-byte file {p}")
        assert not problems, f"Path issues: {problems}"


# =============================================================================
# S7 — Catholic Canon Verse Totals (73 Books, ~35,606 Verses)
# =============================================================================

@pytest.mark.skipif(not _has_canon, reason="Canon not available")
class TestS7CanonVerseTotals:
    """Canon book counts and approximate verse totals."""

    def test_catholic_73_books(self):
        """Catholic canon has 73 books."""
        assert len(CANON_CATHOLIC.books) == 73, (
            f"Catholic canon: {len(CANON_CATHOLIC.books)} books, expected 73"
        )

    def test_protestant_66_books(self):
        """Protestant canon has 66 books."""
        assert len(CANON_PROTESTANT.books) == 66, (
            f"Protestant canon: {len(CANON_PROTESTANT.books)} books, expected 66"
        )

    def test_catholic_verse_total(self):
        """Catholic canon has 35,000-36,500 total verses."""
        total = 0
        for book in CANON_CATHOLIC.books:
            verses = CANON_CATHOLIC.last_verse.get(book.osis, ())
            total += sum(verses)
        assert 35000 <= total <= 36500, (
            f"Catholic verse total: {total}, expected 35,000-36,500"
        )

    def test_protestant_verse_total(self):
        """Protestant canon has 31,000-32,000 total verses."""
        total = 0
        for book in CANON_PROTESTANT.books:
            verses = CANON_PROTESTANT.last_verse.get(book.osis, ())
            total += sum(verses)
        assert 31000 <= total <= 32000, (
            f"Protestant verse total: {total}, expected 31,000-32,000"
        )

    def test_deuterocanonical_verse_total(self):
        """Deuterocanonical-only books have 3,500-5,000 verses."""
        cath_osis = {b.osis for b in CANON_CATHOLIC.books}
        prot_osis = {b.osis for b in CANON_PROTESTANT.books}
        deutero_osis = cath_osis - prot_osis

        total = 0
        for osis in deutero_osis:
            verses = CANON_CATHOLIC.last_verse.get(osis, ())
            total += sum(verses)
        assert 3500 <= total <= 5000, (
            f"Deuterocanonical verse total: {total}, expected 3,500-5,000"
        )

    def test_catholic_daniel_14_chapters(self):
        """Catholic Daniel has 14 chapters vs Protestant 12."""
        cath_ch = chapter_count("Dan", CANON_CATHOLIC)
        prot_ch = chapter_count("Dan", CANON_PROTESTANT)
        assert cath_ch == 14, f"Catholic Daniel chapters: {cath_ch}, expected 14"
        assert prot_ch == 12, f"Protestant Daniel chapters: {prot_ch}, expected 12"


# =============================================================================
# S8 — Deuterocanonical Chapter Counts
# =============================================================================

@pytest.mark.skipif(not _has_canon, reason="Canon not available")
class TestS8DeuterocanonicalChapters:
    """Deuterocanonical book chapter counts and Protestant exclusion."""

    _DEUTERO_CHAPTERS = {
        "Tob": 14, "Jdt": 16, "Wis": 19, "Sir": 51,
        "Bar": 6, "1Macc": 16, "2Macc": 15,
    }

    def test_catholic_chapter_counts(self):
        """Each deuterocanonical book has expected chapter count in Catholic canon."""
        for osis, expected in self._DEUTERO_CHAPTERS.items():
            actual = chapter_count(osis, CANON_CATHOLIC)
            assert actual == expected, (
                f"Catholic {osis} chapters: {actual}, expected {expected}"
            )

    def test_protestant_chapter_counts_zero(self):
        """Deuterocanonical books have 0 chapters in Protestant canon."""
        for osis in self._DEUTERO_CHAPTERS:
            actual = chapter_count(osis, CANON_PROTESTANT)
            assert actual == 0, (
                f"Protestant {osis} chapters: {actual}, expected 0"
            )

    def test_sirach_longest_deutero(self):
        """Sirach is the longest deuterocanonical book at 51 chapters."""
        max_ch = max(self._DEUTERO_CHAPTERS.values())
        assert self._DEUTERO_CHAPTERS["Sir"] == max_ch == 51, (
            f"Sirach should be longest deuterocanonical at 51 chapters, max={max_ch}"
        )

    def test_deuterocanonical_books_in_catholic_canon(self):
        """All 7 deuterocanonical books are in the Catholic canon."""
        cath_osis = {b.osis for b in CANON_CATHOLIC.books}
        for osis in self._DEUTERO_CHAPTERS:
            assert osis in cath_osis, (
                f"'{osis}' missing from Catholic canon: {sorted(cath_osis)}"
            )

    def test_deuterocanonical_books_not_in_protestant_canon(self):
        """Deuterocanonical books are NOT in the Protestant canon."""
        prot_osis = {b.osis for b in CANON_PROTESTANT.books}
        for osis in self._DEUTERO_CHAPTERS:
            assert osis not in prot_osis, (
                f"'{osis}' should NOT be in Protestant canon"
            )


# =============================================================================
# S9 — FTS5 Language Detection Correctness
# =============================================================================

@pytest.mark.skipif(not _has_search_index, reason="Search index not available")
class TestS9LanguageDetection:
    """detect_language returns correct language codes."""

    def test_greek_detection(self):
        """άγιος → 'el'."""
        assert detect_language("\u03ac\u03b3\u03b9\u03bf\u03c2") == "el", (
            "άγιος should be detected as Greek (el)"
        )

    def test_hebrew_detection(self):
        """בראשית → 'he'."""
        assert detect_language("\u05d1\u05e8\u05d0\u05e9\u05d9\u05ea") == "he", (
            "בראשית should be detected as Hebrew (he)"
        )

    def test_english_detection(self):
        """justification → 'en'."""
        assert detect_language("justification") == "en", (
            "justification should be detected as English (en)"
        )

    def test_empty_string_english(self):
        """Empty string → 'en'."""
        assert detect_language("") == "en"

    def test_mixed_english_default(self):
        """Mixed ASCII → 'en'."""
        assert detect_language("John 3:16") == "en"


@pytest.mark.skipif(not _has_search_index, reason="Search index not available")
class TestS9SearchIndexMethods:
    """SearchIndex has expected methods and handles graceful degradation."""

    def test_search_methods_exist(self):
        """search(), search_lexicons(), search_transliterated() are callable."""
        idx = SearchIndex()
        try:
            assert callable(idx.search), "search() not callable"
            assert callable(idx.search_lexicons), "search_lexicons() not callable"
            assert callable(idx.search_transliterated), "search_transliterated() not callable"
        finally:
            idx.close()

    def test_search_transliterated_no_crash(self):
        """search_transliterated('logos', 'lexicon', 'el', 5) returns list."""
        idx = SearchIndex()
        try:
            results = idx.search_transliterated("logos", "lexicon", "el", 5)
            assert isinstance(results, list), (
                f"Expected list, got {type(results).__name__}"
            )
        except Exception:
            # Empty/unpopulated index may produce empty results — acceptable
            pass
        finally:
            idx.close()


# =============================================================================
# S10 — OSHB Hebrew Morphology for Miserere (Psalm 51)
# =============================================================================

@pytest.mark.skipif(not _has_morphology, reason="Morphology not available")
class TestS10HebrewMorphology:
    """describe_hebrew_morph returns correct human-readable descriptions."""

    def test_qal_imperative(self):
        """HVqv2ms → contains 'Qal' and 'Imperative'."""
        desc = describe_hebrew_morph("HVqv2ms")
        assert isinstance(desc, str) and len(desc) > 0
        assert "Qal" in desc, f"Expected 'Qal' in {desc!r}"
        assert "Imperative" in desc, f"Expected 'Imperative' in {desc!r}"

    def test_qal_perfect(self):
        """HVqp3ms → contains 'Qal' and 'Perfect'."""
        desc = describe_hebrew_morph("HVqp3ms")
        assert isinstance(desc, str) and len(desc) > 0
        assert "Qal" in desc, f"Expected 'Qal' in {desc!r}"
        assert "Perfect" in desc, f"Expected 'Perfect' in {desc!r}"

    def test_noun_masculine_plural(self):
        """HNcmpa → contains 'Noun' and masculine/plural indicators."""
        desc = describe_hebrew_morph("HNcmpa")
        assert isinstance(desc, str) and len(desc) > 0
        assert "Noun" in desc, f"Expected 'Noun' in {desc!r}"
        assert ("masculine" in desc.lower() or "plural" in desc.lower()), (
            f"Expected 'masculine' or 'plural' in {desc!r}"
        )

    def test_qal_imperfect(self):
        """HVqi3ms → contains 'Imperfect'."""
        desc = describe_hebrew_morph("HVqi3ms")
        assert isinstance(desc, str) and len(desc) > 0
        assert "Imperfect" in desc, f"Expected 'Imperfect' in {desc!r}"

    def test_empty_code_returns_string(self):
        """describe_hebrew_morph('') returns a string, not None."""
        desc = describe_hebrew_morph("")
        assert isinstance(desc, str), (
            f"Expected str for empty code, got {type(desc).__name__}: {desc!r}"
        )
        assert desc is not None


# =============================================================================
# S11 — Strong's H430 Cross-Module Consistency
# =============================================================================

@pytest.mark.skipif(not _has_lexicon, reason="Lexicon not available")
class TestS11StrongsH430CrossModule:
    """H430 (elohim/God) returns consistent God-related content across modules."""

    def test_bdb_h430_is_about_god(self):
        """BDB H430 contains 'God' or 'god' or 'divine'."""
        lex = _require_module("BDB Complete")
        result = lex.lookup("H430")
        assert result is not None, "H430 not found in BDB"
        lower = result.lower()
        assert any(w in lower for w in ("god", "divine")), (
            f"H430 BDB entry should be about God, got: {result[:200]!r}"
        )

    def test_nas_hebrew_h430_hebrew_unicode(self):
        """NAS Hebrew H430 returns ≥50 chars with Hebrew Unicode."""
        lex = _require_module("NAS Hebrew")
        result = lex.lookup("H430")
        if result is None:
            # Try numbered format
            result = lex.lookup("430")
        if result is None:
            pytest.skip("H430 not found in NAS Hebrew (either format)")
        assert len(result) >= 50, f"NAS Hebrew H430 too short: {len(result)} chars"
        has_hebrew = any("\u0590" <= c <= "\u05ff" for c in result)
        assert has_hebrew, "NAS Hebrew H430 should contain Hebrew Unicode"

    def test_halot_elohim_non_none(self):
        """HALOT lookup of אל (god root, H410) returns non-None.
        
        HALOT organizes by Hebrew root; elohim (אלהים) is a derived plural
        form under the אל root and does not have its own entry.
        """
        lex = _require_module("HALOT")
        # Try the root אל which IS in HALOT
        result = lex.lookup("\u05d0\u05dc")  # אל
        assert result is not None, "אל not found in HALOT"
        # Should be about God
        assert len(result) > 0, "HALOT אל entry is empty"

    def test_three_sources_pairwise_different(self):
        """BDB, NAS Hebrew, and HALOT texts for God are pairwise different."""
        bdb = _require_module("BDB Complete")
        nas = _require_module("NAS Hebrew")
        halot = _require_module("HALOT")

        bdb_text = bdb.lookup("H430")
        nas_text = nas.lookup("H430") or nas.lookup("430")
        halot_text = halot.lookup("\u05d0\u05dc")  # אל (god root)

        if bdb_text is None or nas_text is None or halot_text is None:
            pytest.skip("One or more H430/אל lookups returned None")

        assert bdb_text != nas_text, "BDB and NAS Hebrew should differ"
        assert bdb_text != halot_text, "BDB and HALOT should differ"
        assert nas_text != halot_text, "NAS Hebrew and HALOT should differ"

    def test_hebrew_strongs_5650_non_empty(self):
        """Hebrew Strong\u2019s lookup of '5650' returns non-empty."""
        lex = _require_module("Hebrew Strong\u2019s")
        result = lex.lookup("5650")
        if result is None:
            pytest.skip("5650 not found in Hebrew Strong\u2019s")
        assert len(result) > 0, "Hebrew Strong\u2019s 5650 should have content"


# =============================================================================
# S12 — Strong's G3056 (λόγος) Cross-Module Consistency
# =============================================================================

@pytest.mark.skipif(not _has_lexicon, reason="Lexicon not available")
class TestS12StrongsG3056CrossModule:
    """G3056 (λόγος) returns consistent word/speech content across modules."""

    _LOGOS_KEYWORDS = ["word", "speech", "logos", "\u03bb\u03cc\u03b3"]

    def _contains_keyword(self, text: str) -> bool:
        lower = text.lower()
        return any(kw in lower for kw in self._LOGOS_KEYWORDS)

    def test_bdag_logos_is_about_word(self):
        """BDAG λόγος entry starts with 'word' or 'speech'."""
        lex = _require_module("BDAG")
        result = lex.lookup("\u03bb\u03cc\u03b3\u03bf\u03c2")  # λόγος
        assert result is not None, "λόγος not found in BDAG"
        assert self._contains_keyword(result), (
            f"BDAG λόγος should contain word/speech/logos, got: {result[:200]!r}"
        )

    def test_thayer_logos_greek_unicode(self):
        """Thayer λόγος returns ≥100 chars with Greek Unicode."""
        lex = _require_module("Thayer")
        result = lex.lookup("\u03bb\u03cc\u03b3\u03bf\u03c2")
        if result is None:
            # Try numbered format (G3056)
            result = lex.lookup("3056")
        assert result is not None, "λόγος/G3056 not found in Thayer"
        assert len(result) >= 100, f"Thayer λόγος too short: {len(result)} chars"
        has_greek = any("\u0370" <= c <= "\u03ff" for c in result)
        assert has_greek, "Thayer λόγος should contain Greek Unicode"

    def test_nas_greek_logos_non_empty(self):
        """NAS Greek lookups of λόγος or G3056 return non-empty."""
        lex = _require_module("NAS Greek")
        result = lex.lookup("\u03bb\u03cc\u03b3\u03bf\u03c2")
        if result is None:
            result = lex.lookup("G3056")
        if result is None:
            result = lex.lookup("3056")
        assert result is not None, "λόγος/G3056 not found in NAS Greek"
        assert len(result) > 0, "NAS Greek λόγος should have content"

    def test_greek_strongs_3056_unicode_greek(self):
        """Greek Strong\u2019s lookup of '3056' contains Unicode Greek, not Beta Code."""
        lex = _require_module("Greek Strong\u2019s")
        result = lex.lookup("3056")
        assert result is not None, "3056 not found in Greek Strong\u2019s"
        assert len(result) > 0, "Greek Strong\u2019s 3056 should have content"
        # Must have Greek Unicode characters
        has_unicode_greek = any("\u0370" <= c <= "\u03ff" for c in result)
        assert has_unicode_greek, (
            f"Greek Strong\u2019s 3056 should have Unicode Greek, not Beta Code: "
            f"{result[:200]!r}"
        )

    def test_all_four_sources_contain_keyword(self):
        """All 4 modules (BDAG, Thayer, NAS Greek, Greek Strong's) contain a
        λόγος keyword."""
        sources = {
            "BDAG": _require_module("BDAG").lookup("\u03bb\u03cc\u03b3\u03bf\u03c2"),
            "Thayer": _require_module("Thayer").lookup("\u03bb\u03cc\u03b3\u03bf\u03c2")
                       or _require_module("Thayer").lookup("3056"),
            "NAS Greek": _require_module("NAS Greek").lookup("\u03bb\u03cc\u03b3\u03bf\u03c2")
                          or _require_module("NAS Greek").lookup("G3056")
                          or _require_module("NAS Greek").lookup("3056"),
            "Greek Strong\u2019s": _require_module("Greek Strong\u2019s").lookup("3056"),
        }

        failures = []
        for name, text in sources.items():
            if text is None:
                failures.append(f"{name}: lookup returned None")
            elif not self._contains_keyword(text):
                failures.append(
                    f"{name}: missing keyword in: {text[:120]!r}"
                )
        assert not failures, f"λόγος keyword failures: {failures}"
