"""Тесты granddoc_lookup (#1061) — read-side lookup граддокументации из planning_projects."""

from __future__ import annotations

from datetime import date
from typing import Any

from sqlalchemy.exc import OperationalError, ProgrammingError

from app.services.site_finder.granddoc_lookup import parcel_granddoc

# ── Моки БД (аналог test_reservation_lookup.py) ────────────────────────────


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

    def mappings(self) -> _Result:
        return self

    def all(self) -> list[dict[str, Any]]:
        return self._rows


class _FakeDB:
    def __init__(
        self,
        rows: list[dict[str, Any]] | None = None,
        raise_exc: Exception | None = None,
    ) -> None:
        self._rows = rows or []
        self._raise = raise_exc

    def execute(self, sql: Any, params: dict[str, Any] | None = None) -> _Result:
        if self._raise is not None:
            raise self._raise
        return _Result(self._rows)


# ── Фикстура строк ────────────────────────────────────────────────────────────

_ROW_PAGA = {
    "project_type": "ppt",
    "doc_status_name": "Действующий",
    "full_name": "ПАГЕ № 2792 от 16.12.2021\nРеконструкция ул. Декабристов",
    "doc_full_name": "Проект планировки территории ул. Декабристов",
}

_ROW_NO_PAGA = {
    "project_type": "pmt",
    "doc_status_name": "Недействующий",
    "full_name": "Проект межевания без реквизитов ПАГЕ",
    "doc_full_name": "ПМТ квартал 24",
}

_ROW_INACTIVE = {
    "project_type": "ppt",
    "doc_status_name": "Недействующий",
    "full_name": "ПАГЕ № 111 от 01.03.2015\nПрежний проект",
    "doc_full_name": "Старый ППТ",
}

_ROW_ACTIVE_NEWER = {
    "project_type": "ppt",
    "doc_status_name": "Действующий",
    "full_name": "ПАГЕ № 3500 от 20.06.2023\nНовый квартал",
    "doc_full_name": "ППТ 2023",
}

# ── Тесты: парсинг ПАГЕ ───────────────────────────────────────────────────────


def test_paga_number_and_date_parsed() -> None:
    """full_name с «ПАГЕ № 2792 от 16.12.2021» → paga_number='2792', paga_date=date(2021,12,16)."""
    result = parcel_granddoc(_FakeDB([_ROW_PAGA]), "POINT(60 56)")
    assert len(result) == 1
    row = result[0]
    assert row["paga_number"] == "2792"
    assert row["paga_date"] == date(2021, 12, 16)


def test_paga_subject_extracted() -> None:
    """Предмет (текст после реквизитов ПАГЕ) извлекается как subject."""
    result = parcel_granddoc(_FakeDB([_ROW_PAGA]), "POINT(60 56)")
    assert result[0]["subject"] == "Реконструкция ул. Декабристов"


def test_is_active_true_for_dejstvuyushiy() -> None:
    """doc_status_name='Действующий' → is_active=True."""
    result = parcel_granddoc(_FakeDB([_ROW_PAGA]), "POINT(60 56)")
    assert result[0]["is_active"] is True


def test_is_active_false_for_nedejstvuyushiy() -> None:
    """doc_status_name='Недействующий' → is_active=False."""
    result = parcel_granddoc(_FakeDB([_ROW_INACTIVE]), "POINT(60 56)")
    assert result[0]["is_active"] is False


def test_no_paga_in_full_name_returns_none_graceful() -> None:
    """full_name без «ПАГЕ № …» → paga_number=None, paga_date=None, subject=None (graceful)."""
    result = parcel_granddoc(_FakeDB([_ROW_NO_PAGA]), "POINT(60 56)")
    assert len(result) == 1
    row = result[0]
    assert row["paga_number"] is None
    assert row["paga_date"] is None
    assert row["subject"] is None


def test_all_expected_keys_present() -> None:
    """Каждый элемент содержит все ожидаемые ключи."""
    result = parcel_granddoc(_FakeDB([_ROW_PAGA, _ROW_NO_PAGA]), "POINT(60 56)")
    expected = {
        "project_type",
        "doc_status_name",
        "is_active",
        "paga_number",
        "paga_date",
        "subject",
        "doc_full_name",
    }
    for item in result:
        assert set(item.keys()) == expected


# ── Тесты: сортировка ────────────────────────────────────────────────────────


def test_active_first_sorting() -> None:
    """Действующие документы идут первыми независимо от порядка в БД."""
    rows = [_ROW_INACTIVE, _ROW_PAGA]  # inactive первый в БД
    result = parcel_granddoc(_FakeDB(rows), "POINT(60 56)")
    assert result[0]["is_active"] is True
    assert result[1]["is_active"] is False


def test_active_sorted_by_date_desc() -> None:
    """Среди действующих: более новая дата идёт раньше (desc)."""
    rows = [_ROW_PAGA, _ROW_ACTIVE_NEWER]  # 2021 первый в БД, 2023 второй
    result = parcel_granddoc(_FakeDB(rows), "POINT(60 56)")
    # Оба active; 2023 > 2021 → 2023 должен быть первым
    active_results = [r for r in result if r["is_active"]]
    assert len(active_results) == 2
    assert active_results[0]["paga_date"] == date(2023, 6, 20)
    assert active_results[1]["paga_date"] == date(2021, 12, 16)


# ── Тесты: graceful ──────────────────────────────────────────────────────────


def test_empty_when_parcel_wkt_none() -> None:
    """parcel_wkt=None → пустой список, БД не вызывается."""
    result = parcel_granddoc(_FakeDB([_ROW_PAGA]), None)
    assert result == []


def test_empty_when_parcel_wkt_empty_string() -> None:
    """parcel_wkt='' → пустой список (falsy guard)."""
    result = parcel_granddoc(_FakeDB([_ROW_PAGA]), "")
    assert result == []


def test_empty_when_no_rows() -> None:
    """Нет пересечений в БД → пустой список."""
    result = parcel_granddoc(_FakeDB([]), "POINT(60 56)")
    assert result == []


def test_graceful_on_operational_error() -> None:
    """OperationalError (таблица не задеплоена) → [] без падения."""
    db = _FakeDB(raise_exc=OperationalError("stmt", {}, Exception("no such table")))
    result = parcel_granddoc(db, "POINT(60 56)")
    assert result == []


def test_graceful_on_programming_error() -> None:
    """ProgrammingError → [] без падения."""
    db = _FakeDB(raise_exc=ProgrammingError("stmt", {}, Exception("column not found")))
    result = parcel_granddoc(db, "POINT(60 56)")
    assert result == []
