"""Тесты read-only retrieval чата (#957): §22-отчёт + курируемый паспорт участка.

Покрываем `get_parcel_context_for_chat` (§19 data-residency):
  • строит ПЛОСКИЙ whitelist-дикт из фиксированных градо-полей analyze-рана;
  • НЕ протаскивает наружу сырые под-словари / геометрию / любой лишний ключ payload;
  • ЗОУИТ: dedup названий, cap, разрешение противоречия encumbrance↔overlaps;
  • None, если analyze-рана/результата нет (эндпоинт работает как раньше).

Стратегия: latest_run_for патчится (никакой прод-БД); db — MagicMock.
"""

from __future__ import annotations

from typing import Any
from unittest.mock import MagicMock, patch

from app.services.chat import retrieval

_CAD = "66:41:0204016:10"


def _analyze_payload() -> dict[str, Any]:
    """analyze-1.0 result с курируемыми полями И «шумом», который наружу уходить НЕ должен."""
    return {
        "egrn": {
            "address": "Екатеринбург, ул. Ленина, 1",
            "area_m2": 5400,
            "land_category": "земли населённых пунктов",
            "permitted_use_text": "многоэтажная жилая застройка",
            "cadastral_value_rub": 123456789,
            "parcel_status": "учтённый",
            "ownership_type": "муниципальная",
            # Шум: сырой под-словарь + PII-подобные поля — НЕ должны попасть в срез.
            "raw": {"owner_name": "Иванов И.И.", "internal_id": 42},
        },
        "nspd_zoning": {
            "zone_code": "Ж-4",
            "zone_name": "Зона многоэтажной жилой застройки",
            "max_far": 2.5,
            "max_floors": 25,
            "max_height_m": 75,
            "max_building_pct": 40,
            "min_parcel_area_m2": 1500,
            "regulation_source": "ПЗЗ Екатеринбурга",
            "main_vri": ["2.6", "3.1"],  # список ВРИ — в срез НЕ тянем (не в whitelist)
        },
        "encumbrance": {"has_zouit": True, "zouit_count": 2},
        "nspd_zouit_overlaps": [
            {"type_zone": "Приаэродромная территория", "reg_numb_border": "66:00-6.1"},
            {"name": "Охранная зона ЛЭП", "coverage_pct": 12.3},
            {"type_zone": "Приаэродромная территория"},  # дубль → dedup
        ],
        # Тяжёлый сырой блоб + геометрия — НЕ курируемые, наружу НЕЛЬЗЯ.
        "geometry": {"type": "Polygon", "coordinates": [[[1, 2], [3, 4]]]},
        "market_now": {"deals": [{"price": 1}]},
    }


def _make_run(result: Any, run_id: int = 7) -> MagicMock:
    run = MagicMock()
    run.result = result
    run.id = run_id
    return run


def _get_context(result: Any) -> dict[str, Any] | None:
    with patch(
        "app.services.chat.retrieval.latest_run_for",
        return_value=_make_run(result) if result is not None else None,
    ):
        return retrieval.get_parcel_context_for_chat(MagicMock(), _CAD)


# ── whitelist: нужные поля есть, лишних нет ──────────────────────────────────────


def test_builds_curated_whitelist_dict() -> None:
    ctx = _get_context(_analyze_payload())
    assert ctx is not None
    # ЕГРН-скаляры.
    assert ctx["address"] == "Екатеринбург, ул. Ленина, 1"
    assert ctx["area_m2"] == 5400
    assert ctx["land_category"] == "земли населённых пунктов"
    assert ctx["permitted_use_text"] == "многоэтажная жилая застройка"
    assert ctx["cadastral_value_rub"] == 123456789
    assert ctx["parcel_status"] == "учтённый"
    assert ctx["ownership_type"] == "муниципальная"
    # Градрегламент ПЗЗ.
    assert ctx["zone_code"] == "Ж-4"
    assert ctx["zone_name"] == "Зона многоэтажной жилой застройки"
    assert ctx["max_far"] == 2.5
    assert ctx["max_floors"] == 25
    assert ctx["max_height_m"] == 75
    assert ctx["max_building_pct"] == 40
    assert ctx["min_parcel_area_m2"] == 1500
    assert ctx["regulation_source"] == "ПЗЗ Екатеринбурга"
    # ЗОУИТ-сводка + dedup названий.
    assert ctx["has_zouit"] is True
    assert ctx["zouit_count"] == 2
    assert ctx["zouit_zone_names"] == ["Приаэродромная территория", "Охранная зона ЛЭП"]


def test_does_not_leak_geometry_raw_or_extra_keys() -> None:
    """§19: срез — ТОЛЬКО фиксированный whitelist; geometry/raw/market и т.п. НЕ протекают."""
    ctx = _get_context(_analyze_payload())
    assert ctx is not None
    allowed = {
        "address",
        "area_m2",
        "land_category",
        "permitted_use_text",
        "cadastral_value_rub",
        "parcel_status",
        "ownership_type",
        "zone_code",
        "zone_name",
        "max_far",
        "max_floors",
        "max_height_m",
        "max_building_pct",
        "min_parcel_area_m2",
        "regulation_source",
        "has_zouit",
        "zouit_count",
        "zouit_zone_names",
    }
    assert set(ctx) <= allowed
    # Явно: ни сырых под-словарей, ни геометрии, ни PII/ВРИ-списка.
    assert "geometry" not in ctx
    assert "raw" not in ctx
    assert "market_now" not in ctx
    assert "main_vri" not in ctx
    # Все значения — скаляры или список строк (никаких вложенных dict/геометрии).
    for key, value in ctx.items():
        if key == "zouit_zone_names":
            assert isinstance(value, list)
            assert all(isinstance(n, str) for n in value)
        else:
            assert isinstance(value, str | int | float | bool)


# ── §19 type-guard: не-скаляры отбрасываются, а не протаскиваются ─────────────────


def test_dict_value_in_egrn_field_is_dropped() -> None:
    """Под-словарь в egrn.address (напр. с PII) НЕ должен утечь в срез — ключ отсутствует."""
    payload = {
        "egrn": {
            "address": {"owner_pii": "Иванов И.И."},  # вложенный dict — §19-угроза
            "area_m2": 500,
        }
    }
    ctx = _get_context(payload)
    assert ctx is not None
    assert "address" not in ctx  # dict-значение отброшено гейтом типа
    assert ctx["area_m2"] == 500  # скаляр рядом при этом сохраняется


def test_list_with_nested_dict_in_zone_code_is_dropped() -> None:
    """Список (тем более с вложенным dict) в zone_code НЕ протаскивается — ключа нет."""
    payload = {"nspd_zoning": {"zone_code": ["Ж-4", {"nested": "leak"}], "max_floors": 25}}
    ctx = _get_context(payload)
    assert ctx is not None
    assert "zone_code" not in ctx  # list-значение отброшено
    assert ctx["max_floors"] == 25  # соседний скаляр цел


def test_zouit_names_drops_non_string_elements() -> None:
    """В списке ЗОУИТ не-строковые элементы (dict/число) отбрасываются, строки остаются."""
    payload = {
        "nspd_zouit_overlaps": [
            {"type_zone": "Санитарно-защитная зона"},
            {"type_zone": {"leak": "нельзя"}},  # dict вместо строки — отбросить
            {"name": 12345},  # число вместо строки — отбросить
            {"name": "Водоохранная зона"},
        ],
    }
    ctx = _get_context(payload)
    assert ctx is not None
    assert ctx["zouit_zone_names"] == ["Санитарно-защитная зона", "Водоохранная зона"]
    # Ни одного не-строкового элемента / repr'а вложенной структуры.
    assert all(isinstance(n, str) for n in ctx["zouit_zone_names"])
    assert "leak" not in " ".join(ctx["zouit_zone_names"])


# ── зона: fallback zone_code → regulation_zone_index ─────────────────────────────


def test_zone_code_falls_back_to_regulation_zone_index() -> None:
    payload = {"nspd_zoning": {"regulation_zone_index": "ОД-1"}}
    ctx = _get_context(payload)
    assert ctx is not None
    assert ctx["zone_code"] == "ОД-1"


# ── ЗОУИТ: противоречие encumbrance=нет, но пересечения есть ──────────────────────


def test_zouit_contradiction_resolved_from_overlaps() -> None:
    payload = {
        "encumbrance": {"has_zouit": False, "zouit_count": 0},
        "nspd_zouit_overlaps": [
            {"type_zone": "Санитарно-защитная зона"},
            {"name": "Водоохранная зона"},
        ],
    }
    ctx = _get_context(payload)
    assert ctx is not None
    assert ctx["has_zouit"] is True
    assert ctx["zouit_count"] == 2
    assert ctx["zouit_zone_names"] == ["Санитарно-защитная зона", "Водоохранная зона"]


def test_zouit_names_capped() -> None:
    payload = {
        "nspd_zouit_overlaps": [{"type_zone": f"Зона {i}"} for i in range(30)],
    }
    ctx = _get_context(payload)
    assert ctx is not None
    assert len(ctx["zouit_zone_names"]) == retrieval._MAX_ZOUIT_NAMES


# ── graceful: отсутствие полей / рана ────────────────────────────────────────────


def test_missing_fields_are_omitted_not_none() -> None:
    # Пустой ЕГРН/зонирование → эти ключи просто отсутствуют (не None).
    ctx = _get_context({"egrn": {"address": "Тест"}})
    assert ctx == {"address": "Тест"}
    assert "zone_code" not in ctx
    assert "area_m2" not in ctx


def test_returns_none_when_no_run() -> None:
    assert _get_context(None) is None


def test_returns_none_when_result_not_dict() -> None:
    assert _get_context(["not", "a", "dict"]) is None


def test_returns_none_when_no_curated_fields() -> None:
    # Ран есть, но ни одного курируемого поля → None (нечего вливать в отчёт).
    assert _get_context({"market_now": {"x": 1}, "geometry": {}}) is None
