"""Тесты krt_geometry_sync (#1130) — WFS portal_geo_oks_krt → ekb_krt_geometry.

Сеть/БД не дёргаются: мокаем EKBGeoportalClient + SessionLocal, проверяем upsert-идемпотентность
(одни и те же feature_id → ON CONFLICT → UPDATE), skip фич без геометрии, graceful WFS-ошибку.
"""

from __future__ import annotations

from contextlib import contextmanager
from typing import Any

import pytest

from app.workers.tasks import krt_geometry_sync


class _Feat:
    """EKBFeature-двойник."""

    def __init__(
        self,
        feature_id: str | None,
        geometry: dict[str, Any] | None,
        properties: dict[str, Any],
    ) -> None:
        self.feature_id = feature_id
        self.geometry = geometry
        self.properties = properties


_GEOM = {
    "type": "Polygon",
    "coordinates": [[[60.6, 56.8], [60.7, 56.8], [60.7, 56.9], [60.6, 56.8]]],
}

_FEAT_1 = _Feat(
    "portal_geo_oks_krt.7",
    _GEOM,
    {"num_oks_krt": 7, "full_name": "КРТ ул. Тверитина", "status": "В работе", "area": 3.5},
)
_FEAT_2 = _Feat(
    "portal_geo_oks_krt.12",
    _GEOM,
    {"num_oks_krt": 12, "full_name": "КРТ ул. Ленина", "status": "Завершён", "area": 1.2},
)


class _FakeClient:
    def __init__(self, features: list[_Feat], *, boom: bool = False) -> None:
        self._features = features
        self._boom = boom
        self.bbox_calls: list[tuple[str, Any]] = []

    def features_in_bbox(self, layer: str, bbox: Any) -> list[_Feat]:
        self.bbox_calls.append((layer, bbox))
        if self._boom:
            raise RuntimeError("geoportal timeout")
        return self._features


class _FakeDB:
    def __init__(self) -> None:
        self.executed: list[tuple[Any, dict[str, Any] | None]] = []
        self.committed = False
        self.closed = False

    @contextmanager
    def begin_nested(self) -> Any:
        yield

    def execute(self, sql: Any, params: dict[str, Any] | None = None) -> Any:
        self.executed.append((sql, params))
        return None

    def commit(self) -> None:
        self.committed = True

    def rollback(self) -> None:
        pass

    def close(self) -> None:
        self.closed = True


def _patch(monkeypatch: Any, features: list[_Feat], *, boom: bool = False) -> _FakeDB:
    fake_client = _FakeClient(features, boom=boom)
    fake_db = _FakeDB()
    monkeypatch.setattr(krt_geometry_sync, "EKBGeoportalClient", lambda *a, **kw: fake_client)
    monkeypatch.setattr(krt_geometry_sync, "SessionLocal", lambda: fake_db)
    return fake_db


def test_upsert_two_features(monkeypatch: Any) -> None:
    """Два WFS-feature → два UPSERT-вызова; возвращает {"krt": 2}."""
    fake_db = _patch(monkeypatch, [_FEAT_1, _FEAT_2])

    result = krt_geometry_sync.sync_krt_geometry()

    assert result == {"krt": 2}
    assert fake_db.committed
    assert fake_db.closed
    # По одному execute на каждую фичу
    assert len(fake_db.executed) == 2


def test_upsert_idempotent_same_feature_id(monkeypatch: Any) -> None:
    """Одна и та же feature_id дважды → 2 execute (ON CONFLICT DO UPDATE — идемпотентно)."""
    fake_db = _patch(monkeypatch, [_FEAT_1, _FEAT_1])

    result = krt_geometry_sync.sync_krt_geometry()

    # Воркер не дедуплицирует на стороне Python — ON CONFLICT в SQL.
    assert result == {"krt": 2}
    assert len(fake_db.executed) == 2


def test_skip_feature_without_geometry(monkeypatch: Any) -> None:
    """Фича без геометрии пропускается (failed++), остальные обрабатываются."""
    no_geom = _Feat("portal_geo_oks_krt.99", None, {"num_oks_krt": 99})
    fake_db = _patch(monkeypatch, [no_geom, _FEAT_1])

    result = krt_geometry_sync.sync_krt_geometry()

    # Только _FEAT_1 дошёл до UPSERT
    assert result == {"krt": 1}
    assert len(fake_db.executed) == 1


def test_empty_wfs_returns_zero(monkeypatch: Any) -> None:
    """WFS вернул 0 фич → {"krt": 0}, никаких DB-вызовов."""
    fake_db = _patch(monkeypatch, [])

    result = krt_geometry_sync.sync_krt_geometry()

    assert result == {"krt": 0}
    assert len(fake_db.executed) == 0


def test_wfs_error_propagates(monkeypatch: Any) -> None:
    """Сбой WFS (boom=True) → Exception пробрасывается (harvest не должен глотать ошибку)."""
    _patch(monkeypatch, [], boom=True)

    with pytest.raises(RuntimeError, match="geoportal timeout"):
        krt_geometry_sync.sync_krt_geometry()


def test_upsert_params_contain_feature_id_and_geom(monkeypatch: Any) -> None:
    """Параметры UPSERT содержат feature_id, geom_json (GeoJSON), num_oks_krt."""
    fake_db = _patch(monkeypatch, [_FEAT_1])

    krt_geometry_sync.sync_krt_geometry()

    assert len(fake_db.executed) == 1
    _, params = fake_db.executed[0]
    assert params is not None
    assert params["feature_id"] == "portal_geo_oks_krt.7"
    assert params["num_oks_krt"] == 7
    assert params["status"] == "В работе"
    assert params["full_name"] == "КРТ ул. Тверитина"
    assert "geom_json" in params
    import json

    geom = json.loads(params["geom_json"])
    assert geom["type"] == "Polygon"


def test_savepoint_skips_bad_row(monkeypatch: Any) -> None:
    """UPSERT падает для одной строки (SAVEPOINT) → остальные обработаны, задача не падает."""
    call_count = 0

    class _FakeDBWithError(_FakeDB):
        @contextmanager
        def begin_nested(self) -> Any:
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                raise RuntimeError("constraint violation")
            yield

    fake_client = _FakeClient([_FEAT_1, _FEAT_2])
    fake_db = _FakeDBWithError()
    monkeypatch.setattr(krt_geometry_sync, "EKBGeoportalClient", lambda *a, **kw: fake_client)
    monkeypatch.setattr(krt_geometry_sync, "SessionLocal", lambda: fake_db)

    result = krt_geometry_sync.sync_krt_geometry()

    # Первая строка упала в SAVEPOINT, вторая прошла
    assert result == {"krt": 1}
    assert fake_db.committed
