"""Тесты SDMX-парсера ЕМИСС/fedstat (offline, на фикстурах — без живой сети).

Парсер ``parse_emiss_sdmx`` — pure (SDMX-байты → list[EmissRow]); fetch с fedstat
исполняется только на проде (за WAF с dev), поэтому здесь тестируем РАЗБОР на реальных
(обрезанных) SDMX-выгрузках, снятых с прода 2026-06-07:
  • emiss_income_57039.xml — «Среднедушевые денежные доходы населения» (id=57039),
    размерность s_OKATO, квартальный (рубль/мес). Свердл = ОКАТО 65000000000.
  • emiss_empty.xml — валидный конверт с пустым <DataSet>.

Покрывают: извлечение конкретных строк (Свердл, реальные период+значение),
region-фильтрацию (др. регионы/федокруга исключены), маппинг квартала→obs_date,
дедуп, graceful на пустом/битом/без-DataSet SDMX → [].
"""

from __future__ import annotations

from datetime import date
from decimal import Decimal
from pathlib import Path

from app.services.scrapers.rosstat_emiss import (
    INCOME_PER_CAPITA_SPEC,
    OKATO_SVERDLOVSK_OBLAST,
    EmissIndicatorSpec,
    RegionTarget,
    _emiss_period_granularity,
    _emiss_period_to_month,
    parse_emiss_sdmx,
)

_FIXTURES = Path(__file__).resolve().parent.parent / "fixtures"


def _load(name: str) -> bytes:
    return (_FIXTURES / name).read_bytes()


# ── period mapping ────────────────────────────────────────────────────────────────


def test_period_quarter_to_start_month() -> None:
    """Квартал → стартовый месяц квартала (регистр/пробелы игнор)."""
    assert _emiss_period_to_month("I квартал") == 1
    assert _emiss_period_to_month("II квартал") == 4
    assert _emiss_period_to_month("III квартал") == 7
    assert _emiss_period_to_month("  iv КВАРТАЛ ") == 10


def test_period_month_name_to_num() -> None:
    """Русское название месяца → номер месяца."""
    assert _emiss_period_to_month("январь") == 1
    assert _emiss_period_to_month("Декабрь") == 12


def test_period_year_and_unknown() -> None:
    """Годовой под-период → январь; незнакомый/пустой → None."""
    assert _emiss_period_to_month("год") == 1
    assert _emiss_period_to_month("значение показателя за год") == 1
    assert _emiss_period_to_month("3 неделя") is None
    assert _emiss_period_to_month("") is None


def test_period_granularity() -> None:
    """_emiss_period_granularity корректно классифицирует все под-периоды (#1606)."""
    assert _emiss_period_granularity("год") == "year"
    assert _emiss_period_granularity("значение показателя за год") == "year"
    assert _emiss_period_granularity("за год") == "year"
    assert _emiss_period_granularity("I квартал") == "quarter"
    assert _emiss_period_granularity("IV квартал") == "quarter"
    assert _emiss_period_granularity("январь") == "month"
    assert _emiss_period_granularity("декабрь") == "month"
    assert _emiss_period_granularity("3 неделя") == "unknown"
    assert _emiss_period_granularity("") == "unknown"


def test_yearly_and_q1_both_survive_dedup() -> None:
    """Годовой ряд ('год') и Q1 ('I квартал') за один год — оба остаются (#1606).

    До фикса: оба давали obs_date=YYYY-01-01, ключ (region, obs_date) был один →
    второй по XML-порядку перезаписывал первого. После фикса: ключ трёхкомпонентный
    (region, obs_date, granularity) → 'year' и 'quarter' — разные слоты.
    """
    xml = (
        '<GenericData xmlns="http://www.SDMX.org/resources/SDMXML/schemas/v1_0/message"'
        ' xmlns:generic="http://www.SDMX.org/resources/SDMXML/schemas/v1_0/generic">'
        "<DataSet>"
        # годовой ряд за 2023 — значение 48000
        "<generic:Series><generic:SeriesKey>"
        '<generic:Value concept="s_OKATO" value="65000000000"/></generic:SeriesKey>'
        '<generic:Attributes><generic:Value concept="PERIOD" value="год"/>'
        "</generic:Attributes>"
        "<generic:Obs><generic:Time>2023</generic:Time>"
        '<generic:ObsValue value="48000"/></generic:Obs></generic:Series>'
        # Q1 того же 2023 — значение 42000; obs_date тоже 2023-01-01
        "<generic:Series><generic:SeriesKey>"
        '<generic:Value concept="s_OKATO" value="65000000000"/></generic:SeriesKey>'
        '<generic:Attributes><generic:Value concept="PERIOD" value="I квартал"/>'
        "</generic:Attributes>"
        "<generic:Obs><generic:Time>2023</generic:Time>"
        '<generic:ObsValue value="42000"/></generic:Obs></generic:Series>'
        "</DataSet></GenericData>"
    )
    rows = parse_emiss_sdmx(xml, INCOME_PER_CAPITA_SPEC)
    # Оба наблюдения должны присутствовать — разные гранулярности
    assert len(rows) == 2, f"ожидались 2 строки, получили {len(rows)}: {rows}"
    values = {r.value for r in rows}
    assert Decimal("48000") in values, "годовое наблюдение потеряно"
    assert Decimal("42000") in values, "Q1-наблюдение потеряно"
    # obs_date у обоих одинаковый (это нормально — коллизия теперь на стороне DB-upsert)
    assert all(r.obs_date == date(2023, 1, 1) for r in rows)
    # period_type: годовой → 'year', Q1 → 'quarter' (часть PK в macro_indicator, migration 163)
    period_types = {r.period_type for r in rows}
    assert period_types == {
        "year",
        "quarter",
    }, f"ожидались period_type year+quarter, получили {period_types}"


# ── real-fixture extraction (income id=57039) ─────────────────────────────────────


def test_income_extracts_sverdlovsk_only() -> None:
    """Из реальной SDMX-выгрузки извлекается ТОЛЬКО Свердл (ОКАТО 65) — РФ/ЦФО/Адыгея нет.
    Все строки квартальные → period_type='quarter' (часть PK macro_indicator, migration 163).
    """
    rows = parse_emiss_sdmx(_load("emiss_income_57039.xml"), INCOME_PER_CAPITA_SPEC)
    assert rows, "ожидались строки по Свердл"
    assert {r.region for r in rows} == {"sverdl"}
    assert all(r.indicator_type == "income_per_capita" for r in rows)
    assert all(r.unit == "руб" for r in rows)
    assert all(r.frequency == "quarterly" for r in rows)
    assert all(
        r.period_type == "quarter" for r in rows
    ), "квартальные строки должны иметь period_type='quarter'"


def test_income_concrete_values_and_dates() -> None:
    """Конкретные реальные значения Свердл + obs_date = 1-е число стартового месяца квартала.

    Реальные данные fedstat (Свердл, 2024): I=54006, II=61136, III=66849, IV=76701 ₽/мес;
    I кв.2025 = 64361. Квартал → стартовый месяц (I→01, II→04, III→07, IV→10).
    """
    rows = parse_emiss_sdmx(_load("emiss_income_57039.xml"), INCOME_PER_CAPITA_SPEC)
    by_date = {r.obs_date: r.value for r in rows}

    assert by_date[date(2024, 1, 1)] == Decimal("54006")
    assert by_date[date(2024, 4, 1)] == Decimal("61136")
    assert by_date[date(2024, 7, 1)] == Decimal("66849")
    assert by_date[date(2024, 10, 1)] == Decimal("76701")
    assert by_date[date(2025, 1, 1)] == Decimal("64361")
    # ровно 5 свердловских наблюдений в фикстуре (4 квартала 2024 + I кв.2025)
    assert len(rows) == 5
    # отсортировано по дате ASC
    assert [r.obs_date for r in rows] == sorted(by_date)


def test_income_other_regions_excluded() -> None:
    """ОКАТО других субъектов/агрегатов (Адыгея 79, ЦФО 030, РФ 643) НЕ попадают.

    Значение Адыгеи (40190) и ЦФО (68110) присутствуют в фикстуре, но НЕ должны
    появиться среди извлечённых (фильтр строго по целевому ОКАТО 65000000000).
    """
    rows = parse_emiss_sdmx(_load("emiss_income_57039.xml"), INCOME_PER_CAPITA_SPEC)
    values = {r.value for r in rows}
    assert Decimal("40190") not in values  # Адыгея
    assert Decimal("68110") not in values  # ЦФО
    assert Decimal("53221") not in values  # РФ


def test_income_region_filter_targets_adygea() -> None:
    """Подменив целевой ОКАТО на Адыгею — извлекается ИМЕННО Адыгея (доказывает фильтр)."""
    spec = EmissIndicatorSpec(
        indicator_id="57039",
        indicator_type="income_per_capita",
        unit="руб",
        frequency="quarterly",
        targets=(RegionTarget(oktmo="79000000000", region="adygea"),),
    )
    rows = parse_emiss_sdmx(_load("emiss_income_57039.xml"), spec)
    assert len(rows) == 1
    assert rows[0].region == "adygea"
    assert rows[0].value == Decimal("40190")
    assert rows[0].obs_date == date(2024, 1, 1)


# ── graceful degradation ──────────────────────────────────────────────────────────


def test_empty_dataset_returns_empty() -> None:
    """Валидный SDMX с пустым <DataSet> → [] (не падает)."""
    assert parse_emiss_sdmx(_load("emiss_empty.xml"), INCOME_PER_CAPITA_SPEC) == []


def test_malformed_xml_returns_empty() -> None:
    """Битый XML → [] (+warning), не бросает."""
    assert parse_emiss_sdmx(b"<GenericData><DataSet><not closed", INCOME_PER_CAPITA_SPEC) == []
    assert parse_emiss_sdmx("не xml вовсе", INCOME_PER_CAPITA_SPEC) == []


def test_no_dataset_element_returns_empty() -> None:
    """SDMX-конверт без <DataSet> → [] (не падает)."""
    xml = (
        '<GenericData xmlns="http://www.SDMX.org/resources/SDMXML/schemas/v1_0/message">'
        "<Header/></GenericData>"
    )
    assert parse_emiss_sdmx(xml, INCOME_PER_CAPITA_SPEC) == []


def test_unknown_period_series_skipped() -> None:
    """Серия с незнакомым PERIOD (неделя) пропускается, валидная остаётся."""
    xml = (
        '<GenericData xmlns="http://www.SDMX.org/resources/SDMXML/schemas/v1_0/message"'
        ' xmlns:generic="http://www.SDMX.org/resources/SDMXML/schemas/v1_0/generic">'
        "<DataSet>"
        "<generic:Series><generic:SeriesKey>"
        '<generic:Value concept="s_OKATO" value="65000000000"/></generic:SeriesKey>'
        '<generic:Attributes><generic:Value concept="PERIOD" value="3 неделя"/>'
        "</generic:Attributes>"
        "<generic:Obs><generic:Time>2024</generic:Time>"
        '<generic:ObsValue value="999"/></generic:Obs></generic:Series>'
        "<generic:Series><generic:SeriesKey>"
        '<generic:Value concept="s_OKATO" value="65000000000"/></generic:SeriesKey>'
        '<generic:Attributes><generic:Value concept="PERIOD" value="I квартал"/>'
        "</generic:Attributes>"
        "<generic:Obs><generic:Time>2024</generic:Time>"
        '<generic:ObsValue value="50000"/></generic:Obs></generic:Series>'
        "</DataSet></GenericData>"
    )
    rows = parse_emiss_sdmx(xml, INCOME_PER_CAPITA_SPEC)
    assert len(rows) == 1
    assert rows[0].value == Decimal("50000")
    assert rows[0].obs_date == date(2024, 1, 1)


def test_comma_decimal_value_parsed() -> None:
    """Значение с запятой-десятичным (ИПЦ-вариант '113,2') → Decimal('113.2')."""
    xml = (
        '<GenericData xmlns="http://www.SDMX.org/resources/SDMXML/schemas/v1_0/message"'
        ' xmlns:generic="http://www.SDMX.org/resources/SDMXML/schemas/v1_0/generic">'
        "<DataSet><generic:Series><generic:SeriesKey>"
        '<generic:Value concept="s_OKATO" value="65000000000"/></generic:SeriesKey>'
        '<generic:Attributes><generic:Value concept="PERIOD" value="январь"/>'
        "</generic:Attributes>"
        "<generic:Obs><generic:Time>2024</generic:Time>"
        '<generic:ObsValue value="113,2"/></generic:Obs></generic:Series></DataSet>'
        "</GenericData>"
    )
    rows = parse_emiss_sdmx(xml, INCOME_PER_CAPITA_SPEC)
    assert len(rows) == 1
    assert rows[0].value == Decimal("113.2")
    assert rows[0].obs_date == date(2024, 1, 1)


def test_dedup_last_wins() -> None:
    """Дубль (region, obs_date) схлопывается — последний выигрывает (PK всё равно один)."""
    xml = (
        '<GenericData xmlns="http://www.SDMX.org/resources/SDMXML/schemas/v1_0/message"'
        ' xmlns:generic="http://www.SDMX.org/resources/SDMXML/schemas/v1_0/generic">'
        "<DataSet>"
        "<generic:Series><generic:SeriesKey>"
        '<generic:Value concept="s_OKATO" value="65000000000"/></generic:SeriesKey>'
        '<generic:Attributes><generic:Value concept="PERIOD" value="I квартал"/>'
        "</generic:Attributes>"
        "<generic:Obs><generic:Time>2024</generic:Time>"
        '<generic:ObsValue value="100"/></generic:Obs></generic:Series>'
        "<generic:Series><generic:SeriesKey>"
        '<generic:Value concept="s_OKATO" value="65000000000"/></generic:SeriesKey>'
        '<generic:Attributes><generic:Value concept="PERIOD" value="I квартал"/>'
        "</generic:Attributes>"
        "<generic:Obs><generic:Time>2024</generic:Time>"
        '<generic:ObsValue value="200"/></generic:Obs></generic:Series>'
        "</DataSet></GenericData>"
    )
    rows = parse_emiss_sdmx(xml, INCOME_PER_CAPITA_SPEC)
    assert len(rows) == 1
    assert rows[0].value == Decimal("200")


def test_spec_targets_sverdlovsk_okato() -> None:
    """Активная спека доходов целит ИМЕННО ОКАТО Свердл (контракт §7.11)."""
    assert OKATO_SVERDLOVSK_OBLAST == "65000000000"
    assert any(t.oktmo == OKATO_SVERDLOVSK_OBLAST for t in INCOME_PER_CAPITA_SPEC.targets)
    assert INCOME_PER_CAPITA_SPEC.indicator_id == "57039"
    assert INCOME_PER_CAPITA_SPEC.frequency == "quarterly"
