#!/usr/bin/env python3
"""Test de aceptación del calendario litúrgico 2026-2040 (issue #20).

Criterio acordado entre Codix/Opix/Claudix: cobertura bidireccional contra
las 500 entradas del libro, más un puñado de fechas móviles verificadas de
forma independiente (cómputo de Pascua propio, no la fuente del generador).

Uso: python3 tests/test_liturgical_calendar_coverage.py
Sale con exit code != 0 si algo falla.
"""
from __future__ import annotations

import json
import sys
from datetime import date, timedelta
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
CAL_PATH = ROOT / "data/evangelio-diario/calendario-liturgico-2026-2040.json"
BOOK_PATH = ROOT / "data/evangelio-diario/entradas-libro.json"


def easter_sunday(year: int) -> date:
    a = year % 19
    b = year // 100
    c = year % 100
    d_ = b // 4
    e = b % 4
    f = (b + 8) // 25
    g = (b - f + 1) // 3
    h = (19 * a + b - d_ - g + 15) % 30
    i = c // 4
    k = c % 4
    l = (32 + 2 * e + 2 * i - h - k) % 7
    m = (a + 11 * h + 22 * l) // 451
    month = (h + l - 7 * m + 114) // 31
    day = ((h + l - 7 * m + 114) % 31) + 1
    return date(year, month, day)


def fail(msg: str, errors: list[str]) -> None:
    errors.append(msg)


def main() -> int:
    errors: list[str] = []
    cal = json.loads(CAL_PATH.read_text(encoding="utf-8"))
    book = json.loads(BOOK_PATH.read_text(encoding="utf-8"))
    dates = cal["dates"]
    n_book = len(book)

    # 1) toda fecha del rango existe y tiene book_index valido
    d = date.fromisoformat(cal["range"]["from"])
    end = date.fromisoformat(cal["range"]["to"])
    n_dates = 0
    while d <= end:
        iso = d.isoformat()
        if iso not in dates:
            fail(f"fecha ausente: {iso}", errors)
        else:
            idx = dates[iso]["book_index"]
            if not (1 <= idx <= n_book):
                fail(f"{iso}: book_index fuera de rango: {idx}", errors)
            n_dates += 1
        d += timedelta(days=1)

    # 2) toda entrada del libro es alcanzable en algun año del rango
    used = {v["book_index"] for v in dates.values()}
    never_used = sorted(set(range(1, n_book + 1)) - used)
    # las 3 excepciones conocidas y documentadas en el propio JSON (ver "notes")
    known_gaps = {34, 62, 134}
    unexpected_gaps = [i for i in never_used if i not in known_gaps]
    if unexpected_gaps:
        fail(f"entradas del libro nunca alcanzadas (no documentadas): {unexpected_gaps}", errors)

    # 3) fechas moviles clave, computadas de forma independiente (no depende
    #    del propio generador ni de su fuente CEE)
    checks_ok = 0
    for year in range(2026, 2041):
        easter = easter_sunday(year)
        expects = [
            (easter, 199, "Pascua"),
            (easter - timedelta(days=46), 141, "Ceniza"),
            (easter + timedelta(days=49), 258, "Pentecostes"),
        ]
        for dt, expected_idx, label in expects:
            v = dates.get(dt.isoformat())
            got = v["book_index"] if v else None
            if got != expected_idx:
                fail(f"{year} {label} {dt}: esperado idx={expected_idx}, obtenido={got}", errors)
            else:
                checks_ok += 1
        palm = easter - timedelta(days=7)
        v = dates.get(palm.isoformat())
        if not (v and 190 <= v["book_index"] <= 192):
            fail(f"{year} Ramos {palm}: obtenido={v}", errors)
        else:
            checks_ok += 1
        trinity = easter + timedelta(days=56)
        v = dates.get(trinity.isoformat())
        if not (v and 259 <= v["book_index"] <= 261):
            fail(f"{year} Trinidad {trinity}: obtenido={v}", errors)
        else:
            checks_ok += 1

    # 4) solemnidades con clave propia, todos los años (no diluidas en un
    #    domingo/feria generico) -- el bug central del commit retirado 2713e13
    for year in range(2026, 2041):
        ep = date(year, 1, 6)
        baptism = (ep + timedelta(days=1)) if ep.weekday() == 6 else (
            ep + timedelta(days=(6 - ep.weekday()) % 7)
        )
        v = dates.get(baptism.isoformat())
        if not (v and 54 <= v["book_index"] <= 56):
            fail(f"{year} Bautismo {baptism}: sin clave propia ({v})", errors)

        easter = easter_sunday(year)
        ascension = easter + timedelta(days=42)
        v = dates.get(ascension.isoformat())
        if not (v and 249 <= v["book_index"] <= 251):
            fail(f"{year} Ascension {ascension}: sin clave propia ({v})", errors)

        corpus = easter + timedelta(days=63)
        v = dates.get(corpus.isoformat())
        if not (v and 262 <= v["book_index"] <= 264):
            fail(f"{year} Corpus {corpus}: sin clave propia o no en domingo ({v})", errors)

    years_with_ck = {k[:4] for k, v in dates.items() if 481 <= v["book_index"] <= 483}
    if len(years_with_ck) != 15:
        fail(f"Cristo Rey ausente en algun año: {sorted(years_with_ck)}", errors)

    years_with_family = {k[:4] for k, v in dates.items() if v["book_index"] == 40}
    if len(years_with_family) != 15:
        fail(f"Sagrada Familia ausente en algun año: {sorted(years_with_family)}", errors)

    # 5) Adviento no debe seguir contando tras Navidad (fuga del commit retirado)
    leak = [k for k, v in dates.items()
            if k[5:7] == "12" and int(k[8:10]) >= 26 and v["book_index"] <= 37]
    if leak:
        fail(f"fuga de Adviento tras Navidad: {leak[:10]}", errors)

    print(f"fechas verificadas: {n_dates}")
    print(f"book_index distintos usados: {len(used)}/{n_book}")
    print(f"chequeos de fechas moviles ok: {checks_ok}")
    if errors:
        print(f"\nFALLOS ({len(errors)}):")
        for e in errors:
            print(" -", e)
        return 1
    print("\nTodo OK.")
    return 0


if __name__ == "__main__":
    sys.exit(main())
