"""Тесты per-category OSRM routing в `_apply_osrm_road_distances` (#39 A3).

A3 чинит переоценку пешей доступности: walk-релевантные POI (school/shop/park/
остановки ∈ settings.osrm_walk_categories) маршрутизируются по FOOT-графу
(settings.osrm_walk_local_url, profile='foot'), всё остальное (incl. shop_mall,
hospital, неизвестная/None категория) — по DRIVING-графу (settings.osrm_local_url,
profile='driving') как безопасный дефолт.

Покрывает:
(a) разбиение по категориям: walk-POI → foot mock, drive-POI → car mock;
(b) per-group graceful fallback: один сервис лёг → его группа straight-line, другая
    группа всё равно получает дорожное расстояние;
(c) None-/неизвестная категория → drive-группа (безопасный дефолт);
(d) функция НИКОГДА не бросает (оба сервиса легли → straight-line, нет исключения).

`get_road_distances_m` мокается на app.api.v1.parcels.get_road_distances_m
(реальный OSRM не дёргается), маршрутизация различается по kwargs base_url/profile.
"""

from __future__ import annotations

from typing import Any
from unittest.mock import patch

from app.api.v1.parcels import _apply_osrm_road_distances
from app.core.config import settings
from app.services.site_finder.osrm_client_local import OsrmLocalUnavailableError


def _poi(category: str | None, lon: float, lat: float, distance_m: float) -> dict[str, Any]:
    return {"category": category, "lon": lon, "lat": lat, "distance_m": distance_m}


def test_splits_walk_and_drive_to_separate_servers() -> None:
    """walk-POI → foot-сервис/profile=foot; drive-POI → car-сервис/profile=driving."""
    pois = [
        _poi("school", 60.61, 56.84, 200.0),  # walk
        _poi("shop_mall", 60.62, 56.85, 500.0),  # drive (НЕ в walk-set)
        _poi("park", 60.63, 56.86, 300.0),  # walk
    ]

    def fake(origin_lon, origin_lat, dests, *, profile, base_url):
        # FOOT-граф вернёт 111 для каждой walk-точки, DRIVING — 999.
        if profile == "foot":
            assert base_url == settings.osrm_walk_local_url
            return [111.0] * len(dests)
        assert profile == "driving"
        assert base_url is None  # → settings.osrm_local_url (DRIVING)
        return [999.0] * len(dests)

    with patch("app.api.v1.parcels.get_road_distances_m", side_effect=fake) as mock:
        _apply_osrm_road_distances(pois, origin_lon=60.6, origin_lat=56.83, label="t")

    # Ровно два вызова: один foot, один driving.
    assert mock.call_count == 2
    profiles = {c.kwargs["profile"] for c in mock.call_args_list}
    assert profiles == {"foot", "driving"}

    # walk-POI получили foot-расстояние, drive-POI — driving-расстояние.
    assert pois[0]["distance_m"] == 111.0  # school → foot
    assert pois[1]["distance_m"] == 999.0  # shop_mall → driving
    assert pois[2]["distance_m"] == 111.0  # park → foot


def test_intra_group_writeback_is_position_correct() -> None:
    """Внутри группы расстояние пишется ИМЕННО своему POI (ловит транспозицию индексов).

    Каждый mock возвращает значение, привязанное к КООРДИНАТЕ точки
    (`round((lon-60.6)*10000)`), а не одинаковое на группу — поэтому перестановка
    внутри walk- или drive-группы при write-back завалила бы ассерты.
    """
    pois = [
        _poi("school", 60.61, 56.84, 1.0),  # walk → ждём 100
        _poi("shop_mall", 60.62, 56.85, 2.0),  # drive → ждём 200
        _poi("park", 60.63, 56.86, 3.0),  # walk → ждём 300
        _poi("hospital", 60.64, 56.87, 4.0),  # drive → ждём 400
    ]

    def fake(origin_lon, origin_lat, dests, *, profile, base_url):
        # Значение зависит от lon каждой точки → порядок внутри группы значим.
        return [round((lon - 60.6) * 10000) for lon, _lat in dests]

    with patch("app.api.v1.parcels.get_road_distances_m", side_effect=fake):
        _apply_osrm_road_distances(pois, origin_lon=60.6, origin_lat=56.83, label="t")

    assert pois[0]["distance_m"] == 100  # school (walk)
    assert pois[1]["distance_m"] == 200  # shop_mall (drive)
    assert pois[2]["distance_m"] == 300  # park (walk)
    assert pois[3]["distance_m"] == 400  # hospital (drive)


def test_unknown_and_none_category_route_via_drive() -> None:
    """None / неизвестная категория → DRIVING-граф (безопасный дефолт)."""
    pois = [
        _poi(None, 60.61, 56.84, 200.0),
        _poi("hospital", 60.62, 56.85, 400.0),  # явно drive-side
        _poi("totally_unknown", 60.63, 56.86, 600.0),
    ]

    def fake(origin_lon, origin_lat, dests, *, profile, base_url):
        # Не должно быть foot-вызова: все три POI — drive.
        assert profile == "driving"
        assert base_url is None
        return [42.0] * len(dests)

    with patch("app.api.v1.parcels.get_road_distances_m", side_effect=fake) as mock:
        _apply_osrm_road_distances(pois, origin_lon=60.6, origin_lat=56.83, label="t")

    assert mock.call_count == 1  # только driving-группа
    assert all(p["distance_m"] == 42.0 for p in pois)


def test_foot_server_down_drive_still_replaced() -> None:
    """FOOT-сервис лёг → walk-POI остаются straight-line, drive-POI всё равно заменены."""
    pois = [
        _poi("school", 60.61, 56.84, 200.0),  # walk → foot (упадёт)
        _poi("shop_mall", 60.62, 56.85, 500.0),  # drive → ok
    ]

    def fake(origin_lon, origin_lat, dests, *, profile, base_url):
        if profile == "foot":
            raise OsrmLocalUnavailableError("foot down")
        return [999.0] * len(dests)

    with patch("app.api.v1.parcels.get_road_distances_m", side_effect=fake):
        _apply_osrm_road_distances(pois, origin_lon=60.6, origin_lat=56.83, label="t")

    # walk-POI: foot лёг → straight-line baseline сохранён.
    assert pois[0]["distance_m"] == 200.0
    # drive-POI: DRIVING-граф жив → дорожное расстояние применено.
    assert pois[1]["distance_m"] == 999.0


def test_drive_server_down_walk_still_replaced() -> None:
    """DRIVING-сервис лёг → drive-POI straight-line, walk-POI всё равно заменены (foot жив)."""
    pois = [
        _poi("school", 60.61, 56.84, 200.0),  # walk → foot ok
        _poi("shop_mall", 60.62, 56.85, 500.0),  # drive (упадёт)
    ]

    def fake(origin_lon, origin_lat, dests, *, profile, base_url):
        if profile == "driving":
            raise OsrmLocalUnavailableError("driving down")
        return [111.0] * len(dests)

    with patch("app.api.v1.parcels.get_road_distances_m", side_effect=fake):
        _apply_osrm_road_distances(pois, origin_lon=60.6, origin_lat=56.83, label="t")

    # walk-POI: FOOT-граф жив → дорожное (пешее) расстояние применено.
    assert pois[0]["distance_m"] == 111.0
    # drive-POI: DRIVING лёг → straight-line baseline сохранён.
    assert pois[1]["distance_m"] == 500.0


def test_both_servers_down_never_raises() -> None:
    """Оба OSRM легли → функция НЕ бросает, все POI остаются straight-line."""
    pois = [
        _poi("school", 60.61, 56.84, 200.0),
        _poi("shop_mall", 60.62, 56.85, 500.0),
    ]

    with patch(
        "app.api.v1.parcels.get_road_distances_m",
        side_effect=OsrmLocalUnavailableError("all down"),
    ):
        # Не должно поднять исключение — /analyze не падает.
        _apply_osrm_road_distances(pois, origin_lon=60.6, origin_lat=56.83, label="t")

    assert pois[0]["distance_m"] == 200.0
    assert pois[1]["distance_m"] == 500.0


def test_poi_without_coords_skipped() -> None:
    """POI без lon/lat не попадает ни в одну группу (сохраняет straight-line)."""
    pois = [
        _poi("school", 60.61, 56.84, 200.0),  # walk
        {"category": "shop_mall", "lon": None, "lat": None, "distance_m": 500.0},  # skip
    ]

    def fake(origin_lon, origin_lat, dests, *, profile, base_url):
        # Только walk-группа с одной точкой; drive-группа пуста → нет driving-вызова.
        assert profile == "foot"
        assert len(dests) == 1
        return [111.0]

    with patch("app.api.v1.parcels.get_road_distances_m", side_effect=fake) as mock:
        _apply_osrm_road_distances(pois, origin_lon=60.6, origin_lat=56.83, label="t")

    assert mock.call_count == 1  # только foot (drive-группа пуста)
    assert pois[0]["distance_m"] == 111.0
    assert pois[1]["distance_m"] == 500.0  # без координат — не тронут


def test_no_routable_pois_early_return() -> None:
    """Нет POI с координатами → ранний выход, OSRM не дёргается вообще."""
    pois = [
        {"category": "school", "lon": None, "lat": None, "distance_m": 200.0},
    ]

    with patch("app.api.v1.parcels.get_road_distances_m") as mock:
        _apply_osrm_road_distances(pois, origin_lon=60.6, origin_lat=56.83, label="t")

    assert mock.call_count == 0
    assert pois[0]["distance_m"] == 200.0
