"""Tests for /api/v1/geocode/reverse — snapped coords + precision in response.

Bug context (issue #582 Phase 5):
До этого PR'а endpoint echo'ил input lat/lon в ответе. После reverse-геокодинга
адрес resolved'ился до canonical здания (ул. Малышева, 125), но marker на фронте
оставался где user кликнул — иногда в проезде / дворе.

Fix: backend теперь возвращает snapped_lat/snapped_lon (центр matched здания
от Yandex/Nominatim/cadastral) + precision, фронт двигает marker если precision
in (exact, number, cadastral).
"""

from __future__ import annotations

import os
import sys
from unittest.mock import AsyncMock, MagicMock, patch

# DATABASE_URL required by config before any app import.
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")

# WeasyPrint stub — not installed in CI without GTK.
_wp_mock = MagicMock()
sys.modules.setdefault("weasyprint", _wp_mock)


import pytest  # noqa: E402
from fastapi import FastAPI  # noqa: E402
from fastapi.testclient import TestClient  # noqa: E402

from app.api.v1 import geocode as geocode_module  # noqa: E402
from app.core.db import get_db  # noqa: E402
from app.services.geocoder import (  # noqa: E402
    ReverseGeocodeResult,
    snap_precision_useful,
)


@pytest.fixture
def app() -> FastAPI:
    application = FastAPI()
    application.include_router(geocode_module.router, prefix="/api/v1/geocode")
    application.dependency_overrides[get_db] = lambda: MagicMock()
    return application


# ── Endpoint response shape ──────────────────────────────────────────────────

def test_reverse_endpoint_returns_snapped_fields(app: FastAPI) -> None:
    """Endpoint should include address + lat/lon (echo) + snapped_lat/snapped_lon + precision."""
    client = TestClient(app)
    fake = ReverseGeocodeResult(
        address="улица Малышева, 51, Екатеринбург",
        snapped_lat=56.838004,
        snapped_lon=60.586155,
        precision="exact",
        provider="yandex",
    )
    with patch(
        "app.api.v1.geocode.reverse_geocode",
        new_callable=AsyncMock,
        return_value=fake,
    ):
        r = client.get("/api/v1/geocode/reverse?lat=56.8381&lon=60.5860")

    assert r.status_code == 200
    body = r.json()
    # Echo input
    assert body["lat"] == 56.8381
    assert body["lon"] == 60.5860
    # Snap from provider
    assert body["address"] == "улица Малышева, 51, Екатеринбург"
    assert body["snapped_lat"] == 56.838004
    assert body["snapped_lon"] == 60.586155
    assert body["precision"] == "exact"
    assert body["provider"] == "yandex"


def test_reverse_endpoint_404_when_no_address(app: FastAPI) -> None:
    """Provider returned None → endpoint должен дать 404, не 500."""
    client = TestClient(app)
    with patch(
        "app.api.v1.geocode.reverse_geocode",
        new_callable=AsyncMock,
        return_value=None,
    ):
        r = client.get("/api/v1/geocode/reverse?lat=56.0&lon=60.0")
    assert r.status_code == 404


def test_reverse_endpoint_street_precision_does_not_lose_snap_fields(app: FastAPI) -> None:
    """Для precision=street snapped_lat/lon всё равно присутствуют (могут == input)."""
    client = TestClient(app)
    fake = ReverseGeocodeResult(
        address="улица Малышева, Екатеринбург",
        snapped_lat=56.84,
        snapped_lon=60.61,
        precision="street",
        provider="nominatim",
    )
    with patch(
        "app.api.v1.geocode.reverse_geocode",
        new_callable=AsyncMock,
        return_value=fake,
    ):
        r = client.get("/api/v1/geocode/reverse?lat=56.84&lon=60.61")
    assert r.status_code == 200
    body = r.json()
    assert "snapped_lat" in body
    assert "snapped_lon" in body
    assert body["precision"] == "street"


# ── snap_precision_useful helper ─────────────────────────────────────────────

def test_snap_precision_useful_exact_and_number() -> None:
    assert snap_precision_useful("exact") is True
    assert snap_precision_useful("number") is True


def test_snap_precision_useful_rejects_street_and_other() -> None:
    assert snap_precision_useful("street") is False
    assert snap_precision_useful("range") is False
    assert snap_precision_useful("near") is False
    assert snap_precision_useful("locality") is False
    assert snap_precision_useful("other") is False
    assert snap_precision_useful("") is False


# ── Yandex reverse parsing ───────────────────────────────────────────────────

async def test_yandex_reverse_parses_snapped_point_and_precision() -> None:
    """`_yandex_reverse` извлекает Point.pos (lon lat) и precision из metaDataProperty."""
    import httpx

    from app.services.geocoder import _yandex_reverse

    sample = {
        "response": {
            "GeoObjectCollection": {
                "featureMember": [
                    {
                        "GeoObject": {
                            "metaDataProperty": {
                                "GeocoderMetaData": {
                                    "precision": "exact",
                                    "text": (
                                        "Россия, Свердловская область, "
                                        "Екатеринбург, улица Малышева, 51"
                                    ),
                                    "kind": "house",
                                }
                            },
                            "name": "улица Малышева, 51",
                            "Point": {"pos": "60.586155 56.838004"},
                        }
                    }
                ]
            }
        }
    }

    class _FakeResp:
        status_code = 200

        def raise_for_status(self) -> None:
            return None

        def json(self) -> dict:
            return sample

    class _FakeClient:
        async def __aenter__(self) -> _FakeClient:
            return self

        async def __aexit__(self, *_: object) -> None:
            return None

        async def get(self, *_: object, **__: object) -> _FakeResp:
            return _FakeResp()

    with patch.object(httpx, "AsyncClient", lambda *a, **kw: _FakeClient()):
        result = await _yandex_reverse(56.8381, 60.5860, api_key="fake")

    assert result is not None
    # Yandex pos формат: "lon lat" → snapped_lat=56.838004, snapped_lon=60.586155
    assert abs(result.snapped_lat - 56.838004) < 1e-6
    assert abs(result.snapped_lon - 60.586155) < 1e-6
    assert result.precision == "exact"
    assert result.provider == "yandex"
    # Address text должен быть очищен от "Россия, Свердловская область"
    assert "Россия" not in result.address
    assert "Свердловская область" not in result.address
    assert "Малышева" in result.address
    assert "51" in result.address


async def test_yandex_reverse_returns_none_on_empty_results() -> None:
    """Empty featureMember → None."""
    import httpx

    from app.services.geocoder import _yandex_reverse

    sample = {"response": {"GeoObjectCollection": {"featureMember": []}}}

    class _FakeResp:
        status_code = 200

        def raise_for_status(self) -> None:
            return None

        def json(self) -> dict:
            return sample

    class _FakeClient:
        async def __aenter__(self) -> _FakeClient:
            return self

        async def __aexit__(self, *_: object) -> None:
            return None

        async def get(self, *_: object, **__: object) -> _FakeResp:
            return _FakeResp()

    with patch.object(httpx, "AsyncClient", lambda *a, **kw: _FakeClient()):
        result = await _yandex_reverse(56.0, 60.0, api_key="fake")

    assert result is None
