"""
Comprehensive tests for bible_api.py, lectionary.py, citation_format.py,
and canon.py (Advanced).

Covers 12 multi-step scenarios:
  A3-1:  Multi-verse range retrieval
  A3-2:  Translation parameter validation
  A3-3:  Network failure → local cache fallback
  A3-4:  Catholic lectionary full traversal
  A3-5:  Lectionary edge dates
  A3-6:  Lectionary invalid dates
  A3-7:  Deuterocanonical books
  A3-8:  Inter-testament boundary
  A3-9:  Single-chapter book disambiguation
  A3-10: Chapter/verse out-of-bounds
  A3-11: JSON cache integrity
  A3-12: Citation formatting

Strategy: Test pure functions and local cache paths (no network required).
Skip network-dependent tests.
"""

from __future__ import annotations

import datetime as dt

import pytest

# ---------------------------------------------------------------------------
# Canon imports (pure functions, no network)
# ---------------------------------------------------------------------------

from accordance_parser.canon import (
    BOOKS_BY_OSIS,
    chapter_count,
    last_verse_of_chapter,
    parse_reference,
    resolve_book,
    resolve_book_id,
    is_old_testament,
    is_new_testament,
    CANON_PROTESTANT,
    CANON_CATHOLIC,
)

# ---------------------------------------------------------------------------
# Bible API imports (some require local JSON cache)
# ---------------------------------------------------------------------------

from accordance_parser.bible_api import (
    _resolve_translation,
    BOLLS_CODES,
    lookup_cached_verse,
    lookup_kjv_local,
    get_verse,
    get_verse_strongs,
    get_verse_range,
    parse_reference as api_parse_reference,
    clean_text,
    extract_strongs,
    VerseResult,
)

# ---------------------------------------------------------------------------
# Citation imports (pure functions)
# ---------------------------------------------------------------------------

from accordance_parser.citation_format import (
    lexicon_citation,
    lexicon_citation_markdown,
    commentary_citation,
)

# ---------------------------------------------------------------------------
# Lectionary imports
# ---------------------------------------------------------------------------

from accordance_parser.lectionary import (
    get_lectionary_readings,
    get_lectionary_for_date_string,
)


# =============================================================================
# A3-1: Multi-verse range retrieval
# =============================================================================

def test_parse_reference_range():
    """Parse 'Genesis 1:1-5' and verify parsed fields."""
    ref = parse_reference("Genesis 1:1-5")
    assert ref is not None
    assert ref.book_osis == "Gen"
    assert ref.chapter == 1
    assert ref.verse == 1
    assert ref.end_verse == 5


def test_api_parse_reference_range():
    """bible_api.parse_reference returns integer tuple for range."""
    result = api_parse_reference("Genesis 1:1-5")
    assert result is not None
    book_id, ch, v, end_v = result
    assert book_id == 1
    assert ch == 1
    assert v == 1
    assert end_v == 5


def test_get_verse_single_with_range_string():
    """get_verse with a single-verse string uses local cache (no network)."""
    result = get_verse("Genesis 1:1", translation="kjv")
    assert result is not None
    assert result.book_id == 1
    assert result.chapter == 1
    assert result.verse == 1
    assert result.translation == "KJV"
    assert "beginning" in result.text.lower()
    assert len(result.strongs) >= 3


@pytest.mark.skip(reason="get_verse_range with multi-verse hits bolls.life API (network)")
def test_get_verse_range_multi_verse():
    """get_verse_range('Genesis 1:1-5', translation='kjv') — requires network."""
    ...


def test_get_verse_range_single_verse_local():
    """get_verse_range on a single verse uses get_verse → local cache."""
    results = get_verse_range("Genesis 1:1", translation="kjv")
    assert len(results) == 1
    result = results[0]
    assert result.book_id == 1
    assert result.chapter == 1
    assert result.verse == 1
    assert "beginning" in result.text.lower()


def test_parse_range_with_en_dash():
    """parse_reference handles en-dash (U+2013) in ranges."""
    # Range with en-dash
    ref = parse_reference("Genesis 1:1\u20135")
    assert ref is not None
    assert ref.end_verse == 5


def test_parse_range_with_em_dash():
    """parse_reference handles em-dash (U+2014) in ranges."""
    ref = parse_reference("Genesis 1:1\u20145")
    assert ref is not None
    assert ref.end_verse == 5


# =============================================================================
# A3-2: Translation parameter validation
# =============================================================================

def test_resolve_translation_known():
    """_resolve_translation returns correct bolls code for known translations."""
    assert _resolve_translation("kjv") == "KJV"
    assert _resolve_translation("KJV") == "KJV"
    assert _resolve_translation("kjvs") == "KJV"
    assert _resolve_translation("esv") == "ESV"
    assert _resolve_translation("nasb") == "NASB"
    assert _resolve_translation("niv") == "NIV"
    assert _resolve_translation("nlt") == "NLT"
    assert _resolve_translation("nkjv") == "NKJV"
    assert _resolve_translation("web") == "WEB"
    assert _resolve_translation("nrsv") == "NRSVCE"
    assert _resolve_translation("nabre") == "NABRE"
    assert _resolve_translation("drb") == "DRB"
    assert _resolve_translation("douay") == "DRB"
    assert _resolve_translation("douayrheims") == "DRB"
    assert _resolve_translation("lxx") == "LXX"
    assert _resolve_translation("wlc") == "WLC"
    assert _resolve_translation("tr") == "TR"
    assert _resolve_translation("tisch") == "TISCH"
    assert _resolve_translation("opengnt") == "OPENGNT"
    assert _resolve_translation("morphhb") == "MORPHHB"


def test_resolve_translation_unknown_becomes_itself():
    """_resolve_translation with bogus ID passes through unchanged."""
    assert _resolve_translation("BOGUS") == "BOGUS"
    assert _resolve_translation("nonexistent") == "nonexistent"
    assert _resolve_translation("abc123") == "abc123"


def test_resolve_translation_empty_string():
    """_resolve_translation with empty string returns empty string."""
    assert _resolve_translation("") == ""


def test_resolve_translation_whitespace():
    """_resolve_translation with whitespace-only string."""
    assert _resolve_translation("  ") == "  "


def test_resolve_translation_uppercase_variants():
    """_resolve_translation is case-insensitive via .lower() lookup."""
    assert _resolve_translation("KJV") == "KJV"           # exact match in BOLLS_CODES
    assert _resolve_translation("Esv") == "ESV"           # lowercases, finds "esv" → "ESV"
    assert _resolve_translation("kjv") == "KJV"
    # Mixed-case that doesn't lowercase to a known code passes through unchanged
    assert _resolve_translation("NaSBrE") == "NaSBrE"


def test_bolls_codes_coverage():
    """Verify BOLLS_CODES dict has expected entries."""
    # All canonical entries present
    for key, expected_code in [
        ("kjv", "KJV"), ("esv", "ESV"), ("nasb", "NASB"),
        ("niv", "NIV"), ("nlt", "NLT"), ("nkjv", "NKJV"),
        ("web", "WEB"), ("nrsv", "NRSVCE"), ("nabre", "NABRE"),
        ("drb", "DRB"), ("lxx", "LXX"), ("wlc", "WLC"),
        ("tr", "TR"), ("tisch", "TISCH"), ("opengnt", "OPENGNT"),
        ("morphhb", "MORPHHB"),
    ]:
        assert BOLLS_CODES[key] == expected_code, f"BOLLS_CODES[{key!r}] expected {expected_code!r}"


def test_get_verse_invalid_translation_fallback():
    """get_verse with invalid translation still tries cache, returns None if not found."""
    # "bogustrans" won't be in any local cache → returns None
    result = get_verse("John 3:16", translation="bogustrans")
    # Should return None after cache miss (no network fallback for unknown code)
    assert result is None


# =============================================================================
# A3-3: Network failure → local cache fallback
# =============================================================================

def test_lookup_cached_verse_john_11_35():
    """John 11:35 from KJV local cache returns 'Jesus wept.' with correct Strong's."""
    result = lookup_cached_verse("kjv", 43, 11, 35)
    assert result is not None
    assert result.text == "Jesus wept."
    assert "G2424" in result.strongs
    assert "G1145" in result.strongs
    assert result.book_id == 43
    assert result.chapter == 11
    assert result.verse == 35
    assert result.translation == "KJV"
    assert "John 11:35" in result.reference


def test_lookup_cached_verse_strongs_order_preserved():
    """Strong's numbers appear in word order within the verse."""
    # Gen 1:1: "In the beginning God created ..."
    # Strong's: H7225 (beginning), H0430 (God), H1254 (created), ...
    result = lookup_cached_verse("kjv", 1, 1, 1)
    assert result is not None
    assert result.strongs[0] == "H7225"
    assert result.strongs[1] == "H0430"
    assert result.strongs[2] == "H1254"


def test_lookup_cached_verse_ot_vs_nt_prefix():
    """OT verses get H prefix, NT verses get G prefix."""
    # Genesis 1:1 (OT) → H prefix
    ot_result = lookup_cached_verse("kjv", 1, 1, 1)
    assert ot_result is not None
    assert all(s.startswith("H") for s in ot_result.strongs), \
        f"OT verse should have H-prefixed Strong's: {ot_result.strongs}"

    # John 3:16 (NT) → G prefix
    nt_result = lookup_cached_verse("kjv", 43, 3, 16)
    assert nt_result is not None
    assert all(s.startswith("G") for s in nt_result.strongs), \
        f"NT verse should have G-prefixed Strong's: {nt_result.strongs}"


def test_lookup_cached_verse_cache_miss():
    """lookup_cached_verse returns None for nonexistent book/chapter/verse."""
    # Book 999 doesn't exist in KJV JSON
    result = lookup_cached_verse("kjv", 999, 1, 1)
    assert result is None


def test_lookup_cached_verse_alternate_translation():
    """lookup_cached_verse works for non-KJV translations that have JSON caches."""
    # ESV JSON is available in data/
    result = lookup_cached_verse("esv", 43, 3, 16)
    if result is not None:
        assert result.translation == "ESV"
        assert "God" in result.text


def test_lookup_kjv_local_delegate():
    """lookup_kjv_local delegates to lookup_cached_verse correctly."""
    result = lookup_kjv_local(43, 11, 35)
    assert result is not None
    assert result.text == "Jesus wept."


# =============================================================================
# A3-4: Catholic lectionary full traversal
# =============================================================================

# All hardcoded lectionary dates (from lectionary_data.py / lectionary.py)
_HARDCODED_DATES = [
    ("2026-05-25", "Monday after Pentecost", 3),          # first, psalm, gospel
    ("2026-05-26", "Tuesday after Pentecost", 3),
    ("2026-05-27", "Wednesday after Pentecost", 3),
    ("2026-05-28", "Thursday after Pentecost", 3),
    ("2026-05-29", "Friday after Pentecost", 3),
    ("2026-05-30", "Saturday after Pentecost (Vigil of Trinity Sunday)", 3),
    ("2026-05-31", "Solemnity of the Most Holy Trinity", 4),  # has second reading
    ("2026-06-07", "Solemnity of the Most Holy Body and Blood of Christ (Corpus Christi)", 4),
    ("2026-06-12", "Solemnity of the Most Sacred Heart of Jesus", 4),
]

_REQUIRED_READING_KEYS = {"first", "psalm", "gospel"}


def test_lectionary_full_traversal_hardcoded():
    """All 9 hardcoded lectionary dates return season + readings with required keys."""
    for date_str, expected_season, _min_keys in _HARDCODED_DATES:
        target = dt.date.fromisoformat(date_str)
        season, readings = get_lectionary_readings(target)

        assert season == expected_season, \
            f"{date_str}: expected season {expected_season!r}, got {season!r}"

        # All readings have required keys
        for key in _REQUIRED_READING_KEYS:
            assert key in readings, \
                f"{date_str}: missing required reading key {key!r}"

        # Readings are non-empty strings
        for key, val in readings.items():
            assert isinstance(val, str) and val.strip(), \
                f"{date_str}: reading {key!r} is empty or non-string: {val!r}"


def test_lectionary_trinity_sunday_has_second_reading():
    """Trinity Sunday and other solemnities include a 'second' reading."""
    for date_str, _, _ in _HARDCODED_DATES:
        target = dt.date.fromisoformat(date_str)
        _, readings = get_lectionary_readings(target)
        if "second" in readings:
            assert readings["second"].strip(), \
                f"{date_str}: second reading is empty: {readings['second']!r}"


def test_lectionary_string_date_api():
    """get_lectionary_for_date_string works for hardcoded dates."""
    result = get_lectionary_for_date_string("2026-05-31")
    assert result is not None
    season, readings = result
    assert season == "Solemnity of the Most Holy Trinity"
    assert "gospel" in readings
    # Gospel from scraped 1998 US lectionary source
    assert readings["gospel"] == "John 3:16-18"


# =============================================================================
# A3-5: Lectionary edge dates
# =============================================================================

def test_lectionary_dec31_boundary():
    """Dec 31 not hardcoded → falls through to fallback logic."""
    result = get_lectionary_readings(dt.date(2026, 12, 31))
    assert result is not None
    season, readings = result
    # December 31 > Trinity Sunday → "Ordinary Time (Weekday, Year II)"
    # But wait, Dec 31 is after Trinity (May 31) → it's also after Pentecost
    assert isinstance(season, str) and len(season) > 0
    assert "first" in readings
    assert "psalm" in readings
    assert "gospel" in readings


def test_lectionary_feb29_leap_year():
    """Feb 29 in a leap year (2024, 2028) should parse and return a season."""
    # 2024 was a leap year
    result = get_lectionary_readings(dt.date(2024, 2, 29))
    assert result is not None
    season, readings = result
    assert isinstance(season, str) and len(season) > 0
    assert "first" in readings
    assert "gospel" in readings


def test_lectionary_pentecost_sunday_day_before():
    """Day before Pentecost (2026-05-23) — now has Vigil Mass readings."""
    # May 23 is Pentecost Vigil (now in the comprehensive data)
    result = get_lectionary_readings(dt.date(2026, 5, 23))
    assert result is not None
    season, readings = result
    assert "Pentecost" in season and "Vigil" in season


def test_lectionary_pentecost_sunday():
    """Pentecost Sunday itself (2026-05-24) — now has full readings."""
    # May 24 is Pentecost Sunday Day Mass (now in the comprehensive data)
    result = get_lectionary_readings(dt.date(2026, 5, 24))
    assert result is not None
    season, readings = result
    assert "Pentecost" in season and "Day" in season


def test_lectionary_pentecost_monday_hardcoded():
    """Monday after Pentecost (2026-05-25) — IS hardcoded."""
    result = get_lectionary_readings(dt.date(2026, 5, 25))
    assert result is not None
    season, _ = result
    assert season == "Monday after Pentecost"


# =============================================================================
# A3-6: Lectionary invalid dates
# =============================================================================

def test_lectionary_invalid_strings():
    """get_lectionary_for_date_string returns None for invalid date strings."""
    assert get_lectionary_for_date_string("not-a-date") is None
    assert get_lectionary_for_date_string("") is None
    assert get_lectionary_for_date_string("banana") is None
    assert get_lectionary_for_date_string("2026") is None  # year only
    assert get_lectionary_for_date_string("May 25, 2026") is None


def test_lectionary_invalid_month_day():
    """get_lectionary_for_date_string returns None for impossible dates."""
    assert get_lectionary_for_date_string("2026-13-01") is None   # month 13
    assert get_lectionary_for_date_string("2026-01-32") is None   # day 32
    assert get_lectionary_for_date_string("2026-00-01") is None   # month 0
    assert get_lectionary_for_date_string("2026-06-00") is None   # day 0


def test_lectionary_far_future():
    """Dates far in the future should not crash, return fallback."""
    result = get_lectionary_for_date_string("2099-12-25")
    assert result is not None
    season, readings = result
    assert isinstance(season, str)
    assert "first" in readings
    assert "gospel" in readings


def test_lectionary_ancient_date():
    """Ancient dates should not crash, return fallback."""
    result = get_lectionary_for_date_string("0033-04-03")
    assert result is not None
    season, readings = result
    assert isinstance(season, str)
    assert "first" in readings
    assert "gospel" in readings


def test_lectionary_whitespace_tolerance():
    """Date strings with leading/trailing whitespace are handled."""
    result = get_lectionary_for_date_string("  2026-05-31  ")
    assert result is not None
    season, _ = result
    assert season == "Solemnity of the Most Holy Trinity"


# =============================================================================
# A3-7: Deuterocanonical books
# =============================================================================

def test_resolve_book_wisdom():
    """resolve_book('Wisdom') returns Book with bolls_id 70, testament 'ot'."""
    book = resolve_book("Wisdom")
    assert book is not None
    assert book.osis == "Wis"
    assert book.bolls_id == 70
    assert book.testament == "ot"
    assert book.name == "Wisdom"


def test_resolve_book_sirach():
    """resolve_book('Sirach') returns correct Book."""
    book = resolve_book("Sirach")
    assert book is not None
    assert book.osis == "Sir"
    assert book.bolls_id == 71


def test_resolve_book_tobit():
    """resolve_book('Tobit') returns correct Book."""
    book = resolve_book("Tobit")
    assert book is not None
    assert book.osis == "Tob"
    assert book.bolls_id == 68
    assert book.testament == "ot"


def test_deuterocanonical_abbreviations():
    """Deuterocanonical book abbreviations resolve correctly."""
    assert resolve_book_id("wis") == 70
    assert resolve_book_id("wisdom") == 70
    assert resolve_book_id("sir") == 71
    assert resolve_book_id("sirach") == 71
    assert resolve_book_id("bar") == 73
    assert resolve_book_id("baruch") == 73
    assert resolve_book_id("1macc") == 74
    assert resolve_book_id("2macc") == 75


def test_deuterocanonical_in_catholic_canon():
    """Catholic canon includes deuterocanonical books."""
    catholic_osis = {b.osis for b in CANON_CATHOLIC.books}
    assert "Wis" in catholic_osis
    assert "Sir" in catholic_osis
    assert "Tob" in catholic_osis
    assert "Jdt" in catholic_osis
    assert "Bar" in catholic_osis
    assert "1Macc" in catholic_osis
    assert "2Macc" in catholic_osis


def test_deuterocanonical_not_in_protestant_canon():
    """Protestant canon excludes deuterocanonical books."""
    protestant_osis = {b.osis for b in CANON_PROTESTANT.books}
    assert "Wis" not in protestant_osis
    assert "Tob" not in protestant_osis


def test_wisdom_verse_count():
    """Wisdom has correct verse count structure."""
    verses = CANON_CATHOLIC.last_verse.get("Wis", ())
    assert len(verses) == 19  # 19 chapters
    assert verses[0] == 16    # Chapter 1 has 16 verses
    assert verses[2] == 19    # Chapter 3 has 19 verses


@pytest.mark.skip(reason="NABRE local JSON has only books 1-66; Wisdom (70) requires network")
def test_nabre_wisdom_lookup():
    """NABRE lookup for Wisdom 3:1 — requires bolls.life API (network)."""
    from accordance_parser.bible_api import fetch_bolls_verse
    result = fetch_bolls_verse("nabre", 70, 3, 1)
    assert result is not None


# =============================================================================
# A3-8: Inter-testament boundary
# =============================================================================

def test_ot_nt_boundary_malachi():
    """Malachi 4:6 is OT → Strong's with H prefix."""
    result = lookup_cached_verse("kjv", 39, 4, 6)
    assert result is not None
    assert result.strongs, "Malachi 4:6 should have Strong's numbers"
    for s in result.strongs:
        assert s.startswith("H"), f"Malachi 4:6 Strong's should start with H: {s}"
    # Verify Malachi is rightly identified as OT
    assert is_old_testament("Mal")
    assert not is_new_testament("Mal")


def test_ot_nt_boundary_matthew():
    """Matthew 1:1 is NT → Strong's with G prefix."""
    result = lookup_cached_verse("kjv", 40, 1, 1)
    assert result is not None
    assert result.strongs, "Matthew 1:1 should have Strong's numbers"
    for s in result.strongs:
        assert s.startswith("G"), f"Matthew 1:1 Strong's should start with G: {s}"
    # Verify Matthew is rightly identified as NT
    assert not is_old_testament("Matt")
    assert is_new_testament("Matt")


def test_inter_testament_prefix_flip():
    """Strong's prefix flips from H to G crossing OT→NT boundary."""
    # Last OT book (Malachi 4)
    for mal_ch in range(1, 5):
        result = lookup_cached_verse("kjv", 39, mal_ch, 1)
        if result and result.strongs:
            assert all(s.startswith("H") for s in result.strongs), \
                f"Malachi {mal_ch}:1 has unexpected prefix: {result.strongs}"
            break

    # First NT book (Matthew 1)
    result = lookup_cached_verse("kjv", 40, 1, 1)
    assert result is not None
    assert result.strongs
    assert all(s.startswith("G") for s in result.strongs), \
        f"Matthew 1:1 has unexpected prefix: {result.strongs}"


def test_testament_functions_unknown():
    """is_old_testament / is_new_testament return False for unknown OSIS."""
    assert not is_old_testament("NotABook")
    assert not is_new_testament("NotABook")
    assert not is_old_testament("")
    assert not is_new_testament("")


# =============================================================================
# A3-9: Single-chapter book disambiguation
# =============================================================================

# All 5 single-chapter books with their expected properties
_SINGLE_CHAPTER_BOOKS = {
    "Obad":  {"name": "Obadiah", "bolls_id": 31, "last_verse": 21},
    "Phlm":  {"name": "Philemon", "bolls_id": 57, "last_verse": 25},
    "2John": {"name": "2 John", "bolls_id": 63, "last_verse": 13},
    "3John": {"name": "3 John", "bolls_id": 64, "last_verse": 15},
    "Jude":  {"name": "Jude", "bolls_id": 65, "last_verse": 25},
}


def test_single_chapter_books_flagged_correctly():
    """All 5 single-chapter books have is_single_chapter=True."""
    for osis in _SINGLE_CHAPTER_BOOKS:
        book = BOOKS_BY_OSIS[osis]
        assert book.is_single_chapter, f"{osis} should be single-chapter"
        assert chapter_count(osis) == 1, f"{osis} should have 1 chapter"


def test_single_chapter_book_chapter_count():
    """chapter_count returns 1 for all single-chapter books."""
    for osis in _SINGLE_CHAPTER_BOOKS:
        assert chapter_count(osis) == 1


def test_single_chapter_no_colon_form():
    """Single-chapter books: 'Jude 4' → chapter=1, verse=4."""
    tests = [
        ("Jude 4", "Jude", 4, 25),
        ("Philemon 15", "Phlm", 15, 25),
        ("Obadiah 21", "Obad", 21, 21),
        ("2 John 7", "2John", 7, 13),
        ("3 John 10", "3John", 10, 15),
    ]
    for ref_str, expected_osis, expected_verse, expected_last in tests:
        ref = parse_reference(ref_str)
        assert ref is not None, f"parse_reference({ref_str!r}) → None"
        assert ref.book_osis == expected_osis, \
            f"{ref_str}: expected {expected_osis}, got {ref.book_osis}"
        assert ref.chapter == 1, \
            f"{ref_str}: chapter should be 1, got {ref.chapter}"
        assert ref.verse == expected_verse, \
            f"{ref_str}: expected verse {expected_verse}, got {ref.verse}"
        assert ref.end_verse is None

        # Also verify last_verse_of_chapter is correct
        lv = last_verse_of_chapter(expected_osis, 1)
        assert lv == expected_last, \
            f"{expected_osis} ch 1: expected last verse {expected_last}, got {lv}"


def test_single_chapter_explicit_colon_form():
    """Single-chapter books: 'Jude 1:4' → chapter=1, verse=4."""
    tests = [
        ("Jude 1:4", "Jude", 4),
        ("Philemon 1:15", "Phlm", 15),
        ("Obadiah 1:21", "Obad", 21),
        ("2 John 1:7", "2John", 7),
        ("3 John 1:10", "3John", 10),
    ]
    for ref_str, expected_osis, expected_verse in tests:
        ref = parse_reference(ref_str)
        assert ref is not None, f"parse_reference({ref_str!r}) → None"
        assert ref.book_osis == expected_osis
        assert ref.chapter == 1
        assert ref.verse == expected_verse
        assert ref.end_verse is None


def test_single_chapter_no_colon_range():
    """Single-chapter books: 'Jude 3-5' → chapter=1, verse=3, end=5."""
    ref = parse_reference("Jude 3-5")
    assert ref is not None
    assert ref.book_osis == "Jude"
    assert ref.chapter == 1
    assert ref.verse == 3
    assert ref.end_verse == 5


def test_single_chapter_explicit_colon_range():
    """Single-chapter books: 'Jude 1:3-5' → chapter=1, verse=3, end=5."""
    ref = parse_reference("Jude 1:3-5")
    assert ref is not None
    assert ref.book_osis == "Jude"
    assert ref.chapter == 1
    assert ref.verse == 3
    assert ref.end_verse == 5


def test_single_chapter_bolls_ids():
    """Single-chapter books have correct bolls_ids."""
    for osis, info in _SINGLE_CHAPTER_BOOKS.items():
        assert resolve_book_id(osis) == info["bolls_id"]
        assert resolve_book_id(info["name"]) == info["bolls_id"]


def test_single_chapter_last_verse():
    """last_verse_of_chapter for single-chapter books returns correct count."""
    for osis, info in _SINGLE_CHAPTER_BOOKS.items():
        assert last_verse_of_chapter(osis, 1) == info["last_verse"]

    # Chapter 2 should be 0 (out of range) for single-chapter books
    for osis in _SINGLE_CHAPTER_BOOKS:
        assert last_verse_of_chapter(osis, 2) == 0, \
            f"{osis} chapter 2 should return 0"


# =============================================================================
# A3-10: Chapter/verse out-of-bounds
# =============================================================================

# Chapter counts for multi-chapter books
_CHAPTER_COUNTS = {
    "Gen": 50, "Exod": 40, "Lev": 27, "Ps": 150, "Isa": 66,
    "Matt": 28, "Acts": 28, "Rev": 22,
}


def test_chapter_count_known_books():
    """chapter_count returns correct number of chapters for known books."""
    for osis, expected in _CHAPTER_COUNTS.items():
        assert chapter_count(osis) == expected, \
            f"{osis}: expected {expected} chapters, got {chapter_count(osis)}"


def test_chapter_count_unknown_book():
    """chapter_count returns 0 for unknown OSIS."""
    assert chapter_count("NotABook") == 0
    assert chapter_count("") == 0


def test_last_verse_oob_chapter_high():
    """last_verse_of_chapter returns 0 for chapter beyond book's range."""
    # Genesis has 50 chapters
    assert last_verse_of_chapter("Gen", 51) == 0
    # Psalms has 150 chapters
    assert last_verse_of_chapter("Ps", 151) == 0
    # Revelation has 22 chapters
    assert last_verse_of_chapter("Rev", 23) == 0
    # Jude has 1 chapter
    assert last_verse_of_chapter("Jude", 2) == 0


def test_last_verse_oob_chapter_zero():
    """last_verse_of_chapter returns 0 for chapter 0."""
    assert last_verse_of_chapter("Gen", 0) == 0
    assert last_verse_of_chapter("Ps", 0) == 0
    assert last_verse_of_chapter("Rev", 0) == 0


def test_last_verse_oob_chapter_negative():
    """last_verse_of_chapter returns 0 for negative chapter."""
    assert last_verse_of_chapter("Gen", -1) == 0


def test_last_verse_valid():
    """last_verse_of_chapter returns correct values for valid chapters."""
    assert last_verse_of_chapter("Gen", 1) == 31
    assert last_verse_of_chapter("Gen", 50) == 26
    assert last_verse_of_chapter("Exod", 20) == 26  # Ten Commandments chapter
    assert last_verse_of_chapter("Ps", 119) == 176  # Longest chapter
    assert last_verse_of_chapter("Rev", 22) == 21
    assert last_verse_of_chapter("Matt", 28) == 20


def test_get_verse_oob_chapter_kjv():
    """get_verse with out-of-range chapter returns None."""
    # Genesis 51:1 doesn't exist
    assert get_verse("Genesis 51:1") is None


def test_get_verse_oob_psalm_verse():
    """get_verse Psalm 119:177 doesn't exist (Ps 119 has 176 verses)."""
    result = get_verse("Psalm 119:177")
    assert result is None


def test_get_verse_oob_revelation():
    """get_verse Revelation 22:22 doesn't exist (Rev 22 has 21 verses)."""
    result = get_verse("Revelation 22:22")
    assert result is None


def test_parse_reference_oob_chapter():
    """parse_reference returns valid parsed structure even for OOB chapters."""
    # parse_reference doesn't validate chapter ranges — it just parses.
    ref = parse_reference("Genesis 51:1")
    assert ref is not None
    assert ref.book_osis == "Gen"
    assert ref.chapter == 51
    assert ref.verse == 1


# =============================================================================
# A3-11: JSON cache integrity
# =============================================================================

def test_kjv_cache_loaded():
    """KJV cache loads successfully via lookup_cached_verse."""
    result = lookup_cached_verse("kjv", 1, 1, 1)
    assert result is not None
    assert result.text.strip(), "Verse text should not be empty"
    assert result.translation == "KJV"


def test_kjv_genesis_1_1_strongs_completeness():
    """Genesis 1:1 has all expected Strong's numbers with correct prefixes."""
    result = lookup_cached_verse("kjv", 1, 1, 1)
    assert result is not None

    # Known Strong's for Genesis 1:1 (KJV):
    # H7225 (beginning), H0430 (God), H1254 (created), H0853 (et),
    # H8064 (heaven), H0853 (et), H0776 (earth)
    expected_prefixes = ["H7225", "H0430", "H1254"]
    for s in expected_prefixes:
        assert s in result.strongs, \
            f"Expected Strong's {s} in Genesis 1:1, got {result.strongs}"

    # At least 7 Strong's numbers
    assert len(result.strongs) >= 7, \
        f"Genesis 1:1 should have >=7 Strong's, got {len(result.strongs)}: {result.strongs}"

    # All OT prefixes
    for s in result.strongs:
        assert s.startswith("H"), f"All Strong's should be H-prefixed: {s}"


def test_kjv_idempotent_lookups():
    """Repeated lookups return identical results (idempotent)."""
    result1 = lookup_cached_verse("kjv", 43, 3, 16)
    result2 = lookup_cached_verse("kjv", 43, 3, 16)
    assert result1 is not None
    assert result2 is not None
    assert result1.text == result2.text
    assert result1.strongs == result2.strongs
    assert result1.reference == result2.reference


def test_kjv_verses_ascending_order():
    """Verses within a chapter maintain ascending verse order."""
    # Genesis 1:1-5 via individual lookups
    verses = []
    for vnum in range(1, 6):
        result = lookup_cached_verse("kjv", 1, 1, vnum)
        assert result is not None, f"Genesis 1:1:{vnum} should exist"
        assert result.verse == vnum, \
            f"Expected verse {vnum}, got {result.verse}"
        verses.append(result)

    # Each verse has differing text
    texts = [v.text for v in verses]
    assert len(set(texts)) == len(texts), "All Genesis 1:1-5 verses should differ"


def test_strongs_index_integrity():
    """get_verse_strongs returns correct Strong's from the forward index."""
    strongs = get_verse_strongs(1, 1, 1)  # Genesis 1:1
    assert strongs is not None
    assert "H7225" in strongs
    assert "H0430" in strongs

    # Nonexistent verse returns None
    assert get_verse_strongs(1, 999, 1) is None


def test_clean_text_strips_tags():
    """clean_text removes <S>NNN</S> and <sup> tags."""
    raw = "Jesus<S>2424</S> wept<S>1145</S>."
    clean = clean_text(raw)
    assert "<S>" not in clean
    assert "</S>" not in clean
    assert "Jesus wept." in clean


def test_clean_text_strips_sup_tags():
    """clean_text removes <sup>...</sup> translator note tags."""
    raw = 'And he said<sup>[*]</sup> unto them'
    clean = clean_text(raw)
    assert "<sup>" not in clean
    assert "</sup>" not in clean
    assert "[*]" not in clean


def test_extract_strongs_ot_prefix():
    """extract_strongs returns H-prefixed Strong's for OT."""
    raw = "In the beginning<S>7225</S> God<S>430</S>"
    strongs = extract_strongs(raw, is_ot=True)
    assert strongs == ["H7225", "H0430"]


def test_extract_strongs_nt_prefix():
    """extract_strongs returns G-prefixed Strong's for NT."""
    raw = "Jesus<S>2424</S> wept<S>1145</S>"
    strongs = extract_strongs(raw, is_ot=False)
    assert strongs == ["G2424", "G1145"]


def test_extract_strongs_empty_text():
    """extract_strongs returns empty list for text with no Strong's."""
    assert extract_strongs("NoStrongsHere", is_ot=True) == []
    assert extract_strongs("", is_ot=False) == []


def test_verse_result_dataclass():
    """VerseResult dataclass can be constructed and fields match."""
    vr = VerseResult(
        text="clean",
        raw_text="raw<S>1234</S>",
        strongs=["H1234"],
        reference="Genesis 1:1",
        translation="KJV",
        book_id=1,
        chapter=1,
        verse=1,
    )
    assert vr.text == "clean"
    assert vr.strongs == ["H1234"]
    assert vr.book_id == 1
    assert vr.translation == "KJV"


# =============================================================================
# A3-12: Citation formatting
# =============================================================================

def test_lexicon_citation_normal():
    """lexicon_citation formats headword, abbrev, title."""
    result = lexicon_citation("logos", "THAYER", "Thayer's Greek Lexicon")
    assert "logos" in result
    assert "THAYER" in result
    assert "Thayer's Greek Lexicon" in result
    assert "\u2014" in result  # em-dash separator
    assert result.startswith('"logos"')


def test_lexicon_citation_none_headword():
    """lexicon_citation with None headword uses '(unknown)'."""
    result = lexicon_citation(None, "ABBREV", "Title")
    assert "(unknown)" in result
    assert "ABBREV" in result


def test_lexicon_citation_empty_headword():
    """lexicon_citation with empty string headword uses '(unknown)'."""
    result = lexicon_citation("", "ABBREV", "Title")
    assert "(unknown)" in result


def test_lexicon_citation_special_chars():
    """lexicon_citation handles headwords with quotes/special chars."""
    result = lexicon_citation("he said", "BDAG", "Bauer-Danker Lexicon")
    assert '"he said"' in result


def test_lexicon_citation_markdown_normal():
    """lexicon_citation_markdown formats as markdown heading + emphasis."""
    result = lexicon_citation_markdown("logos", "THAYER", "Thayer's Greek Lexicon")
    assert result.startswith("## logos\n")
    assert "*THAYER" in result
    assert "Thayer's Greek Lexicon*" in result
    assert result.endswith("\n\n")


def test_lexicon_citation_markdown_none_headword():
    """lexicon_citation_markdown with None headword uses '(unknown)'."""
    result = lexicon_citation_markdown(None, "ABBREV", "Title")
    assert "## (unknown)" in result


def test_lexicon_citation_markdown_escapes_hash():
    """lexicon_citation_markdown escapes # in headwords."""
    result = lexicon_citation_markdown("#something", "ABBREV", "Title")
    assert "## \\#something" in result


def test_commentary_citation_with_section():
    """commentary_citation with section title includes all parts."""
    result = commentary_citation(
        "Introduction to Genesis",
        "CALVIN",
        "Calvin's Commentaries",
        "Genesis 1:1",
    )
    assert "Introduction to Genesis" in result
    assert "CALVIN" in result
    assert "Calvin's Commentaries" in result
    assert "Genesis 1:1" in result


def test_commentary_citation_no_section():
    """commentary_citation with no section title falls back to reference-only."""
    result = commentary_citation(
        None, "CALVIN", "Calvin's Commentaries", "Genesis 1:1",
    )
    assert "Genesis 1:1" in result
    assert "CALVIN" in result
    # Should NOT have quote marks around the label when section_title is None
    assert not result.startswith('"')


def test_commentary_citation_empty_section():
    """commentary_citation with empty string section_title falls back to reference."""
    result = commentary_citation(
        "", "CALVIN", "Calvin's Commentaries", "Genesis 1:1",
    )
    assert "Genesis 1:1" in result
    assert not result.startswith('"')


# =============================================================================
# Additional integration: name resolution edge cases
# =============================================================================

def test_resolve_book_case_insensitive():
    """resolve_book is case-insensitive."""
    assert resolve_book("genesis") is not None
    assert resolve_book("GENESIS") is not None
    assert resolve_book("Genesis") is not None
    assert resolve_book("GeNeSiS") is not None
    # All resolve to Gen
    for name in ["genesis", "GENESIS", "Genesis", "GeNeSiS"]:
        book = resolve_book(name)
        assert book is not None
        assert book.osis == "Gen", f"resolve_book({name!r}).osis = {book.osis}"


def test_resolve_book_spaceless():
    """resolve_book handles spaceless names like 'SongofSolomon'."""
    book = resolve_book("1Corinthians")
    assert book is not None
    assert book.osis == "1Cor"


def test_resolve_book_unknown():
    """resolve_book returns None for unknown names."""
    assert resolve_book("") is None
    assert resolve_book("   ") is None
    assert resolve_book("zzzzznotabook") is None


def test_resolve_book_via_osis():
    """resolve_book resolves OSIS codes directly."""
    assert resolve_book("Gen") is not None
    assert resolve_book("gen") is not None
    assert resolve_book("Exod") is not None
    assert resolve_book("Ps") is not None
    assert resolve_book("Matt") is not None
    assert resolve_book("Rev") is not None


def test_resolve_book_via_paratext():
    """resolve_book resolves Paratext codes."""
    assert resolve_book("GEN") is not None
    assert resolve_book("EXO") is not None
    assert resolve_book("PSA") is not None
    assert resolve_book("MAT") is not None
    assert resolve_book("REV") is not None


# =============================================================================
# Edge-case: get_verse_range with invalid references
# =============================================================================

def test_get_verse_range_invalid_reference():
    """get_verse_range with garbage reference returns empty list."""
    results = get_verse_range("not-a-verse-reference")
    assert results == []


def test_get_verse_range_empty_string():
    """get_verse_range with empty string returns empty list."""
    results = get_verse_range("")
    assert results == []
