"""Тесты geom-match domrf↔cad_buildings + wire parking_ratio (#96 wiring).

Без живой БД и без сети: мокаем db.execute (PostGIS KNN-строка), get_competitors,
resolve_cad_for_domrf и get_building_premises_for_match. Проверяем:
  • resolve_cad_for_domrf — точка внутри footprint → cad_num; вне радиуса → None;
    нет зданий → None; objdoc_id из cad_buildings прокидывается.
  • get_competitors_parking — top-N конкурентов получают parking_ratio; промах
    geom-match / None-premises → поля None (graceful, не падаем), matched_count.
"""

from __future__ import annotations

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

os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")

from app.schemas.nspd_bulk import ObjectsListing
from app.schemas.parcel import (
    Competitor,
    CompetitorsRequest,
    CompetitorsResponse,
    CompetitorsSummary,
)
from app.services.site_finder import competitors as competitors_mod
from app.services.site_finder import premises_lookup as pl
from app.services.site_finder.premises_lookup import BuildingMatch, resolve_cad_for_domrf

# ── resolve_cad_for_domrf (geom-match) ─────────────────────────────────────────


def _db_returning(row: dict[str, Any] | None) -> MagicMock:
    """Мок Session.execute(...).mappings().first() → row (или None)."""
    db = MagicMock()
    db.execute.return_value.mappings.return_value.first.return_value = row
    return db


def test_resolve_cad_inside_footprint_returns_cad() -> None:
    """Точка внутри footprint (distance 0) → BuildingMatch с cad_num + objdoc_id."""
    db = _db_returning({"cad_num": "66:41:0106036:183", "objdoc_id": 40995027, "distance_m": 0.0})

    match = resolve_cad_for_domrf(db, lat=56.84, lon=60.6)

    assert match is not None
    assert match.cad_num == "66:41:0106036:183"
    assert match.objdoc_id == 40995027
    assert match.distance_m == 0.0


def test_resolve_cad_within_radius_returns_cad() -> None:
    """Ближайшее здание в пределах радиуса (30м ≤ 50м) → матч."""
    db = _db_returning({"cad_num": "66:41:0106036:9", "objdoc_id": None, "distance_m": 30.0})

    match = resolve_cad_for_domrf(db, lat=56.84, lon=60.6)

    assert match is not None
    assert match.cad_num == "66:41:0106036:9"
    # objdoc_id NULL в cad_buildings → None (фоллбэк на cad_num search в lookup).
    assert match.objdoc_id is None
    assert match.distance_m == 30.0


def test_resolve_cad_beyond_radius_returns_none() -> None:
    """Ближайшее здание дальше радиуса (80м > 50м) → None (не приписываем чужое)."""
    db = _db_returning({"cad_num": "66:41:0106036:1", "objdoc_id": 1, "distance_m": 80.0})

    assert resolve_cad_for_domrf(db, lat=56.84, lon=60.6) is None


def test_resolve_cad_custom_radius_accepts_far_match() -> None:
    """max_dist_m=100 принимает 80м-матч, который дефолтные 50м отвергли бы."""
    db = _db_returning({"cad_num": "66:41:0106036:1", "objdoc_id": 1, "distance_m": 80.0})

    match = resolve_cad_for_domrf(db, lat=56.84, lon=60.6, max_dist_m=100.0)

    assert match is not None
    assert match.cad_num == "66:41:0106036:1"


def test_resolve_cad_no_buildings_returns_none() -> None:
    """Нет зданий с geom (пустой результат) → None."""
    db = _db_returning(None)

    assert resolve_cad_for_domrf(db, lat=56.84, lon=60.6) is None


def test_resolve_cad_sql_error_returns_none() -> None:
    """SQL/PostGIS-сбой → None (graceful, lazy-endpoint не падает)."""
    db = MagicMock()
    db.execute.side_effect = RuntimeError("postgis boom")

    assert resolve_cad_for_domrf(db, lat=56.84, lon=60.6) is None


# ── get_competitors_parking (wire) ──────────────────────────────────────────────


def _competitor(obj_id: int, lat: float = 56.84, lng: float = 60.6) -> Competitor:
    """Минимальный Competitor для wire-теста (только нужные wire поля заполнены)."""
    return Competitor(
        obj_id=obj_id,
        comm_name=f"ЖК-{obj_id}",
        dev_name=None,
        obj_class="комфорт",
        distance_m=100.0,
        lat=lat,
        lng=lng,
        stage="Строящиеся",
        flats_total=200,
        flats_sold=None,
        sold_pct=None,
        velocity_per_month=5.0,
        avg_price_per_m2=None,
        is_active=True,
    )


def _competitors_response(*obj_ids: int) -> CompetitorsResponse:
    comps = [_competitor(oid) for oid in obj_ids]
    return CompetitorsResponse(
        competitors=comps,
        summary=CompetitorsSummary(
            total_competitors=len(comps),
            active_count=len(comps),
            weighted_avg_velocity=5.0,
            radius_km=1.0,
            time_window="last_quarter",
        ),
    )


def test_parking_wire_happy_path(monkeypatch: Any) -> None:
    """top-конкурент сматчился на здание → получает parking_ratio из НСПД."""
    monkeypatch.setattr(
        competitors_mod, "get_competitors", lambda **_: _competitors_response(101)
    )
    monkeypatch.setattr(
        competitors_mod,
        "resolve_cad_for_domrf",
        lambda *_a, **_k: BuildingMatch("66:41:0106036:183", 40995027, 12.3),
    )
    listing = ObjectsListing(objdoc_id=40995027, flats_count=200, parking_count=100)
    monkeypatch.setattr(
        competitors_mod, "get_building_premises_for_match", lambda _m: listing
    )

    out = competitors_mod.get_competitors_parking(
        MagicMock(), "66:41:0303001:1", CompetitorsRequest()
    )

    assert out.matched_count == 1
    assert len(out.items) == 1
    item = out.items[0]
    assert item.obj_id == 101
    assert item.matched_cad_num == "66:41:0106036:183"
    assert item.match_distance_m == 12.3
    assert item.parking_ratio == 0.5
    assert item.parking_count == 100
    assert item.flats_count == 200


def test_parking_wire_limits_to_top_n(monkeypatch: Any) -> None:
    """Из 5 конкурентов обогащаются только первые top_n (строгий лимит)."""
    monkeypatch.setattr(
        competitors_mod,
        "get_competitors",
        lambda **_: _competitors_response(1, 2, 3, 4, 5),
    )
    calls: list[float] = []

    def _resolve(_db: Any, *, lat: float, lon: float) -> BuildingMatch:
        calls.append(lat)
        return BuildingMatch("66:41:0106036:1", 1, 0.0)

    monkeypatch.setattr(competitors_mod, "resolve_cad_for_domrf", _resolve)
    monkeypatch.setattr(
        competitors_mod,
        "get_building_premises_for_match",
        lambda _m: ObjectsListing(objdoc_id=1, flats_count=10, parking_count=5),
    )

    out = competitors_mod.get_competitors_parking(
        MagicMock(), "66:41:0303001:1", CompetitorsRequest(), top_n=2
    )

    assert len(out.items) == 2
    assert len(calls) == 2  # geom-match вызван ровно top_n раз — не на всех 5
    assert {i.obj_id for i in out.items} == {1, 2}


def test_parking_wire_no_geom_match_graceful(monkeypatch: Any) -> None:
    """geom-match промахнулся → конкурент в items с None-полями, matched_count=0."""
    monkeypatch.setattr(
        competitors_mod, "get_competitors", lambda **_: _competitors_response(7)
    )
    monkeypatch.setattr(competitors_mod, "resolve_cad_for_domrf", lambda *_a, **_k: None)
    premises = MagicMock()
    monkeypatch.setattr(competitors_mod, "get_building_premises_for_match", premises)

    out = competitors_mod.get_competitors_parking(
        MagicMock(), "66:41:0303001:1", CompetitorsRequest()
    )

    assert out.matched_count == 0
    assert out.items[0].obj_id == 7
    assert out.items[0].matched_cad_num is None
    assert out.items[0].parking_ratio is None
    premises.assert_not_called()  # без матча НСПД не дёргаем


def test_parking_wire_premises_none_keeps_cad(monkeypatch: Any) -> None:
    """Здание сматчилось, но НСПД premises=None → cad есть, parking_ratio None."""
    monkeypatch.setattr(
        competitors_mod, "get_competitors", lambda **_: _competitors_response(9)
    )
    monkeypatch.setattr(
        competitors_mod,
        "resolve_cad_for_domrf",
        lambda *_a, **_k: BuildingMatch("66:41:0106036:5", None, 40.0),
    )
    monkeypatch.setattr(competitors_mod, "get_building_premises_for_match", lambda _m: None)

    out = competitors_mod.get_competitors_parking(
        MagicMock(), "66:41:0303001:1", CompetitorsRequest()
    )

    assert out.matched_count == 0
    assert out.items[0].matched_cad_num == "66:41:0106036:5"
    assert out.items[0].match_distance_m == 40.0
    assert out.items[0].parking_ratio is None


def test_parking_wire_premises_exception_graceful(monkeypatch: Any) -> None:
    """Неожиданное исключение в premises-lookup → конкурент без паркинга, не 500."""
    monkeypatch.setattr(
        competitors_mod, "get_competitors", lambda **_: _competitors_response(11)
    )
    monkeypatch.setattr(
        competitors_mod,
        "resolve_cad_for_domrf",
        lambda *_a, **_k: BuildingMatch("66:41:0106036:5", 1, 0.0),
    )

    def _boom(_m: Any) -> Any:
        raise RuntimeError("nspd exploded")

    monkeypatch.setattr(competitors_mod, "get_building_premises_for_match", _boom)

    out = competitors_mod.get_competitors_parking(
        MagicMock(), "66:41:0303001:1", CompetitorsRequest()
    )

    assert out.matched_count == 0
    assert out.items[0].matched_cad_num == "66:41:0106036:5"
    assert out.items[0].parking_ratio is None


# ── get_building_premises_for_match (short-circuit objdoc) ──────────────────────


def test_premises_for_match_uses_objdoc_skips_search(monkeypatch: Any) -> None:
    """objdoc_id из cad_buildings → пропускаем НСПД search, сразу tab-group."""
    listing = ObjectsListing(objdoc_id=555, flats_count=50, parking_count=60)
    monkeypatch.setattr(pl.asyncio, "run", lambda coro: (coro.close(), listing)[1])
    # search_by_cad НЕ должен вызываться при known objdoc_id.
    full_path = MagicMock()
    monkeypatch.setattr(pl, "get_building_premises", full_path)

    out = pl.get_building_premises_for_match(BuildingMatch("66:41:0106036:5", 555, 0.0))

    assert out is listing
    full_path.assert_not_called()


def test_premises_for_match_no_objdoc_falls_back(monkeypatch: Any) -> None:
    """objdoc_id None → фоллбэк на get_building_premises(cad_num) (2 НСПД-запроса)."""
    listing = ObjectsListing(objdoc_id=1, flats_count=10, parking_count=3)
    fallback = MagicMock(return_value=listing)
    monkeypatch.setattr(pl, "get_building_premises", fallback)

    out = pl.get_building_premises_for_match(BuildingMatch("66:41:0106036:5", None, 0.0))

    assert out is listing
    fallback.assert_called_once_with("66:41:0106036:5")
