"""T7: tasas vigentes (BCE y FOMC) y el punto medio del rango; parseo estricto de las tablas."""

from datetime import date

import pytest
from estudio01 import rates
from synth import STAR_DATE, STAR_NOTE, ecb_text, fomc_html

D = date


def ecb(*pairs: tuple[str, str]) -> tuple[rates.EcbObservation, ...]:
    return rates.parse_ecb(ecb_text(list(pairs)))


def decisions(tables) -> tuple[rates.Decision, ...]:
    """Las decisiones en el orden de la página (años descendentes)."""
    return rates.parse_fomc(fomc_html(tables))


def merged(tables) -> tuple[rates.Decision, ...]:
    """Las decisiones ya unidas y en orden cronológico, como las usa el estudio."""
    return rates.merge_decisions(decisions(tables))


def test_t7_ecb_effective_date_equal_to_the_bar_date_applies_that_day():
    """BCE con fecha de entrada en vigor igual a la fecha de t: aplica en t."""
    series = ecb(("2003-06-06", "1"), ("2003-06-05", "2"), ("1999-01-01", "3"))
    assert rates.ecb_rate_at(series, D(2003, 6, 6)) == 0.01  # el día del cambio ya rige
    assert rates.ecb_rate_at(series, D(2003, 6, 5)) == 0.02  # la víspera rige la anterior
    assert rates.ecb_rate_at(series, D(2003, 6, 7)) == 0.01


def test_t7_fomc_decision_date_equal_to_the_bar_date_applies_from_the_next_bar():
    """FOMC con fecha de decisión igual a la fecha de t: NO aplica en t, aplica en t + 1."""
    series = merged({2003: [("June 25", "1.00")], 2002: [("November 6", "1.25")]})
    assert rates.fomc_target_at(series, D(2003, 6, 25)) == 0.0125  # el mismo día rige la anterior
    assert rates.fomc_target_at(series, D(2003, 6, 26)) == 0.01  # desde el fijo siguiente
    assert rates.fomc_target_at(series, D(2003, 6, 24)) == 0.0125


def test_t7_midpoint_of_a_range_is_a_fraction():
    """Punto medio de `0-0.25` = 0.00125 como fracción; los demás rangos, igual."""
    assert rates.parse_level("0-0.25", D(2008, 12, 16)) == 0.00125  # oraculo T7
    assert rates.parse_level("3.75-4.00", D(2026, 9, 17)) == 0.03875
    assert rates.parse_level("0.25 - 0.50", D(2015, 12, 16)) == 0.00375
    assert rates.parse_level("7.00", D(1990, 12, 18)) == 0.07
    assert rates.parse_level("1.25", D(2002, 11, 6)) == 0.0125


def test_range_only_from_2008_12_16_and_single_value_only_before():
    with pytest.raises(rates.RatesFormatError):
        rates.parse_level("0-0.25", D(2008, 12, 15))  # un rango antes de la fecha
    with pytest.raises(rates.RatesFormatError):
        rates.parse_level("0.25", D(2008, 12, 16))  # un valor único desde la fecha
    assert rates.parse_level("0-0.25", D(2008, 12, 16)) == 0.00125


@pytest.mark.parametrize("level", ["", "n.a.", "1,25", "0.25-", "-0.25", "0.50-0.25", "5.25-5.50-"])
def test_levels_with_an_uncovered_format_stop_the_code(level):
    with pytest.raises(rates.RatesFormatError):
        rates.parse_level(level, D(2020, 1, 1))


def test_fomc_decisions_are_read_from_the_year_tables():
    series = decisions(
        {2008: [("December 16", "0-0.25"), ("October 29", "1.00")], 1990: [("July 13", "8.00")]}
    )
    assert [(item.date, item.level_text) for item in series] == [
        (D(2008, 12, 16), "0-0.25"),
        (D(2008, 10, 29), "1.00"),
        (D(1990, 7, 13), "8.00"),
    ]
    merged = rates.merge_decisions(series)
    assert [item.date for item in merged] == sorted(item.date for item in series)


def test_a_year_without_a_table_between_two_years_keeps_the_last_decision_in_force():
    """Sin tabla para 1993, rige la última decisión de 1992 hasta la primera de 1994."""
    series = merged({1994: [("February 4", "3.25")], 1992: [("September 4", "3.00")]})
    assert rates.fomc_target_at(series, D(1993, 6, 1)) == 0.03
    assert rates.fomc_target_at(series, D(1994, 2, 4)) == 0.03  # el día de la decisión aún no rige
    assert rates.fomc_target_at(series, D(1994, 2, 5)) == 0.0325


def test_unexpected_fomc_rows_stop_the_code():
    with pytest.raises(rates.RatesFormatError):  # fecha con nota al pie
        decisions({2003: [("June 25 1", "1.00")]})
    with pytest.raises(rates.RatesFormatError):  # mes que no existe
        decisions({2003: [("Junio 25", "1.00")]})
    with pytest.raises(rates.RatesFormatError):  # día inexistente
        decisions({2003: [("February 30", "1.00")]})
    with pytest.raises(rates.RatesFormatError):  # tabla sin decisiones
        rates.parse_fomc("<html><body><h4>2003</h4></body></html>")
    html = "<h4>2003</h4><table><tr><td>June 25</td><td>1</td></tr></table>"
    with pytest.raises(rates.RatesFormatError):  # fila con menos de cuatro celdas
        rates.parse_fomc(html)
    html = "<table><tr><td>June 25</td><td>1</td><td>.</td><td>1.00</td></tr></table>"
    with pytest.raises(rates.RatesFormatError):  # tabla sin encabezado de año
        rates.parse_fomc(html)


def test_duplicate_decision_dates_stop_the_code():
    one = decisions({2003: [("June 25", "1.00")]})
    two = decisions({2003: [("June 25", "1.25")]})
    with pytest.raises(rates.RatesFormatError):
        rates.merge_decisions(one, two)


def test_ecb_file_checks():
    with pytest.raises(rates.RatesFormatError):
        rates.parse_ecb("KEY,TIME_PERIOD\r\nx,1999-01-01\r\n")  # sin OBS_VALUE
    with pytest.raises(rates.RatesFormatError):
        rates.parse_ecb(ecb_text([("1999-01", "2")]))  # fecha que no es AAAA-MM-DD
    with pytest.raises(rates.RatesFormatError):
        rates.parse_ecb(ecb_text([("1999-01-01", "dos")]))  # valor que no es número
    with pytest.raises(rates.RatesFormatError):
        rates.parse_ecb(ecb_text([("1999-01-01", "2"), ("1999-01-01", "3")]))  # fecha duplicada
    negative = ecb(("2014-06-11", "-0.1"))
    assert negative[0].rate == -0.001  # los valores negativos del BCE son válidos


def test_no_rate_before_the_first_observation():
    series = ecb(("1999-01-01", "2"))
    with pytest.raises(rates.NoRateError):
        rates.ecb_rate_at(series, D(1998, 12, 31))
    with pytest.raises(rates.NoRateError):
        rates.fomc_target_at(decisions({2003: [("June 25", "1.00")]}), D(2003, 6, 25))


def test_rates_for_dates_applies_both_conventions_at_once():
    obs = ecb(("1999-01-01", "2"), ("2003-06-06", "1"))
    dec = merged({2003: [("June 6", "1.00")], 2002: [("November 6", "1.25")]})
    days = [D(2003, 6, 5), D(2003, 6, 6), D(2003, 6, 9)]
    r_eur, r_usd = rates.rates_for_dates(days, obs, dec)
    assert r_eur == [0.02, 0.01, 0.01]  # el BCE aplica el mismo día
    assert r_usd == [0.0125, 0.0125, 0.01]  # el FOMC, desde el fijo siguiente


def star_page(note: str | None = STAR_NOTE, date_cell: str = STAR_DATE) -> str:
    footnotes = [note] if note else []
    tables = {
        2020: [("March 16", "0-0.25"), (date_cell, "1.00-1.25")],
        2019: [("July 31", "2.00-2.25")],
    }
    return fomc_html(tables, footnotes)


def test_a_date_with_an_asterisk_is_accepted_only_if_the_page_corrects_that_same_date():
    """La nota dice que la fecha se corrigió del 3 al 4 de marzo de 2020: la fila vale el 4."""
    series = rates.parse_fomc(star_page())
    starred = [item for item in series if item.date_text.endswith("*")]
    assert [(item.date, item.date_text, item.level_text) for item in starred] == [
        (D(2020, 3, 4), "March 4*", "1.00-1.25")
    ]
    assert starred[0].note == (
        "On July 10, 2024, this date was corrected from March 3, 2020, to March 4, 2020."
    )
    assert all(item.note == "" for item in series if not item.date_text.endswith("*"))


def test_the_corrected_date_decides_from_which_bar_the_new_rate_applies():
    merged = rates.merge_decisions(rates.parse_fomc(star_page()))
    assert rates.fomc_target_at(merged, D(2020, 3, 4)) == 0.02125  # el 4 aún rige 2.00-2.25
    assert rates.fomc_target_at(merged, D(2020, 3, 5)) == 0.01125  # desde el 5, 1.00-1.25
    assert rates.fomc_target_at(merged, D(2020, 3, 17)) == 0.00125  # y desde el 17, 0-0.25


@pytest.mark.parametrize(
    "page",
    [
        star_page(note=None),  # asterisco sin nota
        star_page(note="* Basis points: 1/100 percentage point"),  # otra nota
        star_page(note=STAR_NOTE.replace("to March 4, 2020", "to March 5, 2020")),  # otro día
        star_page(note=STAR_NOTE.replace("to March 4, 2020", "to March 4, 2019")),  # otro año
        star_page(note=STAR_NOTE.replace("to March 4, 2020", "to April 4, 2020")),  # otro mes
        star_page(date_cell=STAR_DATE.replace(">*<", ">**<")),  # dos asteriscos
    ],
    ids=["sin-nota", "otra-nota", "otro-dia", "otro-anio", "otro-mes", "dos-asteriscos"],
)
def test_an_asterisk_without_a_note_that_corrects_that_date_stops_the_code(page):
    with pytest.raises(rates.RatesFormatError):
        rates.parse_fomc(page)
