"""Snapshot tests for apparatus v2 JSON decoding outputs.

Uses syrupy for snapshot comparison.  Pass --snapshot-update to regenerate.

Tests cover:
  - File integrity (valid JSON, expected structure)
  - Key verse snapshots (known-correct content from each module)
  - Encoding correctness (specific byte -> Unicode mappings)
  - No-regression round-trip (all decoded_text fields can be re-decoded)
  - Sigla presence (known apparatus sigla appear in expected modules)
"""

from __future__ import annotations

import json
from pathlib import Path
from typing import Any

import pytest

pytestmark = pytest.mark.snapshot

DATA_DIR = Path(__file__).resolve().parent.parent / "data"

MODULES = {
    "NA28": DATA_DIR / "NA28_apparatus_text_v2.json",
    "LXX": DATA_DIR / "LXX_apparatus_text_v2.json",
    "LXX2": DATA_DIR / "LXX2_apparatus_text_v2.json",
    "MT-ETCBC": DATA_DIR / "MT-ETCBC_apparatus_text_v2.json",
}


# ═══════════════════════════════════════════════════════════════════════
# Module-level fixtures (cached)
# ═══════════════════════════════════════════════════════════════════════


def _load_json(path: Path) -> dict[str, Any]:
    with open(path, encoding="utf-8") as f:
        return json.load(f)  # type: ignore[no-any-return]


@pytest.fixture(scope="module")
def na28_data() -> dict[str, Any]:
    return _load_json(MODULES["NA28"])


@pytest.fixture(scope="module")
def lxx_data() -> dict[str, Any]:
    return _load_json(MODULES["LXX"])


@pytest.fixture(scope="module")
def lxx2_data() -> dict[str, Any]:
    return _load_json(MODULES["LXX2"])


@pytest.fixture(scope="module")
def mt_data() -> dict[str, Any]:
    return _load_json(MODULES["MT-ETCBC"])


# ═══════════════════════════════════════════════════════════════════════
# 1. File integrity tests
# ═══════════════════════════════════════════════════════════════════════


@pytest.mark.parametrize(
    "module_name,expected_name,total_key",
    [
        ("NA28", "NA28", "total_entries"),
        ("LXX", "LXX", "total_entries"),
        ("LXX2", "LXX2", "total_entries"),
        ("MT-ETCBC", None, "_metadata"),  # MT-ETCBC has different structure
    ],
)
def test_file_loads_valid_json(module_name, expected_name, total_key, snapshot):
    """Each v2 JSON file loads as valid JSON with expected structure."""
    data = _load_json(MODULES[module_name])
    if expected_name is not None:
        assert data.get("name") == expected_name
    assert total_key in data
    # Snapshot the metadata keys (not the full content)
    meta = {k: v for k, v in data.items() if k != "entries" and k != "_metadata"}
    if "_metadata" in data:
        meta["_metadata_keys"] = list(data["_metadata"].keys())
    assert meta == snapshot


def test_na28_entry_count(na28_data, snapshot):
    """NA28 v2 has expected number of entries."""
    assert len(na28_data["entries"]) == snapshot


def test_lxx_entry_count(lxx_data, snapshot):
    """LXX v2 has expected number of entries."""
    assert len(lxx_data["entries"]) == snapshot


def test_lxx2_entry_count(lxx2_data, snapshot):
    """LXX2 v2 has expected number of entries."""
    assert len(lxx2_data["entries"]) == snapshot


def test_mt_etbc_verse_count(mt_data, snapshot):
    """MT-ETCBC-A v2 has expected number of verse references."""
    verse_keys = [k for k in mt_data if k.startswith("Cp ")]
    assert len(verse_keys) == snapshot


def test_na28_entries_have_decoded_text(na28_data):
    """Every NA28 v2 entry has a decoded_text field."""
    for entry in na28_data["entries"]:
        assert "decoded_text" in entry, f"Entry {entry.get('index')} missing decoded_text"


def test_lxx_entries_have_decoded_text(lxx_data):
    """Every LXX v2 entry has a decoded_text field."""
    for entry in lxx_data["entries"]:
        assert "decoded_text" in entry, f"Entry {entry.get('index')} missing decoded_text"


def test_lxx2_entries_have_decoded_text(lxx2_data):
    """Every LXX2 v2 entry has a decoded_text field."""
    for entry in lxx2_data["entries"]:
        assert "decoded_text" in entry, f"Entry {entry.get('index')} missing decoded_text"


# ═══════════════════════════════════════════════════════════════════════
# 2. Key verse snapshots
# ═══════════════════════════════════════════════════════════════════════


def _find_entry_containing(entries: list[dict], substring: str) -> dict | None:
    """Find the first entry whose decoded_text contains *substring*."""
    for entry in entries:
        if substring in entry.get("decoded_text", ""):
            return entry
    return None


def test_na28_contains_evangelion_cata_matthaion(na28_data, snapshot):
    """NA28 apparatus includes the Greek title 'ευαγγελιον κατα Ματθαιον'.

    Found in entry 451 (Matt 1:1 per verse index).
    """
    entries = na28_data["entries"]
    found = _find_entry_containing(entries, "ευαγγελιον κατα Ματθαιον")
    assert found is not None, (
        "Could not find entry containing 'ευαγγελιον κατα Ματθαιον' in NA28 decoded text"
    )
    # Snapshot the relevant portion of the entry
    dt = found["decoded_text"]
    idx = dt.lower().find("ματθαιον")
    snippet = dt[max(0, idx - 40) : idx + 100]
    assert snippet == snapshot


def test_lxx_gen1_11_kata_genos(lxx_data, snapshot):
    """LXX apparatus for Gen 1:11 contains 'κατα γενος'."""
    entries = lxx_data["entries"]
    found = _find_entry_containing(entries, "κατα γενος")
    assert found is not None, "Could not find entry containing κατα γενος"
    dt = found["decoded_text"]
    idx = dt.lower().find("κατα γενος")
    snippet = dt[max(0, idx - 30) : idx + 80]
    assert snippet == snapshot


def test_mt_etcbc_gen1_30_nonn_mss_fraktur_g(mt_data, snapshot):
    """MT-ETCBC-A Gen 1:30 contains 'nonn Mss 𝔊'."""
    refs = [k for k in mt_data if "1,30" in k and k.startswith("Cp ")]
    assert refs, "Could not find Gen 1:30 (Cp 1,30) in MT-ETCBC-A data"
    verse_data = mt_data[refs[0]]
    notes = verse_data.get("notes", [])
    # Combine all notes for the snapshot
    combined = " || ".join(f"{n['letter']}: {n['text']}" for n in notes)
    assert combined == snapshot


# ═══════════════════════════════════════════════════════════════════════
# 3. Encoding correctness
# ═══════════════════════════════════════════════════════════════════════


def test_byte_fd_maps_to_fraktur_g(snapshot):
    """Byte 0xFD maps to Fraktur G (𝔊, U+1D50A)."""
    from accordance_parser.apparatus_charset import APPARATUS_SIGLA_MAP

    assert 0xFD in APPARATUS_SIGLA_MAP, "0xFD not in sigla map"
    result = APPARATUS_SIGLA_MAP[0xFD]
    assert result == "\U0001d50a"  # 𝔊
    assert hex(ord(result)) == snapshot


def test_byte_e3_context_dependent(na28_data, mt_data, snapshot):
    """Byte 0xE3 has context-dependent decoding (⅏ or א)."""
    from accordance_parser.apparatus_charset import APPARATUS_SIGLA_MAP, HEBREW_MAP

    sigla_value = APPARATUS_SIGLA_MAP.get(0xE3)
    hebrew_value = HEBREW_MAP.get(0xE3)
    assert sigla_value == "\u214f", f"Expected ⅏ for 0xE3 sigla, got {sigla_value}"
    assert hebrew_value == "\u05d0", f"Expected א for 0xE3 Hebrew, got {hebrew_value}"
    # Snapshot both possible mappings
    assert {"sigla": sigla_value, "hebrew": hebrew_value} == snapshot


def test_byte_ea_context_dependent(na28_data, mt_data, snapshot):
    """Byte 0xEA has context-dependent decoding (𝔖 or א)."""
    from accordance_parser.apparatus_charset import APPARATUS_SIGLA_MAP, HEBREW_MAP

    sigla_value = APPARATUS_SIGLA_MAP.get(0xEA)
    hebrew_value = HEBREW_MAP.get(0xEA)
    assert sigla_value == "\U0001d516", f"Expected 𝔖 for 0xEA sigla, got {sigla_value}"
    assert hebrew_value == "\u05d0", f"Expected א for 0xEA Hebrew, got {hebrew_value}"
    assert {"sigla": sigla_value, "hebrew": hebrew_value} == snapshot


# ═══════════════════════════════════════════════════════════════════════
# 4. No-regression round-trip tests
# ═══════════════════════════════════════════════════════════════════════


def _all_entries(data: dict[str, Any]) -> list[dict[str, Any]]:
    return data.get("entries", [])


@pytest.mark.parametrize(
    "module_name",
    ["NA28", "LXX", "LXX2"],
)
def test_decoded_text_no_unmapped_hex_placeholders(module_name):
    """No decoded_text in any entry contains unmapped hex placeholders [XX]."""
    import re

    # Only match two-digit hex values >= 0x80 (unmapped high bytes).
    # Values like [21] are legitimate verse/apparatus reference markers.
    _hex_placeholder = re.compile(r"\[[89A-F][0-9A-F]\]", re.IGNORECASE)
    data = _load_json(MODULES[module_name])
    for entry in _all_entries(data):
        dt = entry.get("decoded_text", "")
        matches = _hex_placeholder.findall(dt)
        assert not matches, (
            f"{module_name} entry {entry.get('index')}: found unmapped hex placeholder(s) "
            f"{matches}"
        )


@pytest.mark.parametrize(
    "module_name",
    ["NA28", "LXX", "LXX2"],
)
def test_decoded_text_no_control_chars(module_name):
    """No decoded_text contains C0 control characters except \\n, \\r, \\t."""
    import re

    # Match C0 controls except tab, newline, carriage return
    _bad_ctrl = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f]")
    data = _load_json(MODULES[module_name])
    for entry in _all_entries(data):
        dt = entry.get("decoded_text", "")
        m = _bad_ctrl.search(dt)
        assert m is None, (
            f"{module_name} entry {entry.get('index')}: "
            f"control character U+{ord(m.group(0)):04X} found in decoded_text"
        )


@pytest.mark.parametrize(
    "module_name",
    ["NA28", "LXX", "LXX2"],
)
def test_decoded_text_is_valid_unicode(module_name):
    """All decoded_text values are valid Unicode strings (no surrogate errors)."""
    data = _load_json(MODULES[module_name])
    for entry in _all_entries(data):
        dt = entry.get("decoded_text", "")
        # This will raise if there are lone surrogates
        try:
            dt.encode("utf-8")
        except UnicodeEncodeError as e:
            pytest.fail(
                f"{module_name} entry {entry.get('index')}: invalid Unicode: {e}"
            )


# ═══════════════════════════════════════════════════════════════════════
# 5. Sigla presence tests
# ═══════════════════════════════════════════════════════════════════════


SIGLA_CHECKS: dict[str, set[str]] = {
    # sigla_char -> set of module names where it should appear
    "\U0001d50a": {"MT-ETCBC"},  # 𝔊 (Fraktur G) — used in Hebrew apparatus
    "\U0001d516": {"MT-ETCBC"},  # 𝔖 (Fraktur S) — used in Hebrew apparatus
    "\u214f": {"MT-ETCBC"},  # ⅏ (Samaritan source) — Hebrew apparatus note-start
    "\u00b0": {"MT-ETCBC"},  # ° (degree/apparatus marker) — Hebrew apparatus
}


def _all_decoded_texts(data: dict[str, Any]) -> str:
    """Concatenate all decoded text fields into one big string for fast searching."""
    parts: list[str] = []
    if "entries" in data:
        for entry in data["entries"]:
            parts.append(entry.get("decoded_text", ""))
    else:
        # MT-ETCBC format: iterate Cp keys
        for key, value in data.items():
            if key.startswith("Cp "):
                for note in value.get("notes", []):
                    parts.append(note.get("text", ""))
    return " ".join(parts)


@pytest.mark.parametrize(
    "sigla_char,expected_present",
    [
        (char, mods)
        for char, mods in SIGLA_CHECKS.items()
    ],
)
def test_sigla_presence(sigla_char, expected_present):
    """Known sigla characters appear in expected modules."""
    for module_name in expected_present:
        data = _load_json(MODULES[module_name])
        all_text = _all_decoded_texts(data)
        assert sigla_char in all_text, (
            f"Sigla {sigla_char!r} (U+{ord(sigla_char):04X}) not found in {module_name}"
        )


# ═══════════════════════════════════════════════════════════════════════
# 6. MT-ETCBC-A specific structural tests
# ═══════════════════════════════════════════════════════════════════════


def test_mt_etbc_has_metadata(mt_data, snapshot):
    """MT-ETCBC-A v2 has _metadata with expected keys."""
    meta = mt_data.get("_metadata", {})
    assert "total_verses" in meta
    assert meta == snapshot


def test_mt_etbc_notes_structure(mt_data):
    """MT-ETCBC-A notes have expected letter + text structure."""
    verse_keys = [k for k in mt_data if k.startswith("Cp ")]
    # Sample the first 50 verses
    for key in verse_keys[:50]:
        verse_data = mt_data[key]
        assert "notes" in verse_data, f"{key} missing notes"
        for note in verse_data["notes"]:
            assert "letter" in note, f"{key} note missing letter"
            assert "text" in note, f"{key} note missing text"
            assert isinstance(note["letter"], str) and len(note["letter"]) == 1
            assert isinstance(note["text"], str)


def test_mt_etbc_contains_fraktur_g_in_gen_verse(mt_data):
    """MT-ETCBC-A contains 𝔊 (Fraktur G) in Genesis verses."""
    # Check first 50 Genesis verses (Cp 1,X)
    gen_verses = sorted(
        [k for k in mt_data if k.startswith("Cp 1,")],
        key=lambda k: int(k.replace("Cp 1,", "")),
    )[:50]
    found = False
    for key in gen_verses:
        for note in mt_data[key].get("notes", []):
            if "\U0001d50a" in note["text"]:
                found = True
                break
        if found:
            break
    assert found, "𝔊 (Fraktur G) not found in first 50 Genesis verses of MT-ETCBC-A"
