"""Тесты для market_price в POST /api/v1/parcels/{cad_num}/analyze (#33).

Покрывает:
1. analyze с known quarter в mv_quarter_price_per_m2 → возвращает median/p25/p75/source='quarter_mv'
2. analyze с quarter которого нет в MV → deals_count=0, source='no_data'
3. analyze с invalid cad → 404 (no regression)

Стратегия mock: аналогична test_analyze_inline_weights.py — DB mock через
dependency_overrides, тяжёлые сервисы патчим через unittest.mock.patch.

Порядок db.execute calls в analyze_parcel (v3.7 + #33 + #29 G2):
  0. UNION ALL geom + source → .mappings().first()
  1. WKT query → .mappings().first()
  2. District → .mappings().first()
  3. POI rows → .mappings().all()
  4. Competitor rows → .mappings().all()
  5. Pipeline rows → .mappings().all()
  6. Centroid lat/lon → .mappings().first()
  7. Noise rows → .mappings().all()
  8. Hydrology → .mappings().all()
  9. Utilities → .mappings().all()
  10. parcel_meta (cad_parcels) → .mappings().first()   ← #29 G2
  11. Market trend → .mappings().first()
  12. Zoning (begin_nested) → .mappings().first()
  13. Success recommendation (begin_nested) → .mappings().all()
  14. Market price (begin_nested) → .mappings().first()   ← #33
  15. Recent permits (begin_nested) → .mappings().all()   ← #105 Phase 5
  16. _geotech_risk (industrial count) → .scalar()
  17. _neighbors_summary (neighbor_rows) → .mappings().all()
  18. _neighbors_summary (overlap_row) → .mappings().first()
"""

from __future__ import annotations

import datetime as dt
from decimal import Decimal
from typing import Any
from unittest.mock import MagicMock, patch

import pytest
from fastapi.testclient import TestClient

from app.main import app

# ── Константы ─────────────────────────────────────────────────────────────────

_CAD = "66:41:0204016:10"  # cad_num с 4 частями — quarter = "66:41:0204016"
_CAD_3PARTS = "66:41:0204016"  # cad_num уже является quarter
_WKT = "POLYGON((60.6 56.838, 60.61 56.838, 60.61 56.845, 60.6 56.845, 60.6 56.838))"
_GEOJSON = '{"type":"Polygon","coordinates":[[[60.6,56.838],[60.61,56.838]]]}'

# ── Mock factories ─────────────────────────────────────────────────────────────


def _make_mapping(data: dict[str, Any]) -> MagicMock:
    """Создать mock-строку (mapping) с __getitem__ + .get() для dict-like доступа."""
    m = MagicMock()
    m.__getitem__ = lambda self, k: data[k]
    m.get = lambda k, default=None: data.get(k, default)
    return m


def _make_db_for_analyze(
    geom_found: bool = True,
    district_found: bool = True,
    market_price_row: dict[str, Any] | None = None,
) -> MagicMock:
    """Сконструировать mock DB Session для analyze_parcel.

    market_price_row=None → имитирует "нет данных в MV" (mp_row is None → source='no_data').
    market_price_row={...} → имитирует найденную строку в MV.
    """
    db = MagicMock()

    geom_row = (
        _make_mapping({"geom_geojson": _GEOJSON, "geom_wkb": None, "source": "cad_quarter"})
        if geom_found
        else None
    )
    wkt_row = _make_mapping({"wkt": _WKT}) if geom_found else None
    district_row = (
        _make_mapping(
            {
                "district_name": "Октябрьский",
                "median_price_per_m2": 120000,
                "dist_to_center": 1500.0,
            }
        )
        if district_found
        else None
    )
    centroid_row = _make_mapping({"lat": 56.84, "lon": 60.605})

    mp_mock = _make_mapping(market_price_row) if market_price_row is not None else None

    call_idx = [0]
    responses: list[Any] = [
        ("first", geom_row),  # 0: geom UNION ALL
        ("first", wkt_row),  # 1: WKT
        ("first", district_row),  # 2: district
        ("all", []),  # 3: POI rows
        ("all", []),  # 4: competitor rows
        ("all", []),  # 5: pipeline rows
        ("first", centroid_row),  # 6: centroid
        ("all", []),  # 7: noise rows
        ("all", []),  # 8: hydrology rows
        ("all", []),  # 9: utilities rows
        ("first", None),  # 10: parcel_meta (cad_parcels) ← #29 G2
        ("first", None),  # 11: market trend
        ("first", None),  # 12: zoning (begin_nested)
        ("all", []),  # 13: success recommendation (begin_nested)
        ("first", mp_mock),  # 14: market price (begin_nested) ← #33
        ("all", []),  # 15: recent permits (begin_nested)
        ("scalar", 0),  # 16: geotech_risk
        ("all", []),  # 17: neighbors
        ("first", None),  # 18: overlap
    ]

    def _execute_side_effect(*args: Any, **kwargs: Any) -> MagicMock:
        idx = call_idx[0]
        call_idx[0] += 1
        if idx >= len(responses):
            r = MagicMock()
            r.mappings.return_value.first.return_value = None
            r.mappings.return_value.all.return_value = []
            r.scalar.return_value = 0
            return r
        kind, data = responses[idx]
        r = MagicMock()
        r.mappings.return_value.first.return_value = data
        r.mappings.return_value.all.return_value = data if isinstance(data, list) else []
        r.scalar.return_value = data if kind == "scalar" else 0
        return r

    db.execute.side_effect = _execute_side_effect

    ctx = MagicMock()
    ctx.__enter__ = MagicMock(return_value=ctx)
    ctx.__exit__ = MagicMock(return_value=False)
    db.begin_nested.return_value = ctx

    return db


def _override_db(db: MagicMock):
    def _get_db_override():
        yield db

    return _get_db_override


_PATCHES = [
    patch("app.api.v1.parcels._fetch_air_quality_sync", return_value=None),
    patch("app.api.v1.parcels._fetch_weather_sync", return_value=None),
    patch("app.api.v1.parcels._fetch_seasonal_weather_sync", return_value=None),
    patch(
        "app.api.v1.parcels.get_quarter_dump_data",
        return_value={
            "nspd_zoning": None,
            "nspd_zouit_overlaps": [],
            "nspd_engineering_nearby": [],
            "nspd_dump": {"available": False, "stale": False, "harvest_triggered": False},
        },
    ),
    patch("app.api.v1.parcels.compute_velocity", return_value=None),
    patch("app.api.v1.parcels.compute_gate_verdict", return_value={"verdict": "unknown"}),
]


def _start_patches() -> None:
    for p in _PATCHES:
        p.start()


def _stop_patches() -> None:
    for p in _PATCHES:
        p.stop()


# ── Тесты ─────────────────────────────────────────────────────────────────────


def test_market_price_found_in_mv() -> None:
    """analyze с known quarter → market_price содержит median/p25/p75, source='quarter_mv'."""
    from app.core.db import get_db

    mv_data: dict[str, Any] = {
        "p25": Decimal("85000.00"),
        "median": Decimal("102000.00"),
        "p75": Decimal("118000.00"),
        "mean": Decimal("103500.00"),
        "deals_count": 47,
        "median_6m": Decimal("105000.00"),
        "median_12m": Decimal("100000.00"),
        "median_24m": Decimal("102000.00"),
        "last_deal_date": dt.date(2026, 3, 15),
    }
    db = _make_db_for_analyze(market_price_row=mv_data)
    app.dependency_overrides[get_db] = _override_db(db)
    _start_patches()
    try:
        client = TestClient(app)
        resp = client.post(f"/api/v1/parcels/{_CAD}/analyze")
        assert resp.status_code == 200, resp.text
        body = resp.json()
        assert "market_price" in body, "market_price отсутствует в ответе"
        mp = body["market_price"]
        assert mp["source"] == "quarter_mv"
        assert mp["deals_count"] == 47
        assert mp["median"] == pytest.approx(102000.0)
        assert mp["p25"] == pytest.approx(85000.0)
        assert mp["p75"] == pytest.approx(118000.0)
        assert mp["mean"] == pytest.approx(103500.0)
        assert mp["median_6m"] == pytest.approx(105000.0)
        assert mp["median_12m"] == pytest.approx(100000.0)
        assert mp["median_24m"] == pytest.approx(102000.0)
        assert mp["last_deal_date"] == "2026-03-15"
    finally:
        app.dependency_overrides.clear()
        _stop_patches()


def test_market_price_quarter_not_in_mv() -> None:
    """analyze с quarter которого нет в MV → deals_count=0, source='no_data'."""
    from app.core.db import get_db

    # market_price_row=None → mp_mock=None → ветка else {"deals_count": 0, "source": "no_data"}
    db = _make_db_for_analyze(market_price_row=None)
    app.dependency_overrides[get_db] = _override_db(db)
    _start_patches()
    try:
        client = TestClient(app)
        resp = client.post(f"/api/v1/parcels/{_CAD}/analyze")
        assert resp.status_code == 200, resp.text
        body = resp.json()
        assert "market_price" in body
        mp = body["market_price"]
        assert mp["source"] == "no_data"
        assert mp["deals_count"] == 0
        # price fields absent или None
        assert mp.get("median") is None
        assert mp.get("p25") is None
    finally:
        app.dependency_overrides.clear()
        _stop_patches()


def test_market_price_invalid_cad_returns_404() -> None:
    """analyze с несуществующим cad → 404 (no regression)."""
    from app.core.db import get_db

    # geom_found=False → geom_row=None → endpoint вернёт 202 (inline fetch) или 404
    # В тестовой среде on-demand fetch задизейблен (нет Celery/Redis) → 404 expected
    db = _make_db_for_analyze(geom_found=False)
    app.dependency_overrides[get_db] = _override_db(db)
    _start_patches()
    try:
        client = TestClient(app)
        resp = client.post("/api/v1/parcels/00:00:0000000:0/analyze")
        # При отсутствии геометрии возвращается 202 (on-demand fetch enqueue) или 404
        assert resp.status_code in (
            202,
            404,
        ), f"Ожидали 202 или 404 для неизвестного cad, получили {resp.status_code}: {resp.text}"
    finally:
        app.dependency_overrides.clear()
        _stop_patches()
