"""Тесты pure-парсеров CBR (offline, без живой сети).

Покрывают:
  • parse_key_rate_xml — парсинг (date, value), tz-суффикс, сортировка, пустой /
    битый / без-KR XML → []. Фикстуры из реального KeyRateXML (verified 2026-06-02).
  • parse_inflation_xlsx — парсинг колонки «Инфляция, % г/г» Excel-экспорта
    UniDbQuery (#946): дата MM.YYYY → 1-е число, выбор правильной колонки (НЕ «Цель»),
    пропуск строк без значения, сортировка, пустой / битый / без-колонки xlsx → [].
    Фикстуры строятся openpyxl'ом, зеркаля реальный формат CBR (verified 2026-06-08).
"""

from __future__ import annotations

from datetime import date
from io import BytesIO

from openpyxl import Workbook

from app.services.scrapers.cbr_macro import parse_inflation_xlsx, parse_key_rate_xml

# Реальный shape KeyRateXML (без diffgram). 3 строки, descending по дате,
# tz-суффикс +03:00, точка как десятичный разделитель.
FIXTURE_KEYRATE_XML = (
    '<?xml version="1.0" encoding="utf-8"?>'
    '<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">'
    "<soap:Body>"
    '<KeyRateXMLResponse xmlns="http://web.cbr.ru/">'
    "<KeyRateXMLResult>"
    '<KeyRate xmlns="">'
    "<KR><DT>2023-09-15T00:00:00+03:00</DT><Rate>12.00</Rate></KR>"
    "<KR><DT>2023-08-15T00:00:00+03:00</DT><Rate>12.00</Rate></KR>"
    "<KR><DT>2023-08-14T00:00:00+03:00</DT><Rate>8.50</Rate></KR>"
    "</KeyRate>"
    "</KeyRateXMLResult>"
    "</KeyRateXMLResponse>"
    "</soap:Body>"
    "</soap:Envelope>"
)

# Diffgram-вариант (метод KeyRate) — парсер должен его тоже понимать (находит KR
# где угодно), несмотря на schema-блок и namespace на KR.
FIXTURE_KEYRATE_DIFFGRAM = (
    '<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">'
    "<soap:Body><KeyRateResponse xmlns=\"http://web.cbr.ru/\"><KeyRateResult>"
    '<diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"'
    ' xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1">'
    '<KeyRate xmlns="">'
    '<KR diffgr:id="KR1" msdata:rowOrder="0">'
    "<DT>2023-09-29T00:00:00+03:00</DT><Rate>13.00</Rate></KR>"
    '<KR diffgr:id="KR2" msdata:rowOrder="1">'
    "<DT>2023-09-15T00:00:00+03:00</DT><Rate>12.00</Rate></KR>"
    "</KeyRate></diffgr:diffgram></KeyRateResult></KeyRateResponse>"
    "</soap:Body></soap:Envelope>"
)


def test_parse_basic_rows() -> None:
    """3 KR-строки → 3 кортежа (date, float), отсортированы по возрастанию даты."""
    rows = parse_key_rate_xml(FIXTURE_KEYRATE_XML)
    assert rows == [
        (date(2023, 8, 14), 8.5),
        (date(2023, 8, 15), 12.0),
        (date(2023, 9, 15), 12.0),
    ]


def test_parse_returns_sorted_ascending() -> None:
    """Вход descending — выход обязан быть ascending по дате."""
    rows = parse_key_rate_xml(FIXTURE_KEYRATE_XML)
    dates = [d for d, _ in rows]
    assert dates == sorted(dates)


def test_parse_strips_timezone_suffix() -> None:
    """DT с tz-суффиксом '+03:00' парсится в чистую дату (без сдвига)."""
    rows = parse_key_rate_xml(FIXTURE_KEYRATE_XML)
    assert (date(2023, 8, 15), 12.0) in rows
    # значение float, не строка
    assert all(isinstance(v, float) for _, v in rows)


def test_parse_value_types() -> None:
    """Дробная ставка 8.50 → 8.5 (float)."""
    rows = dict(parse_key_rate_xml(FIXTURE_KEYRATE_XML))
    assert rows[date(2023, 8, 14)] == 8.5


def test_parse_diffgram_variant() -> None:
    """Diffgram-обёртка (метод KeyRate) тоже парсится — KR находится в любом
    namespace / вложенности."""
    rows = parse_key_rate_xml(FIXTURE_KEYRATE_DIFFGRAM)
    assert rows == [
        (date(2023, 9, 15), 12.0),
        (date(2023, 9, 29), 13.0),
    ]


def test_parse_empty_string() -> None:
    assert parse_key_rate_xml("") == []
    assert parse_key_rate_xml("   ") == []


def test_parse_empty_response_no_kr() -> None:
    """Валидный, но пустой KeyRateXMLResponse (период без данных) → []."""
    xml = (
        '<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">'
        '<soap:Body><KeyRateXMLResponse xmlns="http://web.cbr.ru/" />'
        "</soap:Body></soap:Envelope>"
    )
    assert parse_key_rate_xml(xml) == []


def test_parse_malformed_xml() -> None:
    """Битый XML → [] (не бросает)."""
    assert parse_key_rate_xml("<KeyRate><KR><DT>2023") == []


def test_parse_skips_kr_with_missing_fields() -> None:
    """KR без DT или без Rate пропускается; валидные остаются."""
    xml = (
        "<KeyRate>"
        "<KR><DT>2023-08-15T00:00:00+03:00</DT><Rate>12.00</Rate></KR>"
        "<KR><DT>2023-08-16T00:00:00+03:00</DT></KR>"  # нет Rate
        "<KR><Rate>11.00</Rate></KR>"  # нет DT
        "</KeyRate>"
    )
    assert parse_key_rate_xml(xml) == [(date(2023, 8, 15), 12.0)]


def test_parse_dedups_by_date_last_wins() -> None:
    """Дубли по дате схлопываются — последний выигрывает."""
    xml = (
        "<KeyRate>"
        "<KR><DT>2023-08-15T00:00:00+03:00</DT><Rate>12.00</Rate></KR>"
        "<KR><DT>2023-08-15T00:00:00+03:00</DT><Rate>13.00</Rate></KR>"
        "</KeyRate>"
    )
    assert parse_key_rate_xml(xml) == [(date(2023, 8, 15), 13.0)]


# ── parse_inflation_xlsx (#946) ─────────────────────────────────────────────────


def _make_inflation_xlsx(
    rows: list[tuple[object, object, object, object]],
    *,
    header: tuple[str, str, str, str] = (
        "Дата",
        "Ключевая ставка, % годовых",
        "Инфляция, % г/г",
        "Цель по инфляции",
    ),
) -> bytes:
    """Собрать .xlsx, зеркаля реальный CBR-экспорт: header + строки (дата, ставка,
    инфляция, цель). Возвращает байты (как вернул бы DownloadExcel)."""
    wb = Workbook()
    ws = wb.active
    ws.append(list(header))
    for r in rows:
        ws.append(list(r))
    buf = BytesIO()
    wb.save(buf)
    return buf.getvalue()


def test_parse_inflation_basic() -> None:
    """Реальный shape: MM.YYYY-даты descending, нативные float → берём колонку
    «Инфляция» (НЕ «Цель»), дата → 1-е число, сортировка по возрастанию."""
    content = _make_inflation_xlsx(
        [
            ("04.2026", 14.5, 5.58, 4.0),
            ("03.2026", 15.0, 5.86, 4.0),
            ("02.2026", 15.5, 5.91, 4.0),
        ]
    )
    rows = parse_inflation_xlsx(content)
    assert rows == [
        (date(2026, 2, 1), 5.91),
        (date(2026, 3, 1), 5.86),
        (date(2026, 4, 1), 5.58),
    ]


def test_parse_inflation_picks_inflation_not_target() -> None:
    """Колонка «Цель по инфляции» (=4.0) НЕ должна попасть вместо «Инфляция»."""
    content = _make_inflation_xlsx([("01.2026", 16.0, 6.0, 4.0)])
    rows = parse_inflation_xlsx(content)
    assert rows == [(date(2026, 1, 1), 6.0)]
    # значение именно инфляции, а не цели
    assert rows[0][1] != 4.0


def test_parse_inflation_skips_rows_without_value() -> None:
    """Строки с пустой инфляцией пропускаются; валидные остаются."""
    content = _make_inflation_xlsx(
        [
            ("03.2013", None, 7.02, None),  # ранний месяц: ставка пустая, инфляция есть
            ("02.2013", None, None, None),  # нет инфляции → скип
        ]
    )
    rows = parse_inflation_xlsx(content)
    assert rows == [(date(2013, 3, 1), 7.02)]


def test_parse_inflation_handles_comma_decimal() -> None:
    """Строковая ячейка с запятой-разделителем (на случай не-числовой ячейки)."""
    content = _make_inflation_xlsx([("12.2025", "16,00", "5,59", "4,00")])
    rows = parse_inflation_xlsx(content)
    assert rows == [(date(2025, 12, 1), 5.59)]


def test_parse_inflation_dedups_by_month_last_wins() -> None:
    content = _make_inflation_xlsx(
        [
            ("05.2025", 20.0, 9.9, 4.0),
            ("05.2025", 20.0, 10.1, 4.0),
        ]
    )
    rows = parse_inflation_xlsx(content)
    assert rows == [(date(2025, 5, 1), 10.1)]


def test_parse_inflation_empty_bytes() -> None:
    assert parse_inflation_xlsx(b"") == []


def test_parse_inflation_malformed_bytes() -> None:
    """Не-xlsx байты → [] (не бросает)."""
    assert parse_inflation_xlsx(b"not an excel file") == []


def test_parse_inflation_no_inflation_column() -> None:
    """Excel без колонки «Инфляция» (формат изменился) → [] (graceful)."""
    content = _make_inflation_xlsx(
        [("04.2026", 14.5, 5.58, 4.0)],
        header=("Дата", "Ставка", "Нечто", "Иное"),
    )
    assert parse_inflation_xlsx(content) == []


def test_parse_inflation_skips_unparseable_date() -> None:
    """Строка с непарсящейся датой (мусор) пропускается, валидные остаются."""
    content = _make_inflation_xlsx(
        [
            ("04.2026", 14.5, 5.58, 4.0),
            ("мусор", 1.0, 9.9, 4.0),
        ]
    )
    rows = parse_inflation_xlsx(content)
    assert rows == [(date(2026, 4, 1), 5.58)]
