"""Тесты harvest_ird_overlays (#1067 B5) — НСПД ИРД-harvest → ird_overlays.

Сеть/БД не дёргаются: мокаем NSPDClient + SessionLocal, проверяем grid-walk loop,
skip битых фич и upsert-параметры.
"""

from __future__ import annotations

from contextlib import contextmanager
from typing import Any

from app.workers.tasks import ird_harvest


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


class _SearchRes:
    def __init__(self, feat: _Feat | None) -> None:
        self.first = feat


class _FakeClient:
    """NSPDClient-двойник: квартал → полигон (3857), grid-walk → 1 фича на слой."""

    def __init__(self, *a: Any, **kw: Any) -> None:
        self.grid_calls: list[int] = []

    def search_by_cad(self, cad: str, thematic_id: int = 1) -> _SearchRes:
        ring = [[0, 0], [100, 0], [100, 100], [0, 100], [0, 0]]
        geom = {"type": "Polygon", "coordinates": [ring]}
        return _SearchRes(_Feat(geom, {"cad": cad}))

    def get_features_in_bbox_grid(self, layer_id: int, bbox: Any, **kw: Any) -> list[_Feat]:
        self.grid_calls.append(layer_id)
        return [
            _Feat(
                {"type": "Polygon", "coordinates": [[[1, 1], [2, 1], [2, 2], [1, 1]]]},
                {
                    "geom_data_id": 1000 + layer_id,
                    "reg_numb_border": "66:41-6.1",
                    "category": 36940,
                    "subcategory": 5,
                },
            )
        ]


class _FakeDB:
    """Session-двойник: пишет execute-вызовы, begin_nested как no-op CM."""

    def __init__(self) -> None:
        self.executed: list[tuple[Any, dict[str, Any] | None]] = []
        self.committed = False
        self.commit_count = 0
        self.rollback_count = 0
        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
        self.commit_count += 1

    def rollback(self) -> None:
        self.rollback_count += 1

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


def _patch(monkeypatch: Any, db: _FakeDB) -> _FakeClient:
    client = _FakeClient()
    monkeypatch.setattr(ird_harvest, "NSPDClient", lambda *a, **kw: client)
    monkeypatch.setattr(ird_harvest, "SessionLocal", lambda: db)
    return client


def test_harvest_explicit_quarters_upserts_all_layers(monkeypatch: Any) -> None:
    """1 квартал × N ИРД-слоёв → upsert по фиче на слой; commit + close."""
    db = _FakeDB()
    client = _patch(monkeypatch, db)
    n_layers = len(ird_harvest.IRD_HARVEST_LAYER_KEYS)

    result = ird_harvest.harvest_ird_overlays(quarters=["66:41:0204016"])

    assert result == {"quarters": 1, "features": n_layers}
    assert len(client.grid_calls) == n_layers  # grid-walk по каждому слою
    assert len(db.executed) == n_layers  # upsert по фиче на слой
    assert db.committed is True
    assert db.closed is True
    # параметры upsert: geom как JSON-строка, layer_kind из ключей
    _, params = db.executed[0]
    assert params is not None
    assert params["layer_kind"] in ird_harvest.IRD_HARVEST_LAYER_KEYS
    assert isinstance(params["geojson"], str)
    assert params["reg_numb_border"] == "66:41-6.1"


def test_harvest_skips_empty_quarter(monkeypatch: Any) -> None:
    """Квартал без геометрии в НСПД → 0 кварталов/фич, без падения."""
    db = _FakeDB()
    client = _patch(monkeypatch, db)
    monkeypatch.setattr(client, "search_by_cad", lambda cad, thematic_id=1: _SearchRes(None))

    result = ird_harvest.harvest_ird_overlays(quarters=["66:41:9999999"])

    assert result == {"quarters": 0, "features": 0}
    assert db.executed == []
    assert db.committed is True


def test_upsert_feature_skips_missing_geom_data_id(monkeypatch: Any) -> None:
    """Фича без geom_data_id / geometry → не пишется (defensive)."""
    db = _FakeDB()
    no_id = _Feat({"type": "Polygon", "coordinates": []}, {"reg_numb_border": "x"})
    no_geom = _Feat(None, {"geom_data_id": 5})
    assert (
        ird_harvest._upsert_feature(
            db, layer_id=37577, layer_kind="zouit_okn", feature=no_id, fetched_at="t"
        )
        is False
    )
    assert (
        ird_harvest._upsert_feature(
            db, layer_id=37577, layer_kind="zouit_okn", feature=no_geom, fetched_at="t"
        )
        is False
    )
    assert db.executed == []


def test_per_quarter_commit_durability(monkeypatch: Any) -> None:
    """Каждый квартал коммитится отдельно → краш середины не теряет прошлые (фикс #1067)."""
    db = _FakeDB()
    _patch(monkeypatch, db)

    result = ird_harvest.harvest_ird_overlays(quarters=["66:41:0000001", "66:41:0000002"])

    assert result["quarters"] == 2
    # commit per-quarter (2) + финальный = >=2; точно больше одного (не «раз в конце»)
    assert db.commit_count >= 2


def test_quarter_failure_rolls_back_and_continues(monkeypatch: Any) -> None:
    """Сбой одного квартала → rollback + продолжение; остальные кварталы коммитятся."""
    db = _FakeDB()
    client = _patch(monkeypatch, db)

    def _flaky(cad: str, thematic_id: int = 1) -> Any:
        if cad == "66:41:0000001":
            raise RuntimeError("NSPD timeout")
        ring = [[0, 0], [100, 0], [100, 100], [0, 100], [0, 0]]
        return _SearchRes(_Feat({"type": "Polygon", "coordinates": [ring]}, {"cad": cad}))

    monkeypatch.setattr(client, "search_by_cad", _flaky)

    result = ird_harvest.harvest_ird_overlays(quarters=["66:41:0000001", "66:41:0000002"])

    assert db.rollback_count >= 1  # битый квартал откатился
    assert result["quarters"] == 1  # второй квартал обработан
    assert db.committed is True
