"""Тесты OSRM road-distance в POST /api/v1/parcels/{cad}/analyze (#39 A2).

Покрывает:
1. flag ON + mocked OSRM → distance_m в score_breakdown заменён дорожным расстоянием,
   POI-вклад (score) отражает road-distance, а не straight-line.
2. flag ON + OSRM бросает OsrmLocalUnavailableError → fallback на straight-line,
   analyze возвращает 200 (НЕ 500), score = straight-line baseline.
3. flag OFF (default) → get_road_distances_m НЕ вызывается ни разу (байт-в-байт
   как сегодня).

Mock-стратегия как в test_analyze_inline_weights.py: DB через dependency_overrides,
тяжёлые service-вызовы через patch, OSRM-клиент через patch на
app.api.v1.parcels.get_road_distances_m.
"""

from __future__ import annotations

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

import pytest
from fastapi.testclient import TestClient

from app.core.config import settings
from app.main import app
from app.services.site_finder.osrm_client_local import OsrmLocalUnavailableError

_CAD = "66:41:0204016:10"
_WKT = "POLYGON((60.6 56.838, 60.61 56.838, 60.61 56.845, 60.6 56.845, 60.6 56.838))"
_GEOJSON = '{"type":"Polygon","coordinates":[[[60.6,56.838],[60.61,56.838]]]}'


def _make_mapping(data: dict[str, Any]) -> MagicMock:
    m = MagicMock()
    m.__getitem__ = lambda self, k: data[k]
    m.get = lambda k, default=None: data.get(k, default)
    return m


def _make_db_for_analyze(poi_rows: list[Any]) -> MagicMock:
    db = MagicMock()

    geom_row = _make_mapping({"geom_geojson": _GEOJSON, "geom_wkb": None, "source": "cad_quarter"})
    wkt_row = _make_mapping({"wkt": _WKT})
    district_row = _make_mapping(
        {
            "district_name": "Октябрьский",
            "ekb_reference_median": 120000,
            "quarter_median_12m": None,
            "quarter_deals_count": 0,
            "dist_to_center": 1500.0,
        }
    )
    centroid_row = _make_mapping({"lat": 56.84, "lon": 60.605})

    def _execute_side_effect(*args: Any, **kwargs: Any) -> MagicMock:
        sql = " ".join(str(args[0]).split()) if args else ""

        first_val: Any = None
        all_val: list[Any] = []

        if "AS geom_geojson" in sql:
            first_val = geom_row
        elif "AS wkt" in sql:
            first_val = wkt_row
        elif "AS ekb_reference_median" in sql and "district_name" in sql:
            first_val = district_row
        elif "AS lon" in sql and "AS lat" in sql:
            # И centroid_row для блока 6, и OSRM-origin SELECT (тот же shape).
            first_val = centroid_row
        elif "category" in sql and "ST_Distance" in sql and "p.geom" in sql:
            all_val = poi_rows

        r = MagicMock()
        r.mappings.return_value.first.return_value = first_val
        r.mappings.return_value.all.return_value = all_val
        r.scalar.return_value = 0
        return r

    db.execute.side_effect = _execute_side_effect

    ctx = MagicMock()
    ctx.__enter__ = MagicMock(return_value=ctx)
    ctx.__exit__ = MagicMock(return_value=False)
    db.begin_nested.return_value = ctx
    return db


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

    return _get_db_override


_PATCHES = [
    patch("app.api.v1.parcels.get_air_quality_cached", return_value=None),
    patch("app.api.v1.parcels.get_weather_cached", return_value=None),
    patch("app.api.v1.parcels.get_seasonal_weather_cached", return_value=None),
    patch(
        "app.api.v1.parcels.get_quarter_dump_data",
        return_value={
            "nspd_zoning": None,
            "nspd_zouit_overlaps": [],
            "nspd_engineering_nearby": [],
            "nspd_dump": {"available": False, "stale": False, "harvest_triggered": False},
        },
    ),
    patch("app.api.v1.parcels.compute_velocity", return_value=None),
    patch("app.api.v1.parcels.compute_gate_verdict", return_value={"verdict": "unknown"}),
    patch(
        "app.api.v1.parcels.build_ird_analyze_block",
        return_value={
            "ird_overlaps": [],
            "ird_by_kind": {},
            "opportunity_overlaps": [],
            "planning_projects": [],
            "functional_zone": None,
            "krt": [],
            "zone_regulation": None,
        },
    ),
]


def _start_patches() -> None:
    for p in _PATCHES:
        p.start()


def _stop_patches() -> None:
    for p in _PATCHES:
        p.stop()


def _poi_row(category: str, name: str, lon: float, lat: float, distance_m: float) -> MagicMock:
    """POI-строка как RowMapping: parcels.py делает dict(r), поэтому нужен .keys()+items."""
    data = {
        "category": category,
        "name": name,
        "lat": lat,
        "lon": lon,
        "distance_m": distance_m,
        "last_osm_edit_date": None,
    }
    m = MagicMock()
    m.__getitem__ = lambda self, k: data[k]
    m.keys = lambda: data.keys()
    m.__iter__ = lambda self: iter(data)
    return m


def _distance_in_breakdown(body: dict[str, Any], name: str) -> float:
    """Найти distance_m конкретного POI по имени в score_breakdown."""
    for _cat, items in body["score_breakdown"].items():
        for it in items:
            if it["name"] == name:
                return float(it["distance_m"])
    raise AssertionError(f"POI {name!r} не найден в score_breakdown: {body['score_breakdown']}")


def test_flag_on_osrm_replaces_distance(monkeypatch) -> None:
    """flag ON + mocked OSRM → distance_m заменён дорожным, score отражает road-distance."""
    from app.core.db import get_db

    # straight-line 200м; OSRM вернёт 700м (дорога длиннее) → меньший decay → меньший score.
    poi_rows = [_poi_row("school", "Школа №1", 60.61, 56.84, 200.0)]
    db = _make_db_for_analyze(poi_rows)
    app.dependency_overrides[get_db] = _override_db(db)
    monkeypatch.setattr(settings, "use_osrm_distances", True)
    _start_patches()
    try:
        with patch("app.api.v1.parcels.get_road_distances_m", return_value=[700.0]) as mock_osrm:
            client = TestClient(app)
            resp = client.post(f"/api/v1/parcels/{_CAD}/analyze")
            assert resp.status_code == 200, resp.text
            body = resp.json()
            # OSRM вызван ровно один раз (один POI-fetch).
            assert mock_osrm.call_count >= 1
            # distance_m в breakdown — дорожное (700), не straight-line (200).
            assert _distance_in_breakdown(body, "Школа №1") == pytest.approx(700.0, abs=1.0)
    finally:
        app.dependency_overrides.clear()
        _stop_patches()


def test_flag_on_osrm_unavailable_falls_back(monkeypatch) -> None:
    """flag ON + OSRM бросает → straight-line fallback, analyze=200 (НЕ 500)."""
    from app.core.db import get_db

    poi_rows = [_poi_row("school", "Школа №1", 60.61, 56.84, 200.0)]
    db = _make_db_for_analyze(poi_rows)
    app.dependency_overrides[get_db] = _override_db(db)
    monkeypatch.setattr(settings, "use_osrm_distances", True)
    _start_patches()
    try:
        with patch(
            "app.api.v1.parcels.get_road_distances_m",
            side_effect=OsrmLocalUnavailableError("osrm down"),
        ):
            client = TestClient(app)
            resp = client.post(f"/api/v1/parcels/{_CAD}/analyze")
            # КРИТИЧНО: /analyze НЕ падает в 500 когда OSRM лежит.
            assert resp.status_code == 200, resp.text
            body = resp.json()
            # distance_m остаётся straight-line baseline (200), OSRM проигнорирован.
            assert _distance_in_breakdown(body, "Школа №1") == pytest.approx(200.0, abs=1.0)
    finally:
        app.dependency_overrides.clear()
        _stop_patches()


def test_flag_off_never_calls_osrm(monkeypatch) -> None:
    """flag OFF (default) → get_road_distances_m НЕ вызывается, distance = straight-line."""
    from app.core.db import get_db

    poi_rows = [_poi_row("school", "Школа №1", 60.61, 56.84, 200.0)]
    db = _make_db_for_analyze(poi_rows)
    app.dependency_overrides[get_db] = _override_db(db)
    # Явно OFF (default, но фиксируем на случай env-override в окружении).
    monkeypatch.setattr(settings, "use_osrm_distances", False)
    _start_patches()
    try:
        with patch("app.api.v1.parcels.get_road_distances_m") as mock_osrm:
            client = TestClient(app)
            resp = client.post(f"/api/v1/parcels/{_CAD}/analyze")
            assert resp.status_code == 200, resp.text
            body = resp.json()
            # OSRM НЕ дёрнут ни разу — байт-в-байт как сегодня.
            assert mock_osrm.call_count == 0
            # distance_m = straight-line baseline.
            assert _distance_in_breakdown(body, "Школа №1") == pytest.approx(200.0, abs=1.0)
    finally:
        app.dependency_overrides.clear()
        _stop_patches()
