"""
Comprehensive tests for interlinear alignment, book traversal, sparse
Strong's data, mixed-script output, and encoding round-trips.

Tests:
  1. Word-to-Strong's alignment (B2-1)
  2. Book traversal — Obadiah (B2-4)
  3. Sparse Strong's data handling (B2-5)
  4. Mixed-script output validation (B2-7)
  5. Encoding round-trip (B2-11)

All tests operate on real Accordance module binaries.
"""

import re
from pathlib import Path

import pytest

from accordance_parser.text_module import TextModule
from accordance_parser.canon import CANON_PROTESTANT, last_verse_of_chapter, chapter_count


# ── Module paths ────────────────────────────────────────────────────────────

_ESVS_PATH = Path(
    "/Users/jacobgruber/Projects/Accordance Port/modules/Texts/ESVS.atext/ESVS"
)
_BDAG_PATH = Path(
    "/Users/jacobgruber/Projects/Accordance Port/modules/Tools/BDAG.atool/BDAG"
)
_BDB_PATH = Path(
    "/Users/jacobgruber/Projects/Accordance Port/modules/Tools/BDB Complete.atool/BDB Complete"
)

ESVS_EXISTS = _ESVS_PATH.is_file()
BDAG_EXISTS = _BDAG_PATH.is_file()
BDB_EXISTS = _BDB_PATH.is_file()


# ── Module-scoped fixtures ──────────────────────────────────────────────────

@pytest.fixture(scope="module")
def esvs():
    """Load the ESVS TextModule once for all tests."""
    if not ESVS_EXISTS:
        pytest.skip("ESVS module file not found")
    return TextModule(str(_ESVS_PATH), module_abbr="ESVS")


@pytest.fixture(scope="module")
def bdag():
    """Load the BDAG lexicon module once for all tests."""
    if not BDAG_EXISTS:
        pytest.skip("BDAG module file not found")
    from accordance_parser.lexicon_module import LexiconModule
    return LexiconModule(str(_BDAG_PATH))


@pytest.fixture(scope="module")
def bdb():
    """Load the BDB lexicon module once for all tests."""
    if not BDB_EXISTS:
        pytest.skip("BDB module file not found")
    from accordance_parser.lexicon_module import LexiconModule
    return LexiconModule(str(_BDB_PATH))


# =============================================================================
# 1. Word-to-Strong's alignment (B2-1)
# =============================================================================

class TestWordToStrongsAlignment:
    """Verify Strong's word_ids align correctly with verse text tokens."""

    def test_genesis_1_1_word_ids_nonempty(self, esvs):
        """Genesis 1:1 get_verse_with_strongs should return non-empty word_ids."""
        result = esvs.get_verse_with_strongs("Genesis 1:1")
        assert result is not None, "get_verse_with_strongs('Gen 1:1') returned None"
        text, word_ids = result
        assert len(text) > 0, "Genesis 1:1 text is empty"
        assert len(word_ids) > 0, (
            f"Expected non-empty word_ids from Gen 1:1, got {len(word_ids)}"
        )

    def test_strongs_count_le_word_count(self, esvs):
        """Strong's token count should not exceed whitespace-split word count.

        Punctuation and conjunctions may lack Strong's numbers, so the
        Strong's count should be ≤ the word count — never greater.

        Uses Genesis 12:1 (WMAP path) rather than Gen 1:1 (entry-block path)
        because Gen 1:1 returns raw uint24 entry-block tokens, not per-word
        Strong's numbers.
        """
        result = esvs.get_verse_with_strongs("Genesis 12:1")
        assert result is not None
        text, word_ids = result

        word_tokens = text.split()
        word_count = len(word_tokens)

        # If no word_ids are available (sparse data), skip the comparison
        if len(word_ids) > 0:
            strongs_count = len(word_ids)
            assert strongs_count <= word_count, (
                f"Strong's count ({strongs_count}) exceeds word count ({word_count}) "
                f"in Gen 12:1. word_ids: {word_ids}"
            )

    def test_john_1_1_g_prefix(self, esvs):
        """NT verse (John 1:1) should yield G-prefix Strong's entries."""
        result = esvs.get_verse_with_strongs("John 1:1")
        assert result is not None, "get_verse_with_strongs('John 1:1') returned None"
        text, word_ids = result
        assert len(text) > 0, "John 1:1 text is empty"

        # At minimum, the text should be returned even if word_ids is sparse
        # If word_ids are present, resolve them and check for G-prefix
        strongs_list = esvs.get_verse_strongs_list("John 1:1")
        if strongs_list and len(strongs_list) > 0:
            all_strongs = set()
            for _u16, snums, _wtype in strongs_list:
                all_strongs.update(snums)
            g_entries = [s for s in all_strongs if s.startswith("G")]
            if all_strongs:
                assert len(g_entries) > 0, (
                    f"Expected G-prefix Strong's in John 1:1, got: {all_strongs}"
                )

    def test_strongs_format_matches_pattern(self, esvs):
        """All Strong's entries should match [HG]\\d{4} pattern."""
        strongs = esvs.get_verse_strongs_list("Genesis 12:1")
        assert strongs is not None
        assert len(strongs) > 0, "Genesis 12:1 strongs list is empty"

        for _u16, snums, _wtype in strongs:
            for s in snums:
                assert re.match(r"^[HG]\d{4}$", s), (
                    f"Strong's {s!r} does not match [HG]\\d{{4}} pattern "
                    f"(u16_lo={_u16})"
                )


# =============================================================================
# 2. Book traversal — Obadiah (B2-4)
# =============================================================================

class TestObadiahTraversal:
    """Verify full book traversal for a single-chapter book."""

    def test_obadiah_canon_data(self):
        """Obadiah should have exactly 1 chapter and 21 verses per canon."""
        obad_chapter_count = chapter_count("Obad")
        assert obad_chapter_count == 1, (
            f"Expected Obadiah to have 1 chapter, got {obad_chapter_count}"
        )

        last_v = last_verse_of_chapter("Obad", 1, CANON_PROTESTANT)
        assert last_v == 21, (
            f"Expected Obadiah ch 1 to have 21 verses, got {last_v}"
        )

    def test_all_obadiah_verses_nonempty(self, esvs):
        """Every verse of Obadiah (1:1 through 1:21) should return text."""
        for v in range(1, 22):
            text = esvs.get_verse(f"Obadiah 1:{v}")
            assert text is not None, f"Obadiah 1:{v} returned None"
            assert len(text) > 0, (
                f"Obadiah 1:{v} returned empty string"
            )

    def test_obadiah_first_verse(self, esvs):
        """Obadiah 1:1 should return recognizably non-empty text about Edom."""
        text = esvs.get_verse("Obadiah 1:1")
        assert text is not None
        text_lower = text.lower()
        # ESVS text may have footnote markers stripped from "The vision of
        # Obadiah", yielding variants like "Thus says the Lord God concerning
        # Edom". Verify it's about the Lord or Edom.
        assert "lord" in text_lower or "edom" in text_lower, (
            f"Obadiah 1:1 does not contain 'Lord' or 'Edom': {text!r}"
        )

    def test_obadiah_last_verse(self, esvs):
        """Obadiah verse 21 should return recognizably non-empty text."""
        text = esvs.get_verse("Obadiah 1:21")
        assert text is not None, "Obadiah 1:21 returned None"
        assert len(text) > 5, (
            f"Obadiah 1:21 text suspiciously short: {text!r}"
        )
        # ESVS text may be truncated; verify it has multiple words and is
        # clearly biblical content (mentions possess, Esau, Negeb, or similar)
        text_lower = text.lower()
        has_content = any(
            word in text_lower
            for word in ["possess", "esau", "negeb", "mount", "zion", "kingdom", "lord"]
        )
        assert has_content, (
            f"Obadiah 1:21 expected recognizable biblical content: {text!r}"
        )


# =============================================================================
# 3. Sparse Strong's data handling (B2-5)
# =============================================================================

class TestSparseStrongs:
    """Verify graceful degradation when Strong's data is sparse or missing."""

    def test_jesus_wept_has_text(self, esvs):
        """John 11:35 ('Jesus wept') — text must always be returned.

        This is the shortest verse in the Bible (2 English words). Strong's
        data may have 0–2 entries; either way the text must be non-empty.
        """
        result = esvs.get_verse_with_strongs("John 11:35")
        assert result is not None, "John 11:35 returned None"
        text, word_ids = result
        assert len(text) > 0, "John 11:35 text is empty"
        assert "wept" in text.lower() or "Jesus" in text, (
            f"John 11:35 expected 'Jesus wept': {text!r}"
        )
        # word_ids may be 0-2 entries; no assertion on count, just that
        # the function doesn't crash and text is returned

    def test_jesus_wept_strongs_limited(self, esvs):
        """John 11:35 word_ids should have at most 2 entries (graceful)."""
        result = esvs.get_verse_with_strongs("John 11:35")
        assert result is not None
        _text, word_ids = result
        assert len(word_ids) <= 2, (
            f"John 11:35 has only 2 words; expected ≤2 word_ids, got {len(word_ids)}: "
            f"{word_ids}"
        )

    def test_genealogy_verse_nonempty(self, esvs):
        """1 Chronicles 1:1 — genealogy verse must return non-empty text."""
        text = esvs.get_verse("1 Chronicles 1:1")
        assert text is not None, "1 Chronicles 1:1 returned None"
        assert len(text) > 0, (
            "1 Chronicles 1:1 returned empty string"
        )


# =============================================================================
# 4. Mixed-script output validation (B2-7)
# =============================================================================

class TestMixedScriptOutput:
    """Verify decoded output contains correct Unicode scripts and no artifacts."""

    GREEK_RANGE = (0x0370, 0x03FF)
    GREEK_EXTENDED = (0x1F00, 0x1FFF)
    HEBREW_RANGE = (0x05D0, 0x05EA)

    def _has_greek(self, text: str) -> bool:
        """Return True if text contains Greek-range characters."""
        return any(
            self.GREEK_RANGE[0] <= ord(c) <= self.GREEK_RANGE[1] or
            self.GREEK_EXTENDED[0] <= ord(c) <= self.GREEK_EXTENDED[1]
            for c in text
        )

    def _has_hebrew(self, text: str) -> bool:
        """Return True if text contains Hebrew-range characters."""
        return any(
            self.HEBREW_RANGE[0] <= ord(c) <= self.HEBREW_RANGE[1]
            for c in text
        )

    def _has_english(self, text: str) -> bool:
        """Return True if text contains ASCII Latin letters."""
        return any(c.isascii() and c.isalpha() for c in text)

    def _has_c1_controls(self, text: str) -> bool:
        """Return True if text contains C1 control chars (0x80–0x9F)."""
        return any(0x80 <= ord(c) <= 0x9F for c in text)

    def test_bdag_logos_output_has_greek_and_english(self, bdag):
        """BDAG lookup of λόγος should contain Greek AND English.

        BDAG uses Unicode Greek headwords, not Strong's numbers.
        """
        result = bdag.lookup("λόγος")
        assert result is not None, "BDAG lookup('λόγος') returned None"
        assert len(result) > 0, "BDAG λόγος output is empty"
        assert self._has_greek(result), (
            f"BDAG λόγος output should contain Greek characters: {result[:200]!r}"
        )
        assert self._has_english(result), (
            f"BDAG λόγος output should contain English text: {result[:200]!r}"
        )

    def test_bdb_h1254_output_has_hebrew_and_english(self, bdb):
        """BDB lookup of H1254 (בָּרָא) should contain Hebrew AND English."""
        result = bdb.lookup("H1254")
        assert result is not None, "BDB lookup('H1254') returned None"
        assert len(result) > 0, "BDB H1254 output is empty"
        assert self._has_hebrew(result), (
            f"BDB H1254 output should contain Hebrew characters: {result[:200]!r}"
        )
        assert self._has_english(result), (
            f"BDB H1254 output should contain English text: {result[:200]!r}"
        )

    def test_no_c1_control_chars_in_bdag_output(self, bdag):
        """BDAG output must not contain C1 control characters (0x80–0x9F)."""
        result = bdag.lookup("λόγος")
        assert result is not None
        assert not self._has_c1_controls(result), (
            f"BDAG λόγος output contains C1 control chars: "
            f"{[hex(ord(c)) for c in result if 0x80 <= ord(c) <= 0x9F][:10]!r}"
        )

    def test_no_c1_control_chars_in_bdb_output(self, bdb):
        """BDB output must not contain C1 control characters (0x80–0x9F)."""
        result = bdb.lookup("H1254")
        assert result is not None
        assert not self._has_c1_controls(result), (
            f"BDB H1254 output contains C1 control chars: "
            f"{[hex(ord(c)) for c in result if 0x80 <= ord(c) <= 0x9F][:10]!r}"
        )

    def test_utf8_roundtrip(self, esvs):
        """UTF-8 encode/decode round-trip must produce identical text."""
        text = esvs.get_verse("Genesis 1:1")
        assert text is not None
        encoded = text.encode("utf-8")
        decoded = encoded.decode("utf-8")
        assert decoded == text, (
            f"UTF-8 roundtrip failed: {text[:50]!r} → encoded → decoded → "
            f"{decoded[:50]!r}"
        )


# =============================================================================
# 5. Encoding round-trip (B2-11)
# =============================================================================

class TestEncodingRoundtrip:
    """Verify Accordance encoding → Unicode produces valid, clean output."""

    def test_decode_always_greek_diacritics(self):
        """decode_always(b\"\\xe1\\xc0\") should produce Greek with diacritics.

        Byte sequence: 0xE1 = rough breathing (on preceding alpha), 0xC0 = acute.
        The byte pair represents alpha with rough breathing + acute accent.
        """
        from accordance_parser.encoding import decode_always

        # 0xE1 = rough breathing on preceding letter
        # 0xC0 = acute on preceding letter
        # Test with alpha: 'a' (0x61) → α, then 0xE1 0xC0 for rough+acute
        result = decode_always(b"a\xe1\xc0")
        assert result is not None
        assert len(result) > 0, "decode_always returned empty string"
        # Should contain Greek polytonic characters (U+1F00-U+1FFF)
        has_polytonic = any(0x1F00 <= ord(c) <= 0x1FFF for c in result)
        has_basic_greek = any(0x0370 <= ord(c) <= 0x03FF for c in result)
        assert has_basic_greek or has_polytonic, (
            f"Expected Greek characters in decode_always output: {result!r}"
        )

    def test_decode_hebrew_niqqud(self):
        """decode_hebrew(b\"a\\xb6\") should produce Hebrew with vowel points.

        'a' → aleph (א), 0xB6 → qamats ( ָ ). Result should be אָ.
        """
        from accordance_parser.encoding import decode_hebrew

        result = decode_hebrew(b"a\xb6")
        assert result is not None
        assert len(result) > 0, "decode_hebrew returned empty string"
        # Should contain aleph (U+05D0) with combining marks
        has_aleph = "\u05d0" in result
        assert has_aleph, (
            f"Expected aleph (U+05D0) in decode_hebrew output: {result!r}"
        )
        # Should contain Hebrew characters
        has_hebrew = any(0x05D0 <= ord(c) <= 0x05EA for c in result)
        assert has_hebrew, (
            f"Expected Hebrew characters in decode_hebrew output: {result!r}"
        )

    def test_no_replacement_chars_greek(self):
        """decode_always output must not contain U+FFFD replacement characters."""
        from accordance_parser.encoding import decode_always

        result = decode_always(b"a\xe1\xc0")
        assert "\ufffd" not in result, (
            f"decode_always output contains U+FFFD replacement chars: {result!r}"
        )

    def test_no_replacement_chars_hebrew(self):
        """decode_hebrew output must not contain U+FFFD replacement characters."""
        from accordance_parser.encoding import decode_hebrew

        result = decode_hebrew(b"a\xb6")
        assert "\ufffd" not in result, (
            f"decode_hebrew output contains U+FFFD replacement chars: {result!r}"
        )

    def test_decode_generic_handles_mixed_bytes(self):
        """decode() on a byte mix of Greek diacritics + ASCII should produce
        valid output without errors."""
        from accordance_parser.encoding import decode

        # Mix: Greek letter 'a' + diacritic + English text
        result = decode(b"a\xe1\xc0 test")
        assert result is not None
        assert len(result) > 0
        assert "\ufffd" not in result


# =============================================================================
# Runner (for script-style execution)
# =============================================================================

if __name__ == "__main__":
    pytest.main([__file__, "-v", "--tb=short"])
