"""
Audit all 7 legacy .acc2 type-15 modules for loading, content,
paragraph structure, text quality, and content-specific assertions.
"""

import pytest
from pathlib import Path

# -- discovery helper
try:
    from accordance_parser.discovery import discover_modules
except ImportError:
    discover_modules = None

try:
    from accordance_parser.acc2_module import Acc2Module
except ImportError:
    Acc2Module = None


ACC2_DIR = Path(__file__).resolve().parent.parent / "modules" / "Tools"

EXPECTED_MODULES = [
    "Synoptics",
    "Gospels",
    "Harmony",
    "Epistles",
    "Q (Sayings)",
    "OT in NT",
    "Old Testament",
]

# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


def _all_modules() -> list[tuple[str, "Acc2Module"]]:
    """Load every .acc2 module.  Skip if the file is missing."""
    mods: list[tuple[str, "Acc2Module"]] = []
    for name in EXPECTED_MODULES:
        path = ACC2_DIR / f"{name}.acc2"
        if not path.exists():
            continue
        mods.append((name, Acc2Module(str(path))))
    return mods


@pytest.fixture(scope="module")
def loaded_modules():
    return _all_modules()


@pytest.fixture(scope="module")
def module_map(loaded_modules):
    return {name: mod for name, mod in loaded_modules}


# ---------------------------------------------------------------------------
# 1.  Discovery — all 7 modules present
# ---------------------------------------------------------------------------


@pytest.mark.parametrize("name", EXPECTED_MODULES)
def test_file_exists(name):
    """Every expected .acc2 file is on disk."""
    path = ACC2_DIR / f"{name}.acc2"
    assert path.exists(), f"Missing: {path}"


def test_discovery_includes_all_seven():
    """``discover_modules()`` finds exactly the 7 type-15 legacy tools."""
    if discover_modules is None:
        pytest.skip("discover_modules not importable")
    registry = discover_modules()
    acc2 = {k for k, v in registry.items() if v.get("type") == 15}
    for name in EXPECTED_MODULES:
        assert name in acc2, f"{name!r} not found by discover_modules()"


# ---------------------------------------------------------------------------
# 2.  Loading — no crash, has paragraphs, non-zero counts
# ---------------------------------------------------------------------------


@pytest.mark.parametrize("name", EXPECTED_MODULES)
def test_module_loads(module_map, name):
    """Acc2Module(path) does not crash and has a title."""
    if name not in module_map:
        pytest.skip(f"{name}.acc2 not found")
    mod = module_map[name]
    assert mod.title, f"{name} has empty title"
    assert len(mod.title) > 5, f"{name} title too short: {repr(mod.title)}"


@pytest.mark.parametrize("name", EXPECTED_MODULES)
def test_has_paragraphs(module_map, name):
    """Module has a non-zero number of paragraph entries."""
    if name not in module_map:
        pytest.skip(f"{name}.acc2 not found")
    mod = module_map[name]
    paras = mod.entries()
    assert len(paras) > 0, f"{name} has 0 paragraphs"

    # Reasonable counts (all known modules fall in 55–434 range)
    assert len(paras) >= 30, f"{name} has only {len(paras)} paragraphs (expected >= 30)"


# ---------------------------------------------------------------------------
# 3.  Content — get_paragraph returns non-empty text
# ---------------------------------------------------------------------------


@pytest.mark.parametrize("name", EXPECTED_MODULES)
def test_get_paragraph_first(module_map, name):
    """get_paragraph(first) returns non-empty text."""
    if name not in module_map:
        pytest.skip(f"{name}.acc2 not found")
    mod = module_map[name]
    entries = mod.entries()
    if not entries:
        pytest.skip(f"{name} has no entries")
    first_num = entries[0][0]
    result = mod.get_paragraph(first_num)
    assert result is not None, f"{name} get_paragraph({first_num}) returned None"
    assert len(result[2]) > 3, (
        f"{name} paragraph {first_num} text too short: {repr(result[2])}"
    )


@pytest.mark.parametrize("name", EXPECTED_MODULES)
def test_get_paragraph_last(module_map, name):
    """get_paragraph(last) returns non-empty text."""
    if name not in module_map:
        pytest.skip(f"{name}.acc2 not found")
    mod = module_map[name]
    entries = mod.entries()
    if not entries:
        pytest.skip(f"{name} has no entries")
    last_num = entries[-1][0]
    result = mod.get_paragraph(last_num)
    assert result is not None, f"{name} get_paragraph({last_num}) returned None"
    assert len(result[2]) > 3, (
        f"{name} paragraph {last_num} text too short: {repr(result[2])}"
    )


def test_get_paragraph_nonexistent_returns_none(module_map):
    """get_paragraph(999999) returns None for a huge number."""
    # Test on the first available module
    mods = list(module_map.values())
    if not mods:
        pytest.skip("No modules loaded")
    assert mods[0].get_paragraph(999999) is None


# ---------------------------------------------------------------------------
# 4.  Text quality — no null bytes, readable text, reasonable line lengths
# ---------------------------------------------------------------------------


@pytest.mark.parametrize("name", EXPECTED_MODULES)
def test_no_null_bytes(module_map, name):
    """No paragraph text contains null bytes."""
    if name not in module_map:
        pytest.skip(f"{name}.acc2 not found")
    mod = module_map[name]
    for pnum, _title, text in mod.entries():
        assert "\x00" not in text, f"{name} #{pnum} contains null byte"


@pytest.mark.parametrize("name", EXPECTED_MODULES)
def test_text_is_readable(module_map, name):
    """All paragraph text decodes as printable ASCII/latin-1 (no control chars below 0x20 except tabs/newlines)."""
    if name not in module_map:
        pytest.skip(f"{name}.acc2 not found")
    mod = module_map[name]
    bad_chars = set()
    for pnum, _title, text in mod.entries():
        for ch in text:
            cp = ord(ch)
            if cp < 0x20 and cp not in (0x09, 0x0A, 0x0D):
                bad_chars.add((pnum, cp, repr(ch)))
    assert not bad_chars, (
        f"{name}: {len(bad_chars)} unprintable chars found. "
        f"Sample: {list(bad_chars)[:5]}"
    )


@pytest.mark.parametrize("name", EXPECTED_MODULES)
def test_paragraph_numbers_are_non_negative(module_map, name):
    """All paragraph numbers are >= 0."""
    if name not in module_map:
        pytest.skip(f"{name}.acc2 not found")
    mod = module_map[name]
    for pnum, _title, _text in mod.entries():
        assert pnum >= 0, f"{name} has negative paragraph number {pnum}"


# ---------------------------------------------------------------------------
# 5.  Gospel parallels — Synoptics and Gospels
# ---------------------------------------------------------------------------

# Keywords that indicate Gospel narrative content
GOSPEL_KEYWORDS = [
    "jesus",
    "disciple",
    "parable",
    "kingdom",
    "baptist",
    "baptism",
    "temptation",
    "sermon",
    "beatitude",
    "miracle",
    "resurrection",
    "crucifixion",
    "passion",
    "temple",
    "pharisee",
    "peter",
    "john",
    "matthew",
    "mark",
    "luke",
    "gospel",
    "healing",
    "demoniac",
]


def _has_gospel_content(mod: Acc2Module) -> tuple[int, int]:
    total = 0
    hits = 0
    for _pnum, title, _text in mod.entries():
        lower = title.lower()
        total += 1
        if any(kw in lower for kw in GOSPEL_KEYWORDS):
            hits += 1
    return hits, total


def test_synoptics_has_gospel_content(module_map):
    """Synoptics module: most paragraph titles reference Gospel subject matter."""
    if "Synoptics" not in module_map:
        pytest.skip("Synoptics.acc2 not found")
    hits, total = _has_gospel_content(module_map["Synoptics"])
    assert hits >= 50, f"Synoptics: only {hits}/{total} entries contain Gospel keywords"


def test_gospels_has_gospel_content(module_map):
    """Gospels module: most paragraph titles reference Gospel subject matter."""
    if "Gospels" not in module_map:
        pytest.skip("Gospels.acc2 not found")
    hits, total = _has_gospel_content(module_map["Gospels"])
    assert hits >= 50, f"Gospels: only {hits}/{total} entries contain Gospel keywords"


def test_harmony_has_gospel_content(module_map):
    """Harmony module: most paragraph titles reference Gospel subject matter."""
    if "Harmony" not in module_map:
        pytest.skip("Harmony.acc2 not found")
    hits, total = _has_gospel_content(module_map["Harmony"])
    assert hits >= 40, f"Harmony: only {hits}/{total} entries contain Gospel keywords"


# ---------------------------------------------------------------------------
# 6.  OT in NT — Old Testament quotation references
# ---------------------------------------------------------------------------

# OT-in-NT paragraph titles describe OT passages being quoted, often without
# naming the book explicitly.  These keywords capture both explicit and
# implicit OT references (patriarchs, prophets, cultic terms, covenant language).
OT_IN_NT_KEYWORDS = [
    "isaiah",
    "jeremiah",
    "psalm",
    "moses",
    "prophet",
    "prophecy",
    "covenant",
    "exodus",
    "elijah",
    "david",
    "abraham",
    "zechariah",
    "hosea",
    "malachi",
    "micah",
    "amos",
    "deutero",
    "genesis",
    "quotation",
    "fulfill",
    "spoken",
    "written",
    "abram",
    "jacob",
    "isaac",
    "joseph",
    "noah",
    "enoch",
    "melchizedek",
    "priest",
    "altar",
    "tabernacle",
    "offering",
    "sacrifice",
    "firstborn",
    "passover",
    "sinai",
    "commandment",
    "statute",
    "israel",
]


def test_ot_in_nt_has_ot_quotation_keywords(module_map):
    """OT in NT module: entries reference OT books/prophets."""
    if "OT in NT" not in module_map:
        pytest.skip("OT in NT.acc2 not found")
    mod = module_map["OT in NT"]
    hits = 0
    total = 0
    for _pnum, title, _text in mod.entries():
        lower = title.lower()
        total += 1
        if any(kw in lower for kw in OT_IN_NT_KEYWORDS):
            hits += 1
    assert hits >= 30, (
        f"OT in NT: only {hits}/{total} entries contain OT quotation keywords"
    )


# ---------------------------------------------------------------------------
# 7.  All 7 modules present — verify as a group
# ---------------------------------------------------------------------------


def test_all_seven_loaded(loaded_modules):
    """All 7 .acc2 files load without crashing."""
    names = {name for name, _mod in loaded_modules}
    missing = set(EXPECTED_MODULES) - names
    assert not missing, f"Modules not loaded: {missing}"
    assert len(loaded_modules) == 7, (
        f"Expected 7 modules, got {len(loaded_modules)}: {names}"
    )


def test_total_paragraph_count(loaded_modules):
    """Combined paragraph count is reasonable (~1592 per README)."""
    total = 0
    for _name, mod in loaded_modules:
        total += len(mod.entries())
    # README says 1,592. Allow ±50 for minor data variations.
    assert 1500 <= total <= 1700, f"Total paragraphs {total} outside expected range"


def test_no_module_has_same_title_as_another(loaded_modules):
    """Each module has a distinct title."""
    titles = {}
    for name, mod in loaded_modules:
        t = mod.title.strip()
        assert t not in titles, f"Duplicate title {t!r}: {name} and {titles[t]}"
        titles[t] = name
