"""SAVEPOINT-регрессия для connection_capacity_lookup.py (#2464 cluster A finding 2).

gas/heat/gas-outlet query helpers (_query_gas_city_grs, _query_gas_outlet_counts,
_query_gas_outlet_points, _query_heat_latest) когда-то ловили db.execute-сбой в
bare ``except Exception`` без SAVEPOINT/rollback — на реальном Postgres это отравляло
db shared с caller (GET .../connection-capacity, Depends(get_db)): следующий db.execute
в этом же запросе падал с "current transaction is aborted". Сиблинг
``_query_nearby_network_zones`` (в этом же файле) уже был правильным (db.begin_nested())
— эти тесты проверяют, что остальные функции приведены в соответствие.

MagicMock-сессия не эмулирует реальный aborted-transaction Postgres — тесты
проверяют (1) graceful fallback при сбое, (2) что begin_nested() реально вызван
вокруг db.execute (SAVEPOINT-обёртка присутствует в коде), (3) что db остаётся
usable для следующего вызова на том же mock-сессии (call_count растёт, ошибка не
проглатывает control flow).
"""

from __future__ import annotations

import os
from unittest.mock import MagicMock

os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")

from app.services.site_finder.connection_capacity_lookup import (
    _query_gas_city_grs,
    _query_gas_outlet_counts,
    _query_gas_outlet_points,
    _query_heat_latest,
)

_WKT = "POLYGON((60.8 56.7, 60.9 56.7, 60.9 56.8, 60.8 56.8, 60.8 56.7))"


def _mock_db_raising_once(final_rows: list[dict] | None = None) -> MagicMock:
    """db.execute кидает на 1-м вызове, на 2-м успешно отдаёт final_rows."""
    db = MagicMock()
    ok_result = MagicMock()
    ok_result.mappings.return_value.all.return_value = final_rows or []
    ok_result.mappings.return_value.first.return_value = (final_rows or [None])[0]
    db.execute.side_effect = [RuntimeError("simulated DB failure"), ok_result]
    return db


class TestGasCityGrsSavepoint:
    def test_query_failure_degrades_gracefully(self) -> None:
        db = MagicMock()
        db.execute.side_effect = RuntimeError("gas_grs_capacity недоступна")
        out = _query_gas_city_grs(db)
        assert out["city_grs"] == []
        assert out["total_free_th_m3_h"] is None

    def test_uses_begin_nested_savepoint(self) -> None:
        """Фикс: query обёрнут в db.begin_nested(), не голый db.execute."""
        db = MagicMock()
        db.execute.return_value.mappings.return_value.all.return_value = []
        db.execute.return_value.mappings.return_value.first.return_value = None
        _query_gas_city_grs(db)
        # 2 execute на функцию: _query_gas_outlet_counts (city-level счётчики) +
        # сам gas_grs_capacity query — оба теперь под begin_nested.
        assert db.begin_nested.call_count >= 1

    def test_session_usable_after_failure(self) -> None:
        """db.execute сбоит один раз (city_grs), но db остаётся usable для
        следующего (не связанного) вызова на этой же сессии."""
        db = _mock_db_raising_once()
        # outlet_counts вызывается ПЕРВЫМ внутри _query_gas_city_grs — заставим его
        # тоже отдать пустой результат нормально, а основной query — кинуть.
        db.execute.side_effect = [
            MagicMock(
                mappings=MagicMock(return_value=MagicMock(first=lambda: None))
            ),  # outlet_counts
            RuntimeError("main query failed"),  # gas_grs_capacity query
        ]
        out = _query_gas_city_grs(db)
        assert out["city_grs"] == []
        # Следующий вызов на том же db.execute отрабатывает (session не отравлена).
        next_result = MagicMock()
        next_result.mappings.return_value.all.return_value = [{"marker": "ok"}]
        db.execute.side_effect = None
        db.execute.return_value = next_result
        assert db.execute("SELECT 1").mappings().all() == [{"marker": "ok"}]


class TestGasOutletCountsSavepoint:
    def test_query_failure_degrades_to_zeros(self) -> None:
        db = MagicMock()
        db.execute.side_effect = RuntimeError("gas_grs_outlet_points недоступна")
        out = _query_gas_outlet_counts(db)
        assert out == {"outlets_total": 0, "outlets_deficit": 0, "outlets_needs_calc": 0}

    def test_uses_begin_nested_savepoint(self) -> None:
        db = MagicMock()
        db.execute.return_value.mappings.return_value.first.return_value = None
        _query_gas_outlet_counts(db)
        assert db.begin_nested.call_count == 1


class TestGasOutletPointsSavepoint:
    def test_query_failure_degrades_to_empty_list(self) -> None:
        db = MagicMock()
        db.execute.side_effect = RuntimeError("gas_grs_outlet_points (радиус) недоступна")
        assert _query_gas_outlet_points(db, _WKT) == []

    def test_uses_begin_nested_savepoint(self) -> None:
        db = MagicMock()
        db.execute.return_value.mappings.return_value.all.return_value = []
        _query_gas_outlet_points(db, _WKT)
        assert db.begin_nested.call_count == 1


class TestHeatLatestSavepoint:
    def test_query_failure_degrades_gracefully(self) -> None:
        db = MagicMock()
        db.execute.side_effect = RuntimeError("heat_system_reserves недоступна")
        out = _query_heat_latest(db)
        assert out == {"systems": [], "total_reserve_gcal_h": None}

    def test_uses_begin_nested_savepoint(self) -> None:
        db = MagicMock()
        db.execute.return_value.mappings.return_value.all.return_value = []
        _query_heat_latest(db)
        assert db.begin_nested.call_count == 1

    def test_session_usable_after_failure(self) -> None:
        """execute сбоит один раз, db остаётся usable для следующего вызова —
        имитирует heat → gas_outlet_points → nearby_network_zones в
        get_connection_capacity() на одной shared Session."""
        db = MagicMock()
        db.execute.side_effect = RuntimeError("boom")
        out = _query_heat_latest(db)
        assert out["systems"] == []

        db.execute.side_effect = None
        ok_result = MagicMock()
        ok_result.mappings.return_value.all.return_value = [{"marker": "next-query-ok"}]
        db.execute.return_value = ok_result
        assert db.execute("SELECT 1").mappings().all() == [{"marker": "next-query-ok"}]
