"""Тесты GET /{cad_num}/neighbor-buildings (Forgejo #2180).

Лёгкий синхронный endpoint соседних footprint'ов для 3D-сцены §7 «Размещение
застройки»: GeoJSON FeatureCollection упрощённых полигонов cad_buildings вокруг
участка.

Механика моков: get_db переопределяем через app.dependency_overrides (FastAPI
Depends держит оригинальную ссылку — unittest.mock.patch неэффективен). Хендлер
делает один основной `db.execute(...).mappings().all()` (соседи), и ТОЛЬКО при
пустом результате — второй `db.execute(...).first()` (EXISTS-пробник участка,
404 vs пустой 200). Мок покрывает обе формы. Очистку dependency_overrides делает
autouse-фикстура в tests/conftest.py. RBAC-гейт в test-mode (settings.testing)
байпасится — auth-заголовок не нужен.
"""

from __future__ import annotations

import json
from typing import Any
from unittest.mock import MagicMock

import pytest
from fastapi.testclient import TestClient

from app.core.db import get_db
from app.main import app

_VALID_CAD = "66:41:0204016:10"

# Валидный GeoJSON-полигон footprint'а — как отдаёт ST_AsGeoJSON (строка).
_POLYGON_GEOJSON = json.dumps(
    {
        "type": "Polygon",
        "coordinates": [
            [
                [60.640, 56.830],
                [60.641, 56.830],
                [60.641, 56.831],
                [60.640, 56.831],
                [60.640, 56.830],
            ]
        ],
    }
)


def _make_building_row(
    cad_num: str = "66:41:0204016:100",
    floors: int | None = 9,
    building_name: str | None = "Жилой дом",
    year_built: int | None = 1998,
    distance_m: float = 42.0,
    geojson: str | None = _POLYGON_GEOJSON,
) -> dict[str, Any]:
    """Одна строка результата _NEIGHBOR_BUILDINGS_SQL (mappings())."""
    return {
        "cad_num": cad_num,
        "floors": floors,
        "building_name": building_name,
        "year_built": year_built,
        "geojson": geojson,
        "distance_m": distance_m,
    }


def _build_mock_db(
    rows: list[dict[str, Any]],
    *,
    parcel_exists: bool = True,
) -> MagicMock:
    """Mock Session.

    Основной запрос соседей → .mappings().all() = rows.
    Fallback EXISTS-пробник (вызывается лишь при пустом rows) → .first() =
    (1,) если parcel_exists иначе None.
    """
    mock_db = MagicMock()

    mappings_mock = MagicMock()
    mappings_mock.all.return_value = rows

    execute_result = MagicMock()
    execute_result.mappings.return_value = mappings_mock
    # .first() дёргается только на EXISTS-пробнике (тот же execute_result — ок,
    # т.к. пробник запускается только когда rows пуст).
    execute_result.first.return_value = (1,) if parcel_exists else None

    mock_db.execute.return_value = execute_result
    return mock_db


def _override_db(db: MagicMock):
    def _get_db_override():
        yield db

    return _get_db_override


@pytest.fixture()
def client() -> TestClient:
    return TestClient(app)


# ── happy path: фичи + floors → height_m ──────────────────────────────────


def test_neighbor_buildings_happy_path(client: TestClient) -> None:
    rows = [
        _make_building_row("66:41:0204016:100", floors=9, distance_m=30.0),
        _make_building_row("66:41:0204016:101", floors=16, distance_m=85.0),
        _make_building_row(
            "66:41:0204016:102",
            floors=None,  # floors неизвестны → height_m null
            building_name="",  # пустое имя → null
            year_built=None,
            distance_m=120.0,
        ),
    ]
    app.dependency_overrides[get_db] = _override_db(_build_mock_db(rows))

    r = client.get(f"/api/v1/parcels/{_VALID_CAD}/neighbor-buildings")

    assert r.status_code == 200, r.text
    body = r.json()
    assert body["type"] == "FeatureCollection"
    assert body["count"] == 3
    assert body["radius_m"] == 300
    assert body["truncated"] is False
    assert len(body["features"]) == 3

    f0 = body["features"][0]
    assert f0["type"] == "Feature"
    assert f0["geometry"]["type"] == "Polygon"  # распарсен из строки, не escaped
    props0 = f0["properties"]
    assert props0["cad_num"] == "66:41:0204016:100"
    assert props0["floors"] == 9
    assert props0["height_m"] == pytest.approx(27.0)  # 9 × 3.0
    assert props0["building_name"] == "Жилой дом"
    assert props0["year_built"] == 1998
    assert props0["is_neighbor"] is True

    # floors=16 → height_m 48.0
    assert body["features"][1]["properties"]["height_m"] == pytest.approx(48.0)

    # floors=None → height_m null, building_name пустой → null, year_built null
    props2 = body["features"][2]["properties"]
    assert props2["floors"] is None
    assert props2["height_m"] is None
    assert props2["building_name"] is None
    assert props2["year_built"] is None
    assert props2["is_neighbor"] is True


# ── пустой результат (участок есть, соседей нет) → 200 empty ───────────────


def test_neighbor_buildings_empty_ok(client: TestClient) -> None:
    """Участок в БД есть (EXISTS=true), но соседей в радиусе нет → 200 пустой."""
    app.dependency_overrides[get_db] = _override_db(_build_mock_db([], parcel_exists=True))

    r = client.get(f"/api/v1/parcels/{_VALID_CAD}/neighbor-buildings")

    assert r.status_code == 200, r.text
    body = r.json()
    assert body["type"] == "FeatureCollection"
    assert body["features"] == []
    assert body["count"] == 0
    assert body["truncated"] is False


# ── участок не найден (нет geom) → 404 ─────────────────────────────────────


def test_neighbor_buildings_parcel_not_found_404(client: TestClient) -> None:
    """Пустой результат + EXISTS-пробник вернул None (нет geom) → 404."""
    app.dependency_overrides[get_db] = _override_db(_build_mock_db([], parcel_exists=False))

    r = client.get("/api/v1/parcels/66:41:9999999:1/neighbor-buildings")

    assert r.status_code == 404
    assert "не найдена" in r.json()["detail"]


# ── truncated: >300 соседей → флаг + обрезка до 300 ────────────────────────


def test_neighbor_buildings_truncated(client: TestClient) -> None:
    """SQL LIMIT 301 → если пришёл 301, отдаём 300 + truncated=true."""
    rows = [
        _make_building_row(f"66:41:0204016:{i}", floors=(i % 20) + 1, distance_m=float(i))
        for i in range(301)
    ]
    app.dependency_overrides[get_db] = _override_db(_build_mock_db(rows))

    r = client.get(f"/api/v1/parcels/{_VALID_CAD}/neighbor-buildings")

    assert r.status_code == 200, r.text
    body = r.json()
    assert body["count"] == 300
    assert len(body["features"]) == 300
    assert body["truncated"] is True


# ── radius валидация → 422 ─────────────────────────────────────────────────


def test_neighbor_buildings_radius_too_small_422(client: TestClient) -> None:
    """radius_m=10 (< min 50) → 422."""
    r = client.get(f"/api/v1/parcels/{_VALID_CAD}/neighbor-buildings?radius_m=10")
    assert r.status_code == 422


def test_neighbor_buildings_radius_too_large_422(client: TestClient) -> None:
    """radius_m=2000 (> max 1000) → 422."""
    r = client.get(f"/api/v1/parcels/{_VALID_CAD}/neighbor-buildings?radius_m=2000")
    assert r.status_code == 422


def test_neighbor_buildings_radius_passthrough(client: TestClient) -> None:
    """radius_m из query отражается в ответе (и внутри — в bind SQL)."""
    app.dependency_overrides[get_db] = _override_db(_build_mock_db([_make_building_row()]))

    r = client.get(f"/api/v1/parcels/{_VALID_CAD}/neighbor-buildings?radius_m=500")

    assert r.status_code == 200, r.text
    assert r.json()["radius_m"] == 500


# ── битая геометрия (ST_AsGeoJSON NULL) пропускается, не роняет сцену ───────


def test_neighbor_buildings_skips_null_geometry(client: TestClient) -> None:
    rows = [
        _make_building_row("66:41:0204016:100", geojson=None),  # битый polygon
        _make_building_row("66:41:0204016:101", geojson=_POLYGON_GEOJSON),
    ]
    app.dependency_overrides[get_db] = _override_db(_build_mock_db(rows))

    r = client.get(f"/api/v1/parcels/{_VALID_CAD}/neighbor-buildings")

    assert r.status_code == 200, r.text
    body = r.json()
    assert body["count"] == 1
    assert body["features"][0]["properties"]["cad_num"] == "66:41:0204016:101"
