"""Тесты gisogd_so_client (#1058) — каталог-резолв + WFS fetch.

Сеть не дёргается: мокаем _http_get_json на module-level.
"""

from __future__ import annotations

from typing import Any

from app.services.scrapers import gisogd_so_client
from app.services.scrapers.gisogd_so_client import (
    _LAYER_FALLBACK,
    _find_layer_in_tree,
    fetch_functional_zones,
    resolve_functional_zone_layer,
)

# ── _find_layer_in_tree ───────────────────────────────────────────────────────

_LAYER_KNOWN = "agate_ekbgo:agate_ekbgo_geo_urban_10_functional_zone_f6c5926"
_LAYER_ABC = "agate_ekbgo:agate_ekbgo_geo_urban_10_functional_zone_abc123"
_LAYER_XYZ = "agate_ekbgo:agate_ekbgo_geo_urban_10_functional_zone_xyz789"


def test_find_layer_direct_match() -> None:
    """Узел с name == 'Функциональные зоны' → возвращает value."""
    node = {"name": "Функциональные зоны", "value": _LAYER_KNOWN}
    result = _find_layer_in_tree(node)
    assert result == _LAYER_KNOWN


def test_find_layer_in_nested_children() -> None:
    """Вложенный узел в children → резолвится рекурсивно."""
    catalog = {
        "name": "root",
        "children": [
            {
                "name": "Зонирование",
                "children": [
                    {"name": "Функциональные зоны", "value": _LAYER_ABC},
                ],
            },
        ],
    }
    result = _find_layer_in_tree(catalog)
    assert result == _LAYER_ABC


def test_find_layer_not_found_returns_none() -> None:
    """Узел не найден → None."""
    catalog = {"name": "Другой слой", "value": "some:layer", "children": []}
    result = _find_layer_in_tree(catalog)
    assert result is None


def test_find_layer_non_dict_node_returns_none() -> None:
    """Нода не dict → None (безопасный обход)."""
    assert _find_layer_in_tree(None) is None  # type: ignore[arg-type]
    assert _find_layer_in_tree("string") is None  # type: ignore[arg-type]
    assert _find_layer_in_tree([]) is None  # type: ignore[arg-type]


# ── resolve_functional_zone_layer ─────────────────────────────────────────────


def test_resolve_layer_from_catalog(monkeypatch: Any) -> None:
    """Каталог доступен → корректный layer из дерева."""
    catalog = {
        "name": "root",
        "children": [
            {"name": "Функциональные зоны", "value": _LAYER_XYZ},
        ],
    }
    monkeypatch.setattr(gisogd_so_client, "_http_get_json", lambda *a, **kw: catalog)

    result = resolve_functional_zone_layer()
    assert result == _LAYER_XYZ


def test_resolve_layer_fallback_on_catalog_error(monkeypatch: Any) -> None:
    """Каталог недоступен → fallback-хардкод."""

    def _raise(*a: Any, **kw: Any) -> None:
        raise Exception("connection refused")

    monkeypatch.setattr(gisogd_so_client, "_http_get_json", _raise)

    result = resolve_functional_zone_layer()
    assert result == _LAYER_FALLBACK


def test_resolve_layer_fallback_node_not_found(monkeypatch: Any) -> None:
    """Каталог пришёл но нужного узла нет → fallback-хардкод."""
    catalog = {
        "name": "root",
        "children": [{"name": "Другой слой", "value": "some:other:layer"}],
    }
    monkeypatch.setattr(gisogd_so_client, "_http_get_json", lambda *a, **kw: catalog)

    result = resolve_functional_zone_layer()
    assert result == _LAYER_FALLBACK


# ── fetch_functional_zones ────────────────────────────────────────────────────


def _make_fc(n: int) -> dict[str, Any]:
    """FeatureCollection с n фичами."""
    return {
        "type": "FeatureCollection",
        "features": [
            {
                "geometry": {"type": "Polygon", "coordinates": []},
                "properties": {"urban10_globalid": f"uuid-{i}"},
            }
            for i in range(n)
        ],
    }


def test_fetch_returns_features_single_request(monkeypatch: Any) -> None:
    """Успешный одиночный WFS-запрос → список фич."""
    fc = _make_fc(6436)
    calls: list[dict[str, Any]] = []

    def _mock_get(url: str, params: dict[str, str] | None = None, **kw: Any) -> Any:
        calls.append({"url": url, "params": params})
        return fc

    monkeypatch.setattr(gisogd_so_client, "_http_get_json", _mock_get)

    features = fetch_functional_zones("agate_ekbgo:test_layer")
    assert len(features) == 6436
    assert len(calls) == 1  # один запрос
    assert calls[0]["params"]["typeNames"] == "agate_ekbgo:test_layer"


def test_fetch_pagination_on_timeout(monkeypatch: Any) -> None:
    """Первый запрос — TimeoutException → постраничная загрузка."""
    import httpx

    first_call = True

    def _mock_get(url: str, params: dict[str, str] | None = None, **kw: Any) -> Any:
        nonlocal first_call
        if first_call:
            first_call = False
            raise httpx.TimeoutException("timeout")
        # Постраничные: 3 фичи на первой странице, пустая на второй.
        start = int((params or {}).get("startIndex", 0))
        if start == 0:
            return _make_fc(3)
        return {"type": "FeatureCollection", "features": []}

    monkeypatch.setattr(gisogd_so_client, "_http_get_json", _mock_get)

    features = fetch_functional_zones("agate_ekbgo:test_layer")
    assert len(features) == 3


def test_fetch_empty_on_wfs_error(monkeypatch: Any) -> None:
    """WFS не отвечает → пустой список, без падения."""

    def _raise(*a: Any, **kw: Any) -> None:
        raise Exception("connection error")

    monkeypatch.setattr(gisogd_so_client, "_http_get_json", _raise)

    features = fetch_functional_zones("agate_ekbgo:test_layer")
    assert features == []


def test_fetch_resolves_layer_when_none(monkeypatch: Any) -> None:
    """type_name=None → вызывается resolve_functional_zone_layer перед WFS."""
    resolved: list[str] = []

    def _mock_resolve() -> str:
        resolved.append("called")
        return "agate_ekbgo:resolved_layer"

    def _mock_get(url: str, params: dict[str, str] | None = None, **kw: Any) -> Any:
        return _make_fc(2)

    monkeypatch.setattr(gisogd_so_client, "resolve_functional_zone_layer", _mock_resolve)
    monkeypatch.setattr(gisogd_so_client, "_http_get_json", _mock_get)

    features = fetch_functional_zones(type_name=None)
    assert resolved == ["called"]
    assert len(features) == 2
