"""Тесты для _save_territorial_zones (bulk_harvest.py) — mock-based.

Проверяет:
- Успешный UPSERT 3 features → 3 строки вставлены
- Повторный вызов → ON CONFLICT обновляет, не дублирует
- Feature без geometry → строка вставлена с geom=NULL, без краша
- Feature без zone_id → синтетический fallback zone_id используется
"""

from __future__ import annotations

from typing import Any
from unittest.mock import MagicMock

from app.services.cadastre.bulk_harvest import _save_territorial_zones


def _make_feature(
    feature_id: Any = "zone_1",
    zone_code: str = "Ж-1",
    zone_name: str = "Жилая смешанная",
    permitted_use: str = "ИЖС",
    has_geometry: bool = True,
) -> dict:
    """Создать raw feature dict в формате get_features_in_bbox_grid."""
    geom = (
        {
            "type": "Polygon",
            "coordinates": [
                [
                    [6090000.0, 7590000.0],
                    [6090100.0, 7590000.0],
                    [6090100.0, 7590100.0],
                    [6090000.0, 7590100.0],
                    [6090000.0, 7590000.0],
                ]
            ],
        }
        if has_geometry
        else None
    )
    return {
        "id": feature_id,
        "geometry": geom,
        "properties": {
            "zone_code": zone_code,
            "zone_name": zone_name,
            "permitted_use": permitted_use,
        },
    }


def _make_db_mock() -> MagicMock:
    """Mock SQLAlchemy Session с begin_nested() savepoint support."""
    db = MagicMock()
    # begin_nested() используется как context manager
    savepoint_ctx = MagicMock()
    savepoint_ctx.__enter__ = MagicMock(return_value=savepoint_ctx)
    savepoint_ctx.__exit__ = MagicMock(return_value=False)
    db.begin_nested.return_value = savepoint_ctx
    return db


class TestSaveTerritorialZones:
    """Тесты для _save_territorial_zones."""

    def test_three_features_inserted(self) -> None:
        """3 features → returned count == 3, execute вызван 3 раза."""
        db = _make_db_mock()
        features = [
            _make_feature("z1", "Ж-1"),
            _make_feature("z2", "ОД-1"),
            _make_feature("z3", "П-1"),
        ]

        result = _save_territorial_zones(db, "66:41:0204016", features)

        assert result == 3
        assert db.execute.call_count == 3
        db.commit.assert_called_once()

    def test_empty_features_list(self) -> None:
        """Пустой список → 0 inserted, commit всё равно вызван."""
        db = _make_db_mock()

        result = _save_territorial_zones(db, "66:41:0204016", [])

        assert result == 0
        db.execute.assert_not_called()
        db.commit.assert_called_once()

    def test_feature_without_geometry_no_crash(self) -> None:
        """Feature без geometry → geom=NULL, строка вставлена без краша."""
        db = _make_db_mock()
        features = [_make_feature("zone_no_geom", has_geometry=False)]

        result = _save_territorial_zones(db, "66:41:0204016", features)

        assert result == 1
        # Проверяем что geom параметр передан как None
        call_kwargs: dict = db.execute.call_args[0][1]
        assert call_kwargs["geom"] is None

    def test_feature_without_zone_id_uses_fallback(self) -> None:
        """Feature без id → md5-based fallback zone_id (stable между runs)."""
        db = _make_db_mock()
        features = [
            {
                "id": None,
                "geometry": None,
                "properties": {"zone_code": "Ж-2"},
            }
        ]

        result = _save_territorial_zones(db, "66:41:0204016", features)

        assert result == 1
        call_kwargs = db.execute.call_args[0][1]
        zone_id: str = call_kwargs["zone_id"]
        # fallback zone_id содержит quarter_cad и стабильный hash (12 hex chars)
        assert zone_id.startswith("66:41:0204016_")
        suffix = zone_id.split("_", 3)[-1]
        assert len(suffix) == 12
        assert all(c in "0123456789abcdef" for c in suffix)

        # Второй вызов с теми же данными → тот же zone_id (идемпотентность)
        db2 = _make_db_mock()
        _save_territorial_zones(db2, "66:41:0204016", features)
        call_kwargs2 = db2.execute.call_args[0][1]
        assert call_kwargs2["zone_id"] == zone_id

    def test_zone_id_from_props_id(self) -> None:
        """Если feature.id=None, но props['id'] есть — используется props['id']."""
        db = _make_db_mock()
        features = [
            {
                "id": None,
                "geometry": None,
                "properties": {"id": "props_id_42", "zone_code": "Ж-3"},
            }
        ]

        result = _save_territorial_zones(db, "66:41:0204016", features)

        assert result == 1
        call_kwargs = db.execute.call_args[0][1]
        assert call_kwargs["zone_id"] == "props_id_42"

    def test_execute_error_logged_not_raised(self) -> None:
        """Exception в execute → строка не вставлена, warning залогирован, не re-raise."""
        db = _make_db_mock()
        db.execute.side_effect = RuntimeError("DB error")
        features = [_make_feature("z_err")]

        # Не должен бросить исключение
        result = _save_territorial_zones(db, "66:41:0204016", features)

        assert result == 0
        db.commit.assert_called_once()

    def test_savepoint_used_per_row(self) -> None:
        """begin_nested() вызывается для каждой строки (SAVEPOINT паттерн)."""
        db = _make_db_mock()
        features = [_make_feature(f"z{i}") for i in range(3)]

        _save_territorial_zones(db, "66:41:0204016", features)

        assert db.begin_nested.call_count == 3

    def test_quarter_cad_param_passed(self) -> None:
        """quarter_cad правильно передаётся в SQL параметры."""
        db = _make_db_mock()
        features = [_make_feature("zone_check")]

        _save_territorial_zones(db, "66:41:9999999", features)

        call_kwargs = db.execute.call_args[0][1]
        assert call_kwargs["quarter_cad"] == "66:41:9999999"

    def test_raw_props_serialized(self) -> None:
        """raw_props — JSON строка из properties dict."""
        import json

        db = _make_db_mock()
        features = [
            {
                "id": "z_props",
                "geometry": None,
                "properties": {"zone_code": "ОД-2", "extra": "value"},
            }
        ]

        _save_territorial_zones(db, "66:41:0204016", features)

        call_kwargs = db.execute.call_args[0][1]
        raw = json.loads(call_kwargs["raw_props"])
        assert raw["zone_code"] == "ОД-2"
        assert raw["extra"] == "value"
