"""Тесты parcel_krt_requisites (#1060) — КРТ-реквизиты из ekb_krt_sites + planning_projects.

DB/сеть не дёргаются: мокаем db.execute через FakeDB.
Проверяем:
 - exact-матч → matched=True, confidence='exact', реквизиты заполнены;
 - planning overlap без матча → matched=False, confidence='none', без застройщика;
 - пустой WKT → [];
 - OperationalError (таблица не задеплоена) → graceful [];
 - дубликаты строк (один pp_id × krt_id) → дедублируются.
"""

from __future__ import annotations

import contextlib
from decimal import Decimal
from typing import Any

from sqlalchemy.exc import OperationalError

from app.services.site_finder.krt_lookup import parcel_krt_requisites


class _MappingRow(dict):  # type: ignore[type-arg]
    """Имитирует sqlalchemy MappingResult row (dict-like)."""


class _Result:
    def __init__(self, rows: list[dict[str, Any]]) -> None:
        self._rows = [_MappingRow(r) for r in rows]

    def mappings(self) -> _Result:
        return self

    def all(self) -> list[_MappingRow]:
        return self._rows


class _FakeDB:
    """Мок SQLAlchemy Session: первый вызов execute → first_rows; второй → second_rows."""

    def __init__(
        self,
        first_rows: list[dict[str, Any]] | None = None,
        second_rows: list[dict[str, Any]] | None = None,
        raise_first: Exception | None = None,
        raise_second: Exception | None = None,
    ) -> None:
        self._calls = 0
        self._first = first_rows or []
        self._second = second_rows or []
        self._raise_first = raise_first
        self._raise_second = raise_second
        self.begin_nested_calls = 0

    @contextlib.contextmanager
    def begin_nested(self) -> Any:
        """Фейковый SAVEPOINT — не глотает исключение из execute (как реальный)."""
        self.begin_nested_calls += 1
        yield

    def execute(self, sql: Any, params: dict[str, Any] | None = None) -> _Result:
        self._calls += 1
        if self._calls == 1:
            if self._raise_first:
                raise self._raise_first
            return _Result(self._first)
        if self._raise_second:
            raise self._raise_second
        return _Result(self._second)


_WKT = "POLYGON((60.5 56.8, 60.6 56.8, 60.6 56.9, 60.5 56.9, 60.5 56.8))"

# Типичная строка exact-матча (planning_projects ⋈ ekb_krt_sites)
_EXACT_ROW = {
    "pp_id": 1166,
    "pp_full_name": (
        " ПАГЕ № 1970 от 16.08.2024\nПроект межевания территории в районе улицы Летчиков"
    ),
    "dmd_actual_year": 2024,
    "doc_status_name": "Действующий",
    "krt_id": 7,
    "site_name": "2. КРТ жилой застройки в районе ул. Летчиков",
    "developer_name": "ООО СЗ Летчиков",
    "contract_number": "2-7/21.7-14",
    "auction_final_price": Decimal("5000000"),
    "gradpotential_sqm": Decimal("25000"),
    "area_ha": Decimal("1.50"),
    "krt_kind": "жилой застройки",
}


def test_exact_match_returns_requisites() -> None:
    """Exact-матч по ПАГЕ-номер+год → matched=True, реквизиты застройщика заполнены."""
    db = _FakeDB(first_rows=[_EXACT_ROW])
    result = parcel_krt_requisites(db, _WKT)

    assert len(result) == 1
    r = result[0]
    assert r["matched"] is True
    assert r["confidence"] == "exact"
    assert r["krt_geom_ref"] == 1166
    assert r["site_name"] == "2. КРТ жилой застройки в районе ул. Летчиков"
    assert r["developer_name"] == "ООО СЗ Летчиков"
    assert r["contract_number"] == "2-7/21.7-14"
    assert r["auction_final_price"] == 5_000_000.0
    assert r["gradpotential_sqm"] == 25_000.0
    assert r["area_ha"] == 1.50


def test_exact_match_deduplicates_pp_krt_pair() -> None:
    """Дублирующиеся строки (pp_id × krt_id) дедублируются → один результат."""
    db = _FakeDB(first_rows=[_EXACT_ROW, _EXACT_ROW])
    result = parcel_krt_requisites(db, _WKT)
    assert len(result) == 1


def test_no_match_returns_geom_only() -> None:
    """Первый запрос пуст (нет exact-матча) → второй запрос → matched=False без реквизитов."""
    no_match_row = {
        "pp_id": 999,
        "pp_full_name": " ПАГЕ № 4033 от 27.12.2022\n"
        "Проект планировки территории в районе улицы Краснофлотцев",
        "dmd_actual_year": 2022,
        "doc_status_name": "Действующий",
    }
    db = _FakeDB(first_rows=[], second_rows=[no_match_row])
    result = parcel_krt_requisites(db, _WKT)

    assert len(result) == 1
    r = result[0]
    assert r["matched"] is False
    assert r["confidence"] == "none"
    assert r["krt_geom_ref"] == 999
    assert r["developer_name"] is None
    assert r["auction_final_price"] is None
    assert r["gradpotential_sqm"] is None
    assert r["site_name"] is None


def test_empty_wkt_returns_empty_list() -> None:
    """parcel_wkt=None → пустой список без вызова DB."""
    db = _FakeDB(first_rows=[_EXACT_ROW])
    assert parcel_krt_requisites(db, None) == []
    assert db._calls == 0  # DB не вызывалась


def test_empty_string_wkt_returns_empty_list() -> None:
    """parcel_wkt='' → пустой список."""
    assert parcel_krt_requisites(_FakeDB(), "") == []


def test_operational_error_first_query_returns_empty() -> None:
    """OperationalError на первом запросе (таблица не задеплоена) → []."""
    db = _FakeDB(raise_first=OperationalError("stmt", {}, Exception("relation does not exist")))
    result = parcel_krt_requisites(db, _WKT)
    assert result == []


def test_operational_error_second_query_returns_empty() -> None:
    """OperationalError на втором запросе (no-match fallback) → []."""
    db = _FakeDB(
        first_rows=[],
        raise_second=OperationalError("stmt", {}, Exception("relation does not exist")),
    )
    result = parcel_krt_requisites(db, _WKT)
    assert result == []


def test_none_decimal_fields_become_none() -> None:
    """NULL Decimal-поля → None в результате, не падают при float()."""
    row = {**_EXACT_ROW, "auction_final_price": None, "gradpotential_sqm": None, "area_ha": None}
    db = _FakeDB(first_rows=[row])
    result = parcel_krt_requisites(db, _WKT)
    assert result[0]["auction_final_price"] is None
    assert result[0]["gradpotential_sqm"] is None
    assert result[0]["area_ha"] is None


def test_no_overlap_at_all_returns_empty() -> None:
    """Оба запроса возвращают пустые списки → []."""
    db = _FakeDB(first_rows=[], second_rows=[])
    result = parcel_krt_requisites(db, _WKT)
    assert result == []


# ── #2464 cluster A: SAVEPOINT-регрессия (session-poisoning) ────────────────────
# Оба db.execute (exact-матч + no-match fallback) раньше глотали сбой БЕЗ SAVEPOINT/
# rollback — на реальном Postgres это отравляет shared session отчёта: следующий
# db.execute на ТОЙ ЖЕ сессии падает "current transaction is aborted". Фикс: оба
# сайта обёрнуты в db.begin_nested().


def test_first_query_uses_savepoint_and_session_reusable_after_failure() -> None:
    """Сбой на первом (exact-матч) запросе: begin_nested использован, [] отдан, и
    follow-up вызов на ТОЙ ЖЕ db-сессии успевает (не отравлен)."""
    db = _FakeDB(raise_first=OperationalError("stmt", {}, Exception("relation does not exist")))
    result = parcel_krt_requisites(db, _WKT)
    assert result == []
    assert db.begin_nested_calls == 1

    # follow-up на той же сессии (первый запрос теперь отдаёт exact-матч). Сбрасываем
    # _calls — _FakeDB считает execute() 1-based ОТ НАЧАЛА каждого прогона.
    db._calls = 0
    db._raise_first = None
    db._first = [_EXACT_ROW]
    result2 = parcel_krt_requisites(db, _WKT)
    assert len(result2) == 1
    assert result2[0]["matched"] is True
    assert db.begin_nested_calls == 2


def test_second_query_uses_savepoint_and_session_reusable_after_failure() -> None:
    """Сбой на втором (no-match fallback) запросе: begin_nested использован, [] отдан,
    и follow-up вызов на ТОЙ ЖЕ db-сессии успевает (не отравлен)."""
    db = _FakeDB(
        first_rows=[],
        raise_second=OperationalError("stmt", {}, Exception("relation does not exist")),
    )
    result = parcel_krt_requisites(db, _WKT)
    assert result == []
    # begin_nested вызван для обоих запросов (первый успешен, второй сбоит).
    assert db.begin_nested_calls == 2

    # follow-up на той же сессии: и первый, и второй запрос теперь успешны.
    db._calls = 0
    db._raise_second = None
    db._second = [{"pp_id": 42, "pp_full_name": "no-match", "dmd_actual_year": 2023}]
    result2 = parcel_krt_requisites(db, _WKT)
    assert len(result2) == 1
    assert result2[0]["matched"] is False
    assert db.begin_nested_calls == 4
