"""Тесты изолированной write-сессии в get_or_fetch_zone_regulation (#1850 item 4).

На cache-MISS резолвер пишет регламент в кэш в СОБСТВЕННОЙ короткой сессии (SessionLocal()),
а НЕ коммитит SHARED-сессию вызывающего (/analyze коммитит её ровно один раз в конце через
persist_analysis_run). Проверяем:
  - happy path: shared db.commit НИКОГДА не вызывается, fresh write-session коммитит РОВНО раз,
    сессия закрыта; read-back идёт по SHARED db (форма результата сохранена для callers);
  - graceful: upsert_zone_regulation вернул None (таблица недоступна) → fresh session rollback,
    shared db не тронута, функция возвращает результат read-back без падения.

Сеть/БД не дёргаются: фейковый клиент + monkeypatch SessionLocal (recording-фейк) +
отдельная фейковая SHARED Session, считающая commit/rollback.
"""

from __future__ import annotations

from typing import Any

import pytest

from app.services.scrapers.ekb_geoportal_client import ZoneRegulation
from app.services.site_finder import zone_regulation as zr


def _reg(idx: str) -> ZoneRegulation:
    return ZoneRegulation(
        zone_index=idx,
        zone_full_name=f"{idx} зона",
        main_vri=[],
        conditional_vri=[],
        auxiliary_vri=[],
        limit_params=[],
    )


class _MissThenResolveClient:
    """Фейковый WFS-клиент: индекс зоны есть, но кэш промахивается → live-резолв регламента."""

    def __init__(self, zone_index: str) -> None:
        self._zone_index = zone_index
        self.index_calls = 0
        self.regulation_calls = 0

    def zone_index_at(self, lon: float, lat: float) -> str:
        self.index_calls += 1
        return self._zone_index

    def zone_regulation_at(self, lon: float, lat: float) -> ZoneRegulation:
        self.regulation_calls += 1
        return _reg(self._zone_index)


class _SharedDB:
    """Фейковая SHARED Session вызывающего: считает commit/rollback, ничего не пишет.

    commit() должен остаться НЕТРОНУТЫМ — фикс #1850 item 4 запрещает преждевременный
    коммит in-flight analyze-транзакции из резолвера.
    """

    def __init__(self) -> None:
        self.commits = 0
        self.rollbacks = 0

    def commit(self) -> None:
        self.commits += 1

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


class _WriteDB:
    """Фейковая короткоживущая write-сессия (то, что отдаёт monkeypatched SessionLocal)."""

    def __init__(self) -> None:
        self.commits = 0
        self.rollbacks = 0
        self.closed = False

    def commit(self) -> None:
        self.commits += 1

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

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


@pytest.fixture(autouse=True)
def _reset_memo() -> Any:
    """Чистый memo до/после теста (процесс-локальный кэш течёт между тестами)."""
    zr._reset_zone_index_memo()
    yield
    zr._reset_zone_index_memo()


def test_cache_miss_writes_in_isolated_session(monkeypatch: Any) -> None:
    """Cache-miss: shared db.commit НЕ вызывается, fresh write-session коммитит РОВНО раз."""
    write_db = _WriteDB()
    created: list[_WriteDB] = []

    def _session_local() -> _WriteDB:
        created.append(write_db)
        return write_db

    monkeypatch.setattr(zr, "SessionLocal", _session_local)
    upserts: list[ZoneRegulation] = []
    # upsert вернул params_numeric (не None) → write-сессия должна закоммитить.
    monkeypatch.setattr(
        zr, "upsert_zone_regulation", lambda db, reg, **kw: upserts.append(reg) or {"x": [1]}
    )
    # get_cached дёргается на SHARED db ДВАЖДЫ: 1) до fetch (cache MISS → None, форсит резолв),
    # 2) read-back после commit write-сессии (READ COMMITTED видит строку → dict для callers).
    cache_reads: list[Any] = []

    def _cached(db: Any, zone_index: str | None, **kw: Any) -> dict[str, Any] | None:
        cache_reads.append(db)
        assert db is shared  # резолвер читает кэш ИМЕННО по SHARED db, не по write-сессии
        return None if len(cache_reads) == 1 else {"zone_index": zone_index}

    monkeypatch.setattr(zr, "get_cached_zone_regulation", _cached)

    shared = _SharedDB()
    client = _MissThenResolveClient("Ц-1")
    result = zr.get_or_fetch_zone_regulation(shared, 60.6, 56.8, client=client)  # type: ignore[arg-type]

    # SHARED транзакция НЕ тронута резолвером.
    assert shared.commits == 0
    assert shared.rollbacks == 0
    # Свежая write-сессия закоммитила РОВНО один раз и закрыта.
    assert len(created) == 1
    assert write_db.commits == 1
    assert write_db.rollbacks == 0
    assert write_db.closed is True
    # Апсёрт ушёл в write-сессию, не в shared.
    assert upserts == [_reg("Ц-1")]
    # get_cached на SHARED db: cache-check (miss) + read-back (hit) = 2 чтения.
    assert len(cache_reads) == 2
    # Read-back вернул кэшированную форму (dict) для callers parcels.py / ird_analyze.py.
    assert result == {"zone_index": "Ц-1"}


def test_cache_miss_table_missing_rolls_back_gracefully(monkeypatch: Any) -> None:
    """upsert вернул None (таблица недоступна) → fresh session rollback, shared не тронута."""
    write_db = _WriteDB()
    monkeypatch.setattr(zr, "SessionLocal", lambda: write_db)
    # upsert_zone_regulation graceful-возврат None (OperationalError/ProgrammingError внутри).
    monkeypatch.setattr(zr, "upsert_zone_regulation", lambda db, reg, **kw: None)
    # Кэш недоступен и для read-back → None (форма как при недоступной таблице у callers).
    monkeypatch.setattr(zr, "get_cached_zone_regulation", lambda *a, **kw: None)

    shared = _SharedDB()
    client = _MissThenResolveClient("Ж-2")
    result = zr.get_or_fetch_zone_regulation(shared, 60.6, 56.8, client=client)  # type: ignore[arg-type]

    # Write-сессия откатилась (нечего персистить), не коммитила, закрыта.
    assert write_db.commits == 0
    assert write_db.rollbacks == 1
    assert write_db.closed is True
    # SHARED транзакция вообще не тронута.
    assert shared.commits == 0
    assert shared.rollbacks == 0
    # Функция отработала gracefully (read-back None при недоступном кэше).
    assert result is None
