"""Тесты для nspd_denorm.py — coerce helpers + denorm_parcel/building/dump.

Не требует реального PostgreSQL — мокает db.execute() и db.begin_nested().
"""

from __future__ import annotations

from typing import Any
from unittest.mock import MagicMock

import pytest

from app.services.scrapers.nspd_denorm import (
    _coerce_int,
    _coerce_numeric,
    denorm_building_feature,
    denorm_dump,
    denorm_parcel_feature,
)

# ── _coerce_int ────────────────────────────────────────────────────────────────


def test_coerce_int_str_number() -> None:
    assert _coerce_int("5") == 5


def test_coerce_int_int_passthrough() -> None:
    assert _coerce_int(17) == 17


def test_coerce_int_none_returns_none() -> None:
    assert _coerce_int(None) is None


def test_coerce_int_invalid_returns_none() -> None:
    assert _coerce_int("не_число") is None


def test_coerce_int_float_truncates() -> None:
    assert _coerce_int(3.9) == 3


# ── _coerce_numeric ────────────────────────────────────────────────────────────


def test_coerce_numeric_float_passthrough() -> None:
    assert _coerce_numeric(12.5) == 12.5


def test_coerce_numeric_str_dot() -> None:
    assert _coerce_numeric("1234567.89") == pytest.approx(1234567.89)


def test_coerce_numeric_comma_decimal() -> None:
    """Европейский формат с запятой → корректный float."""
    assert _coerce_numeric("1234567,89") == pytest.approx(1234567.89)


def test_coerce_numeric_none_returns_none() -> None:
    assert _coerce_numeric(None) is None


def test_coerce_numeric_invalid_returns_none() -> None:
    assert _coerce_numeric("N/A") is None


# ── Helpers для моков ──────────────────────────────────────────────────────────


def _make_mock_session() -> MagicMock:
    """Мок Session: begin_nested() — context manager, execute() → MagicMock."""
    db = MagicMock()
    # begin_nested() должен быть context manager
    cm = MagicMock()
    cm.__enter__ = MagicMock(return_value=None)
    cm.__exit__ = MagicMock(return_value=False)
    db.begin_nested.return_value = cm
    return db


def _parcel_feature(
    cad_num: str = "66:41:0204016:10",
    area: Any = "500.0",
    cost_value: Any = "1500000",
    **extra_props: Any,
) -> dict[str, Any]:
    props: dict[str, Any] = {
        "cad_num": cad_num,
        "area": area,
        "cost_value": cost_value,
        "permitted_use": "Для ИЖС",
        "land_category": "Земли населённых пунктов",
        "address": "г. Екатеринбург",
        **extra_props,
    }
    return {
        "layer": "parcels",
        "feature_id": "123",
        "geometry": {"type": "Polygon", "coordinates": [[[0, 0], [1, 0], [1, 1], [0, 0]]]},
        "properties": props,
    }


def _building_feature(
    cad_num: str = "66:41:0204016:10:1",
    purpose: str = "Многоквартирный дом",
    **extra_props: Any,
) -> dict[str, Any]:
    props: dict[str, Any] = {
        "cad_num": cad_num,
        "purpose": purpose,
        "floors_above_ground": 9,
        "floors_underground": 1,
        "year_built": 1985,
        "cost_value": "50000000",
        "build_record_area": "3200.0",
        "address": "ул. Ленина 1",
        **extra_props,
    }
    return {
        "layer": "buildings",
        "feature_id": "456",
        "geometry": {"type": "Polygon", "coordinates": [[[0, 0], [1, 0], [1, 1], [0, 0]]]},
        "properties": props,
    }


# ── denorm_parcel_feature ──────────────────────────────────────────────────────


def test_denorm_parcel_feature_inserts() -> None:
    """Полный feature с cad_num → вызывает db.execute с правильными параметрами."""
    db = _make_mock_session()

    result = denorm_parcel_feature(
        db,
        feature=_parcel_feature(),
        quarter_cad="66:41:0204016",
        snapshot_date="2026-05-16",
    )

    assert result is True
    db.begin_nested.assert_called_once()
    db.execute.assert_called_once()
    # Проверяем что параметры содержат нужные ключи
    call_kwargs = db.execute.call_args[0][1]  # positional dict
    assert call_kwargs["cad_num"] == "66:41:0204016:10"
    assert call_kwargs["quarter_cad"] == "66:41:0204016"
    assert call_kwargs["permitted_use"] == "Для ИЖС"
    assert call_kwargs["land_category"] == "Земли населённых пунктов"
    assert call_kwargs["area_sqm"] == 500.0
    assert call_kwargs["cost_value"] == 1500000.0
    assert call_kwargs["cost_per_m2"] == pytest.approx(3000.0)
    assert call_kwargs["snapshot_date"] == "2026-05-16"


def test_denorm_parcel_no_cad_num_skipped() -> None:
    """Feature без cad_num → возвращает False, db не вызывается."""
    db = _make_mock_session()
    feature: dict[str, Any] = {
        "layer": "parcels",
        "feature_id": None,
        "geometry": None,
        "properties": {"area": "100"},
    }
    result = denorm_parcel_feature(
        db, feature=feature, quarter_cad="66:41:0204016", snapshot_date="2026-05-16"
    )
    assert result is False
    db.execute.assert_not_called()


def test_denorm_parcel_zero_area_cost_per_m2_none() -> None:
    """area=0 → cost_per_m2 = None (нет деления на ноль)."""
    db = _make_mock_session()
    result = denorm_parcel_feature(
        db,
        feature=_parcel_feature(area="0", cost_value="1000"),
        quarter_cad="66:41:0204016",
        snapshot_date="2026-05-16",
    )
    assert result is True
    call_kwargs = db.execute.call_args[0][1]
    assert call_kwargs["cost_per_m2"] is None


def test_denorm_parcel_null_geometry() -> None:
    """feature без geometry → geom_json=None, INSERT всё равно вызывается."""
    db = _make_mock_session()
    feature = _parcel_feature()
    feature["geometry"] = None

    result = denorm_parcel_feature(
        db, feature=feature, quarter_cad="66:41:0204016", snapshot_date="2026-05-16"
    )
    assert result is True
    call_kwargs = db.execute.call_args[0][1]
    assert call_kwargs["geom_json"] is None


def test_denorm_parcel_db_exception_returns_false() -> None:
    """DB execute raises → возвращает False (не propagate)."""
    db = _make_mock_session()
    db.execute.side_effect = Exception("DB constraint violation")

    result = denorm_parcel_feature(
        db,
        feature=_parcel_feature(),
        quarter_cad="66:41:0204016",
        snapshot_date="2026-05-16",
    )
    assert result is False


# ── denorm_building_feature ────────────────────────────────────────────────────


def test_denorm_building_feature_inserts() -> None:
    """Полный building feature → INSERT с правильными параметрами."""
    db = _make_mock_session()

    result = denorm_building_feature(
        db,
        feature=_building_feature(),
        quarter_cad="66:41:0204016",
        snapshot_date="2026-05-16",
    )

    assert result is True
    db.execute.assert_called_once()
    call_kwargs = db.execute.call_args[0][1]
    assert call_kwargs["cad_num"] == "66:41:0204016:10:1"
    assert call_kwargs["purpose"] == "Многоквартирный дом"
    assert call_kwargs["floors"] == 9
    assert call_kwargs["floors_underground"] == 1
    assert call_kwargs["year_built"] == 1985
    assert call_kwargs["cost_value"] == 50000000.0
    assert call_kwargs["build_record_area"] == 3200.0


def test_denorm_building_no_cad_num_skipped() -> None:
    """Building feature без cad_num → False."""
    db = _make_mock_session()
    feature: dict[str, Any] = {
        "layer": "buildings",
        "feature_id": None,
        "geometry": None,
        "properties": {"purpose": "Нежилое здание"},
    }
    result = denorm_building_feature(
        db, feature=feature, quarter_cad="66:41:0204016", snapshot_date="2026-05-16"
    )
    assert result is False
    db.execute.assert_not_called()


def test_denorm_building_str_floors_coerced() -> None:
    """floors_above_ground строкой → корректно парсится в int."""
    db = _make_mock_session()
    result = denorm_building_feature(
        db,
        feature=_building_feature(floors_above_ground="12"),
        quarter_cad="66:41:0204016",
        snapshot_date="2026-05-16",
    )
    assert result is True
    call_kwargs = db.execute.call_args[0][1]
    assert call_kwargs["floors"] == 12


# ── denorm_dump ────────────────────────────────────────────────────────────────


def test_denorm_dump_aggregates() -> None:
    """3 parcels + 2 buildings + 1 unknown layer → правильные счётчики."""
    db = _make_mock_session()

    features: list[dict[str, Any]] = [
        _parcel_feature("66:41:0101001:1"),
        _parcel_feature("66:41:0101001:2"),
        _parcel_feature("66:41:0101001:3"),
        _building_feature("66:41:0101001:1:1"),
        _building_feature("66:41:0101001:1:2"),
        {
            "layer": "territorial_zones",
            "feature_id": "tz-1",
            "geometry": None,
            "properties": {"type_zone": "Ж-1"},
        },
    ]

    counts = denorm_dump(db, quarter_cad="66:41:0101001", features=features)

    assert counts["parcels"] == 3
    assert counts["buildings"] == 2
    # territorial_zones не считается как ошибка — просто пропускается
    assert counts["errors"] == 0
    db.commit.assert_called_once()


def test_denorm_dump_empty_features() -> None:
    """Пустой список features → нули, commit всё равно вызывается."""
    db = _make_mock_session()
    counts = denorm_dump(db, quarter_cad="66:41:0101001", features=[])

    assert counts == {"parcels": 0, "buildings": 0, "errors": 0, "skipped": 0}
    db.commit.assert_called_once()


def test_denorm_dump_no_cad_num_counted_as_skipped() -> None:
    """Parcel без cad_num → pre-check в denorm_dump → skipped += 1, не errors.

    denorm_parcel_feature не вызывается вовсе — пропуск штатный, не сбой UPSERT.
    """
    db = _make_mock_session()
    feature: dict[str, Any] = {
        "layer": "parcels",
        "feature_id": None,
        "geometry": None,
        "properties": {},
    }
    counts = denorm_dump(db, quarter_cad="66:41:0101001", features=[feature])
    assert counts["parcels"] == 0
    assert counts["errors"] == 0
    assert counts["skipped"] == 1
