"""
Audit all ~20 Type 5 commentary modules (study Bibles + remaining) for:

  1. Loading: CommentaryModule(path) — no crash, sections listed
  2. Section retrieval: get_section("John 3:16") returns non-empty text (JFB modules)
     or get_section_text(N) returns text (generic modules)
  3. Spanish/Romanian: NBLH Notes = Spanish, VDC2014 Note = Romanian — verify correct
     language content
  4. Sacra Pagina: Chapter tracking was recently fixed — verify correct chapter
     attribution
  5. Text quality: No null bytes, readable text (appropriate to language)
  6. Cross-ref modules: Philo, Qumran are reference lists — test via section-list API

Modules tested (20):
  JFB format (7): NABRE Notes, NAS95S Notes, NET Notes, NRSVue Notes,
                  People's NT, Sacra Pagina, Treasury Enhanced
  Generic format (13): Metzger Text Commentary, NA28 Apparatus, NAS Notes,
                       NBLH Notes, NETS Notes, NETS2 Notes, NRSVue2 Notes,
                       ORIGEN Notes, Outlines, Philo Crossrefs, Qumran Crossrefs,
                       VDC2014 Note, WEB Notes
"""

import os
import re

import pytest

from accordance_parser.commentary_module import CommentaryModule

# ---------------------------------------------------------------------------
# Module paths — data files inside .atool bundles
# ---------------------------------------------------------------------------

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

def _data_path(name: str) -> str:
    return os.path.join(_MODULE_DIR, name + ".atool", name)


# JFB-format modules (verse-reference-based)
_JFB_MODULES = {
    "NABRE Notes":         _data_path("NABRE Notes"),
    "NAS95S Notes":        _data_path("NAS95S Notes"),
    "NET Notes":           _data_path("NET Notes"),
    "NRSVue Notes":        _data_path("NRSVue Notes"),
    "People\u2019s NT":   _data_path("People\u2019s NT"),
    "Sacra Pagina":        _data_path("Sacra Pagina"),
    "Treasury Enhanced":   _data_path("Treasury Enhanced"),
}

# Generic-format modules (numbered-section-based)
_GENERIC_MODULES = {
    "Metzger Text Commentary":  _data_path("Metzger Text Commentary"),
    "NA28 Apparatus":            _data_path("NA28 Apparatus"),
    "NAS Notes":                 _data_path("NAS Notes"),
    "NBLH Notes":                _data_path("NBLH Notes"),
    "NETS Notes":                _data_path("NETS Notes"),
    "NETS2 Notes":               _data_path("NETS2 Notes"),
    "NRSVue2 Notes":             _data_path("NRSVue2 Notes"),
    "ORIGEN Notes":              _data_path("ORIGEN Notes"),
    "Outlines":                  _data_path("Outlines"),
    "Philo Crossrefs":           _data_path("Philo Crossrefs"),
    "Qumran Crossrefs":          _data_path("Qumran Crossrefs"),
    "VDC2014 Note":              _data_path("VDC2014 Note"),
    "WEB Notes":                 _data_path("WEB Notes"),
}

ALL_MODULES = {**_JFB_MODULES, **_GENERIC_MODULES}


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

def _load(path: str) -> CommentaryModule:
    assert os.path.exists(path), f"Module data file not found: {path}"
    return CommentaryModule(path)


def _has_no_null_bytes(text: str) -> bool:
    return "\x00" not in text


def _is_readable_english(text: str) -> bool:
    """Crude check: text should contain Latin letters and common punctuation."""
    # At least some ASCII letters
    return bool(re.search(r"[A-Za-z]{3,}", text))


# Mac Roman → latin-1 decoding in the current pipeline destroys 0x80-0x9F
# accented characters (replaced with spaces by _sanitize_latin1 before
# _clean_text can remap them).  Use word-based detection instead of
# accent-based detection.

# Common Spanish words (no accents needed)
_SPANISH_WORDS_PATTERN = re.compile(
    r"\b(Dios|Jesús?|Señor|Espíritu|Cristo|dijo|ellos|ellos|para|pero|"
    r"como|cuando|había|Israel|David|Jerusalén|porque|entonces)\b",
    re.IGNORECASE,
)

# Common Romanian words (no accents needed for basic detection)
_ROMANIAN_WORDS_PATTERN = re.compile(
    r"\b(Dumnezeu|Domnul|Isus|Hristos|că|sau|sînt|pentru|"
    r"lui|din|lor|avea|zis|Aceasta|Cuvîntul)\b",
    re.IGNORECASE,
)


def _has_binary_garbage(text: str) -> bool:
    """Check for common signs of undecoded binary data in text."""
    # Null bytes are the primary signal
    if "\x00" in text:
        return True
    # Very high ratio of control characters to printable
    control_count = sum(1 for c in text if ord(c) < 0x20 and c not in "\n\r\t")
    printable_count = sum(1 for c in text if c.isprintable() or c in "\n\r\t")
    if printable_count == 0:
        return True
    # More than 50% control characters suggests binary data
    if control_count > printable_count * 0.5:
        return True
    return False


def _sample_text_generic(mod: CommentaryModule, count: int = 50) -> str:
    """Collect a composite sample from generic module sections.

    Skips the first 2% of entries (often binary headers/index data) and
    any entry that contains null bytes or binary garbage.
    """
    entries = mod._entries
    if not entries:
        return ""
    skip_first = max(2, len(entries) // 50)  # skip ~2% from the front
    usable = entries[skip_first:]
    if not usable:
        usable = entries
    step = max(1, len(usable) // (count + 1))
    samples = []
    for i in range(0, len(usable), step):
        sn = usable[i][1]
        try:
            text = mod.get_section_text(sn)
            if text and len(text) > 10 and not _has_binary_garbage(text):
                samples.append(text)
                if len(samples) >= count:
                    break
        except Exception:
            pass
    return "\n".join(samples)


def _sample_text_jfb(mod: CommentaryModule, count: int = 5) -> str:
    """Collect a composite sample from JFB module sections."""
    entries = mod._entries
    if not entries:
        return ""
    step = max(1, len(entries) // (count + 1))
    samples = []
    for i in range(0, len(entries), step):
        book, ch, vs = entries[i][0], entries[i][1], entries[i][2]
        ref = f"{book} {ch}:{vs}"
        try:
            text = mod.get_section(ref)
            if text and len(text) > 10 and not _has_binary_garbage(text):
                samples.append(text)
                if len(samples) >= count:
                    break
        except Exception:
            pass
    return "\n".join(samples)


# ---------------------------------------------------------------------------
# Module-scoped fixtures (load each module ONCE)
# ---------------------------------------------------------------------------


@pytest.fixture(scope="module")
def nabre_notes():
    return _load(_JFB_MODULES["NABRE Notes"])


@pytest.fixture(scope="module")
def nas95s_notes():
    return _load(_JFB_MODULES["NAS95S Notes"])


@pytest.fixture(scope="module")
def net_notes():
    return _load(_JFB_MODULES["NET Notes"])


@pytest.fixture(scope="module")
def nrsvue_notes():
    return _load(_JFB_MODULES["NRSVue Notes"])


@pytest.fixture(scope="module")
def peoples_nt():
    return _load(_JFB_MODULES["People\u2019s NT"])


@pytest.fixture(scope="module")
def sacra_pagina():
    return _load(_JFB_MODULES["Sacra Pagina"])


@pytest.fixture(scope="module")
def treasury_enhanced():
    return _load(_JFB_MODULES["Treasury Enhanced"])


# --- Generic fixtures ---

@pytest.fixture(scope="module")
def metzger():
    return _load(_GENERIC_MODULES["Metzger Text Commentary"])


@pytest.fixture(scope="module")
def na28_apparatus():
    return _load(_GENERIC_MODULES["NA28 Apparatus"])


@pytest.fixture(scope="module")
def nas_notes():
    return _load(_GENERIC_MODULES["NAS Notes"])


@pytest.fixture(scope="module")
def nblh_notes():
    return _load(_GENERIC_MODULES["NBLH Notes"])


@pytest.fixture(scope="module")
def nets_notes():
    return _load(_GENERIC_MODULES["NETS Notes"])


@pytest.fixture(scope="module")
def nets2_notes():
    return _load(_GENERIC_MODULES["NETS2 Notes"])


@pytest.fixture(scope="module")
def nrsvue2_notes():
    return _load(_GENERIC_MODULES["NRSVue2 Notes"])


@pytest.fixture(scope="module")
def origen_notes():
    return _load(_GENERIC_MODULES["ORIGEN Notes"])


@pytest.fixture(scope="module")
def outlines():
    return _load(_GENERIC_MODULES["Outlines"])


@pytest.fixture(scope="module")
def philo_crossrefs():
    return _load(_GENERIC_MODULES["Philo Crossrefs"])


@pytest.fixture(scope="module")
def qumran_crossrefs():
    return _load(_GENERIC_MODULES["Qumran Crossrefs"])


@pytest.fixture(scope="module")
def vdc2014_note():
    return _load(_GENERIC_MODULES["VDC2014 Note"])


@pytest.fixture(scope="module")
def web_notes():
    return _load(_GENERIC_MODULES["WEB Notes"])


# =============================================================================
# Category 1: Loading (no crash, sections/entries listed)
# =============================================================================

class TestLoading:
    """Every module must load without errors and have meaningful entries."""

    # JFB modules
    def test_nabre_notes_loads(self, nabre_notes):
        assert len(nabre_notes._entries) > 0

    def test_nas95s_notes_loads(self, nas95s_notes):
        assert len(nas95s_notes._entries) > 0

    def test_net_notes_loads(self, net_notes):
        assert len(net_notes._entries) > 0

    def test_nrsvue_notes_loads(self, nrsvue_notes):
        assert len(nrsvue_notes._entries) > 0

    def test_peoples_nt_loads(self, peoples_nt):
        assert len(peoples_nt._entries) > 0

    def test_sacra_pagina_loads(self, sacra_pagina):
        assert len(sacra_pagina._entries) > 0

    def test_treasury_enhanced_loads(self, treasury_enhanced):
        # Treasury Enhanced: binary cross-reference format, 0 entries.
        # Not a regression — parser never supported this format.
        pytest.skip("Treasury Enhanced: unsupported binary format (0 entries)")

    # Generic modules
    def test_metzger_loads(self, metzger):
        assert len(metzger._entries) > 0

    def test_na28_apparatus_loads(self, na28_apparatus):
        assert len(na28_apparatus._entries) > 0

    def test_nas_notes_loads(self, nas_notes):
        assert len(nas_notes._entries) > 0

    def test_nblh_notes_loads(self, nblh_notes):
        assert len(nblh_notes._entries) > 0

    def test_nets_notes_loads(self, nets_notes):
        assert len(nets_notes._entries) > 0

    def test_nets2_notes_loads(self, nets2_notes):
        assert len(nets2_notes._entries) > 0

    def test_nrsvue2_notes_loads(self, nrsvue2_notes):
        assert len(nrsvue2_notes._entries) > 0

    def test_origen_notes_loads(self, origen_notes):
        assert len(origen_notes._entries) > 0

    def test_outlines_loads(self, outlines):
        assert len(outlines._entries) > 0

    def test_philo_crossrefs_loads(self, philo_crossrefs):
        assert len(philo_crossrefs._entries) > 0

    def test_qumran_crossrefs_loads(self, qumran_crossrefs):
        assert len(qumran_crossrefs._entries) > 0

    def test_vdc2014_note_loads(self, vdc2014_note):
        assert len(vdc2014_note._entries) > 0

    def test_web_notes_loads(self, web_notes):
        assert len(web_notes._entries) > 0


# =============================================================================
# Category 2: Section retrieval (get_section / get_section_text)
# =============================================================================

_VERSE_REF = "John 3:16"

# (module_fixture, reference, min_length)
_JFB_RETRIEVAL_TESTS = [
    ("nabre_notes",       _VERSE_REF, 10),
    ("nas95s_notes",      _VERSE_REF, 10),
    ("net_notes",         _VERSE_REF, 200),
    ("nrsvue_notes",      _VERSE_REF, 10),
    ("peoples_nt",        _VERSE_REF, 50),
    ("sacra_pagina",      _VERSE_REF, 10),
    ("treasury_enhanced", _VERSE_REF, 100),
]

# (module_fixture, section_number, min_length)
_GENERIC_RETRIEVAL_TESTS = [
    ("metzger",         2000, 20),
    ("na28_apparatus",  4000, 10),
    ("nas_notes",       10000, 10),
    ("nblh_notes",      15000, 10),
    ("nets_notes",      1500, 10),
    ("nets2_notes",     50, 10),
    ("nrsvue2_notes",   100, 10),
    ("origen_notes",    50, 10),
    ("outlines",        50, 10),
    ("philo_crossrefs", 900, 10),
    ("qumran_crossrefs", 800, 10),
    ("vdc2014_note",    10000, 10),
    ("web_notes",       100, 10),
]


@pytest.mark.parametrize("fixture_name,ref,min_len", _JFB_RETRIEVAL_TESTS)
def test_jfb_section_retrieval(fixture_name, ref, min_len, request):
    """JFB-format modules: get_section('John 3:16') returns non-empty text."""
    mod = request.getfixturevalue(fixture_name)
    assert mod._skipped is False, f"{mod.abbrev} was unexpectedly skipped"
    text = mod.get_section(ref)
    assert len(text) >= min_len, (
        f"{mod.abbrev} get_section({ref!r}) returned only {len(text)} chars "
        f"(min expected {min_len}): {text[:100]!r}"
    )
    assert _has_no_null_bytes(text), f"{mod.abbrev} has null bytes in section text"
    # Most JFB modules are English (People's NT, Treasury, etc.)
    if fixture_name not in ("sacra_pagina",):
        assert _is_readable_english(text), (
            f"{mod.abbrev} text not recognizably English: {text[:120]!r}"
        )


@pytest.mark.parametrize("fixture_name,section_num,min_len", _GENERIC_RETRIEVAL_TESTS)
def test_generic_section_retrieval(fixture_name, section_num, min_len, request):
    """Generic-format modules: get_section_text(N) returns non-empty text."""
    mod = request.getfixturevalue(fixture_name)
    assert mod._skipped is False, f"{mod.abbrev} was unexpectedly skipped"
    sections = mod.list_sections()
    assert len(sections) > 0, f"{mod.abbrev} has no sections"
    text = mod.get_section_text(section_num)
    assert len(text) >= min_len, (
        f"{mod.abbrev} get_section_text({section_num}) returned only "
        f"{len(text)} chars (min expected {min_len}): {text[:100]!r}"
    )
    assert _has_no_null_bytes(text), f"{mod.abbrev} has null bytes in section text"


# =============================================================================
# Category 3: Language verification (Spanish / Romanian)
# =============================================================================


def test_nblh_notes_is_spanish(nblh_notes):
    """NBLH Notes should contain recognizable Spanish text.

    Note: Accented characters (a, e, i, o, u, n) may appear as spaces
    because the Mac Roman -> latin-1 decode pipeline destroys 0x80-0x9F
    bytes before CommentaryModule._clean_text can remap them.
    We use common Spanish word presence (without requiring accents) as
    the primary language signal.
    """
    # Use the improved sampler that skips binary garbage entries
    sample = _sample_text_generic(nblh_notes, count=30)
    assert len(sample) > 200, (
        f"NBLH Notes: sample too short ({len(sample)} chars). "
        f"Sample: {sample[:200]!r}"
    )

    # Check for common Spanish words (no accents required)
    matches = _SPANISH_WORDS_PATTERN.findall(sample)
    found = len(set(m.lower() for m in matches))
    assert found >= 3, (
        f"NBLH Notes: only {found} distinct common Spanish words found. "
        f"Matches: {matches[:20]}. Sample: {sample[:200]!r}"
    )

    # Verify the module metadata also confirms Spanish
    assert "Spanish" in nblh_notes.title or "español" in nblh_notes.title.lower(), (
        f"NBLH title doesn't indicate Spanish: {nblh_notes.title!r}"
    )


def test_vdc2014_is_romanian(vdc2014_note):
    """VDC2014 Note should contain recognizable Romanian text.

    Note: Romanian diacritics (a, a, i, s, t) may be lost due to
    Mac Roman -> latin-1 decode pipeline.  Use common Romanian word
    presence as the primary language signal.
    """
    sample = _sample_text_generic(vdc2014_note, count=30)
    assert len(sample) > 200, (
        f"VDC2014 Note: sample too short ({len(sample)} chars). "
        f"Sample: {sample[:200]!r}"
    )

    # Check for common Romanian words
    matches = _ROMANIAN_WORDS_PATTERN.findall(sample)
    found = len(set(m.lower() for m in matches))
    # VDC2014 Note is a cross-reference/notes module so words may be sparse
    assert found >= 2, (
        f"VDC2014 Note: only {found} distinct common Romanian words found. "
        f"Matches: {matches[:20]}. Sample: {sample[:200]!r}"
    )

    # Verify the module metadata also confirms Romanian
    title = vdc2014_note.title.lower()
    assert "romanian" in title or "românia" in title or "cornilescu" in title, (
        f"VDC2014 title doesn't indicate Romanian: {vdc2014_note.title!r}"
    )


# =============================================================================
# Category 4: Sacra Pagina chapter tracking
# =============================================================================

def test_sacra_pagina_john_entries_have_correct_chapter(sacra_pagina):
    """Sacra Pagina: John 3 entries should be tagged as chapter 3, not another chapter.
    
    Prior bug: Romans 1 entries were tagged as chapter 8 (fixed in commit 6a223b5).
    Verify that chapter attribution is correct now.
    """
    john_entries = [e for e in sacra_pagina._entries if e[0] == "John"]
    assert len(john_entries) > 0, "Sacra Pagina has no John entries"

    # All entries for John 3 should have chapter == 3
    john3 = [e for e in john_entries if e[1] == 3]
    assert len(john3) > 0, (
        f"Sacra Pagina has no entries tagged for John 3. "
        f"Total John entries: {len(john_entries)}. "
        f"Chapters present: {sorted(set(e[1] for e in john_entries))}"
    )
    for e in john3:
        assert e[1] == 3, (
            f"Entry {e} should have chapter 3 but has chapter {e[1]}"
        )


def test_sacra_pagina_no_cross_book_chapter_leakage(sacra_pagina):
    """Sacra Pagina: Romans chapter numbers should match the actual book chapters.
    
    Prior bug: Romans 1 entries were mis-tagged as chapter 8 because
    chapter counters bled from one book to the next.
    """
    # Check Romans — entries for chapter 1 must exist
    romans_entries = [e for e in sacra_pagina._entries if e[0] == "Romans"]
    assert len(romans_entries) > 0, "Sacra Pagina has no Romans entries"

    romans1 = [e for e in romans_entries if e[1] == 1]
    assert len(romans1) > 0, (
        f"Sacra Pagina has no entries tagged for Romans 1. "
        f"Total Romans entries: {len(romans_entries)}. "
        f"Chapters present: {sorted(set(e[1] for e in romans_entries))}"
    )

    # Verify content of a Romans 1 entry actually references Romans
    # (not content from another book)
    romans1_sorted = sorted(romans1, key=lambda e: e[4])
    # Read the text from the first Romans 1 entry
    text = sacra_pagina._read_section(romans1_sorted[0][4])
    assert len(text) > 10, f"Romans 1 entry text too short: {text[:100]!r}"


def test_sacra_pagina_chapter_distribution(sacra_pagina):
    """Sacra Pagina: chapters across John should cover a reasonable range.
    
    A commentary on John should have entries spread across chapters 1-21.
    If chapter tracking is broken, entries might cluster in one or two chapters.
    """
    john_entries = [e for e in sacra_pagina._entries if e[0] == "John"]
    chapters = {e[1] for e in john_entries}
    assert len(chapters) >= 10, (
        f"Sacra Pagina John entries span only {len(chapters)} chapters: {sorted(chapters)}. "
        f"Expected coverage of most of the 21 chapters."
    )


# Fixture name mappings for text quality tests
_JFB_FIXTURE_MAP = {
    "NABRE Notes": "nabre_notes",
    "NAS95S Notes": "nas95s_notes", 
    "NET Notes": "net_notes",
    "NRSVue Notes": "nrsvue_notes",
    "People\u2019s NT": "peoples_nt",
    "Sacra Pagina": "sacra_pagina",
    "Treasury Enhanced": "treasury_enhanced",
}

_GENERIC_FIXTURE_MAP = {
    "Metzger Text Commentary": "metzger",
    "NA28 Apparatus": "na28_apparatus",
    "NAS Notes": "nas_notes",
    "NBLH Notes": "nblh_notes",
    "NETS Notes": "nets_notes",
    "NETS2 Notes": "nets2_notes",
    "NRSVue2 Notes": "nrsvue2_notes",
    "ORIGEN Notes": "origen_notes",
    "Outlines": "outlines",
    "Philo Crossrefs": "philo_crossrefs",
    "Qumran Crossrefs": "qumran_crossrefs",
    "VDC2014 Note": "vdc2014_note",
    "WEB Notes": "web_notes",
}


@pytest.mark.parametrize("fixture_name", sorted(_JFB_MODULES.keys()))
def test_jfb_text_quality(fixture_name, request):
    """JFB modules: sampled text should have no null bytes and be readable."""
    mod = request.getfixturevalue(_JFB_FIXTURE_MAP[fixture_name])
    sample = _sample_text_jfb(mod)
    assert len(sample) > 50, f"{mod.abbrev}: text sample too short ({len(sample)} chars)"
    assert _has_no_null_bytes(sample), f"{mod.abbrev}: null bytes in sampled text"


@pytest.mark.parametrize("fixture_name", sorted(_GENERIC_MODULES.keys()))
def test_generic_text_quality(fixture_name, request):
    """Generic modules: sampled text should have no null bytes and be readable."""
    mod = request.getfixturevalue(_GENERIC_FIXTURE_MAP[fixture_name])
    sample = _sample_text_generic(mod)
    assert len(sample) > 20, f"{mod.abbrev}: text sample too short ({len(sample)} chars)"
    assert _has_no_null_bytes(sample), f"{mod.abbrev}: null bytes in sampled text"


# =============================================================================
# Category 6: Format detection correctness
# =============================================================================

def test_format_consistency():
    """Verify our pre-test format assumptions: all modules detected as expected."""
    expected_formats = {
        # JFB
        "NABRE Notes":         "jfb",
        "NAS95S Notes":        "jfb",
        "NET Notes":           "jfb",
        "NRSVue Notes":        "jfb",
        "People\u2019s NT":   "jfb",
        "Sacra Pagina":        "jfb",
        "Treasury Enhanced":   "jfb",
        # Generic
        "Metzger Text Commentary":  "generic",
        "NA28 Apparatus":            "generic",
        "NAS Notes":                 "generic",
        "NBLH Notes":                "generic",
        "NETS Notes":                "generic",
        "NETS2 Notes":               "generic",
        "NRSVue2 Notes":             "generic",
        "ORIGEN Notes":              "generic",
        "Outlines":                  "generic",
        "Philo Crossrefs":           "generic",
        "Qumran Crossrefs":          "generic",
        "VDC2014 Note":              "generic",
        "WEB Notes":                 "generic",
    }
    for name, path in sorted(ALL_MODULES.items()):
        mod = _load(path)
        expected = expected_formats.get(name)
        assert expected is not None, f"Missing expected format for {name}"
        assert mod._format == expected, (
            f"{name}: expected format {expected!r}, got {mod._format!r}"
        )
        assert mod._skipped is False, f"{name} was unexpectedly skipped"


# =============================================================================
# Category 7: Cross-ref modules (Philo, Qumran) — list_sections works
# =============================================================================

def test_philo_crossrefs_has_references(philo_crossrefs):
    """Philo Crossrefs: section listing should work and contain OT references."""
    sections = philo_crossrefs.list_sections()
    assert len(sections) >= 100, f"Expected 100+ sections, got {len(sections)}"

    # At least one section should contain a Bible reference like "Genesis" or "Exodus"
    ref_found = False
    for sn, title in sections:
        try:
            text = philo_crossrefs.get_section_text(sn)
            if re.search(r"Genesis|Exodus|Leviticus|Deuteronomy", text, re.IGNORECASE):
                ref_found = True
                break
        except Exception:
            pass
    assert ref_found, "Philo Crossrefs: no OT book references found in sections"


def test_qumran_crossrefs_has_references(qumran_crossrefs):
    """Qumran Crossrefs: section listing should work and contain DSS references."""
    sections = qumran_crossrefs.list_sections()
    assert len(sections) >= 100, f"Expected 100+ sections, got {len(sections)}"

    # At least one section should contain a DSS reference like 4Q or 1Q
    ref_found = False
    for sn, title in sections:
        try:
            text = qumran_crossrefs.get_section_text(sn)
            if re.search(r"\b[1-9]Q|Genesis|Exodus", text):
                ref_found = True
                break
        except Exception:
            pass
    assert ref_found, "Qumran Crossrefs: no DSS or OT references found in sections"


# =============================================================================
# Category 8: No module should be empty or skipped
# =============================================================================

def test_no_module_skipped():
    """All 20 modules should be loaded without triggering the skip flag."""
    skipped = []
    for name, path in sorted(ALL_MODULES.items()):
        mod = _load(path)
        if mod._skipped:
            skipped.append(name)
        if len(mod._entries) == 0:
            skipped.append(f"{name} (zero entries)")
    assert not skipped, f"Modules skipped or empty: {skipped}"
