"""Unit-тесты моста финмодели Site Finder ↔ DCF (PR-4, #1881).

Тестируем чистые функции parcel_financial (без БД, без app.main):
  • synthesize_teap_from_buildability: GFA по max_far / по (%застройки × этажность);
    None когда нет ни far, ни (pct+floors), нет площади;
  • synthesize_parcel_financial gate-guard: can_build_mkd False/"unknown"/нет zoning/
    нет площади → None; валидный участок → dict с gdv/npv/irr + caveat;
  • housing_class по порогам цены; development_type по этажности;
  • интеграция реалистичного участка → нормальный DCF (irr_is_proxy False).

Детерминированно, без LLM / БД.
"""

from __future__ import annotations

import os

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

import math

from app.schemas.concept import TEAP
from app.services.site_finder.parcel_financial import (
    _infer_development_type,
    _infer_housing_class,
    synthesize_parcel_financial,
    synthesize_teap_from_buildability,
)


def _gate_ok() -> dict:
    return {"can_build_mkd": True, "verdict_label": "Можно"}


def _zoning_far(max_far: float = 3.0, max_floors: int = 12) -> dict:
    return {
        "zone_code": "Ж-4",
        "max_far": max_far,
        "max_building_pct": 40.0,
        "max_floors": max_floors,
    }


# ── synthesize_teap_from_buildability ─────────────────────────────────────────


def test_teap_from_max_far() -> None:
    """max_far есть → GFA = area × far, residential = GFA × eff, parking > 0."""
    area = 5000.0
    far = 3.0
    teap = synthesize_teap_from_buildability(
        area_m2=area,
        max_far=far,
        max_building_pct=40.0,
        max_floors=12,
        housing_class="comfort",
    )
    assert teap is not None
    # GFA = area × far
    assert teap.total_floor_area_sqm == round(area * far, 1)
    # Нежилое (comfort share 0.05) вырезается из GFA: office = GFA × 0.05.
    assert teap.office_area_sqm == round(area * far * 0.05, 1)
    # residential = (GFA − office) × efficiency[comfort=0.78]
    assert teap.residential_area_sqm == round(area * far * (1.0 - 0.05) * 0.78, 1)
    # density = FAR
    assert teap.density == round(far, 3)
    assert teap.apartments_count > 0
    assert teap.parking_spaces > 0
    # built = area × pct/100
    assert teap.built_area_sqm == round(area * 0.40, 1)


def test_teap_from_pct_and_floors_when_no_far() -> None:
    """Нет max_far, но есть %застройки + этажность → GFA = area × pct/100 × floors."""
    area = 4000.0
    pct = 30.0
    floors = 9
    teap = synthesize_teap_from_buildability(
        area_m2=area,
        max_far=None,
        max_building_pct=pct,
        max_floors=floors,
        housing_class="econom",
    )
    assert teap is not None
    expected_gfa = area * (pct / 100.0) * floors
    assert teap.total_floor_area_sqm == round(expected_gfa, 1)
    # residential = GFA × efficiency[econom=0.82]
    assert teap.residential_area_sqm == round(expected_gfa * 0.82, 1)


def test_teap_none_when_no_far_no_pct_floors() -> None:
    """Ни max_far, ни (%застройки + этажность) → None (GFA не вывести)."""
    assert (
        synthesize_teap_from_buildability(
            area_m2=5000.0,
            max_far=None,
            max_building_pct=None,
            max_floors=None,
            housing_class="comfort",
        )
        is None
    )
    # %застройки есть, но этажности нет → тоже None.
    assert (
        synthesize_teap_from_buildability(
            area_m2=5000.0,
            max_far=None,
            max_building_pct=40.0,
            max_floors=None,
            housing_class="comfort",
        )
        is None
    )


def test_teap_none_when_no_area() -> None:
    """Нет площади → None."""
    assert (
        synthesize_teap_from_buildability(
            area_m2=None,
            max_far=3.0,
            max_building_pct=40.0,
            max_floors=12,
            housing_class="comfort",
        )
        is None
    )
    assert (
        synthesize_teap_from_buildability(
            area_m2=0.0,
            max_far=3.0,
            max_building_pct=40.0,
            max_floors=12,
            housing_class="comfort",
        )
        is None
    )


def test_teap_parking_uses_class_norm() -> None:
    """parking = ceil(apartments × norm[class]); business норма 1.5 > comfort 1.0."""
    teap = synthesize_teap_from_buildability(
        area_m2=5000.0,
        max_far=3.0,
        max_building_pct=40.0,
        max_floors=12,
        housing_class="business",
    )
    assert teap is not None
    expected_parking = math.ceil(teap.apartments_count * 1.5)
    assert teap.parking_spaces == expected_parking


# ── housing_class по порогам цены ─────────────────────────────────────────────


def test_infer_housing_class_thresholds() -> None:
    assert _infer_housing_class(None) == "comfort"  # None → comfort
    assert _infer_housing_class(100_000.0) == "econom"  # < 125k
    assert _infer_housing_class(124_999.0) == "econom"
    assert _infer_housing_class(125_000.0) == "comfort"  # >= 125k, < 177k
    assert _infer_housing_class(150_000.0) == "comfort"
    assert _infer_housing_class(176_999.0) == "comfort"
    assert _infer_housing_class(177_000.0) == "business"  # >= 177k
    assert _infer_housing_class(250_000.0) == "business"


# ── development_type по этажности ─────────────────────────────────────────────


def test_infer_development_type_thresholds() -> None:
    assert _infer_development_type(None) == "mid_rise"  # дефолт
    assert _infer_development_type(3) == "spot"  # <= 4
    assert _infer_development_type(4) == "spot"
    assert _infer_development_type(5) == "mid_rise"  # 5-8
    assert _infer_development_type(8) == "mid_rise"
    assert _infer_development_type(9) == "high_rise"  # >= 9
    assert _infer_development_type(24) == "high_rise"


# ── synthesize_parcel_financial gate-guard ────────────────────────────────────


def test_financial_none_when_gate_forbids() -> None:
    """can_build_mkd False → None."""
    assert (
        synthesize_parcel_financial(
            area_m2=5000.0,
            nspd_zoning=_zoning_far(),
            market_price={"median": 150_000.0},
            district_price_median=140_000.0,
            cadastral_value_rub=50_000_000.0,
            gate_verdict={"can_build_mkd": False, "verdict_label": "Нельзя"},
        )
        is None
    )


def test_financial_none_when_gate_unknown() -> None:
    """can_build_mkd "unknown" → None."""
    assert (
        synthesize_parcel_financial(
            area_m2=5000.0,
            nspd_zoning=_zoning_far(),
            market_price={"median": 150_000.0},
            district_price_median=140_000.0,
            cadastral_value_rub=50_000_000.0,
            gate_verdict={"can_build_mkd": "unknown", "verdict_label": "Нужна проверка"},
        )
        is None
    )


def test_financial_none_when_no_zoning() -> None:
    """Нет zoning-чисел → None."""
    assert (
        synthesize_parcel_financial(
            area_m2=5000.0,
            nspd_zoning=None,
            market_price={"median": 150_000.0},
            district_price_median=140_000.0,
            cadastral_value_rub=50_000_000.0,
            gate_verdict=_gate_ok(),
        )
        is None
    )
    # zoning есть, но без числовых параметров → None.
    assert (
        synthesize_parcel_financial(
            area_m2=5000.0,
            nspd_zoning={"zone_code": "Ж-4"},
            market_price={"median": 150_000.0},
            district_price_median=140_000.0,
            cadastral_value_rub=50_000_000.0,
            gate_verdict=_gate_ok(),
        )
        is None
    )


def test_financial_none_when_no_area() -> None:
    """Нет площади → None."""
    assert (
        synthesize_parcel_financial(
            area_m2=None,
            nspd_zoning=_zoning_far(),
            market_price={"median": 150_000.0},
            district_price_median=140_000.0,
            cadastral_value_rub=50_000_000.0,
            gate_verdict=_gate_ok(),
        )
        is None
    )


def test_financial_valid_parcel_returns_dict_with_caveat() -> None:
    """Валидный участок → dict с gdv/npv/irr + caveat присутствует."""
    fin = synthesize_parcel_financial(
        area_m2=5000.0,
        nspd_zoning=_zoning_far(max_far=3.0, max_floors=12),
        market_price={"median": 150_000.0},
        district_price_median=140_000.0,
        cadastral_value_rub=50_000_000.0,
        gate_verdict=_gate_ok(),
    )
    assert fin is not None
    # GDV (revenue) и DCF-метрики присутствуют.
    assert fin["revenue_rub"] > 0
    assert fin["cost_rub"] > 0
    assert "net_profit_rub" in fin
    assert "npv_rub" in fin
    assert "irr" in fin
    assert "roi" in fin
    assert "margin_pct" in fin
    # Bridge-метаданные.
    assert fin["housing_class_inferred"] == "comfort"  # price 150k
    assert fin["development_type_inferred"] == "high_rise"  # 12 этажей
    assert fin["schedule_is_default"] is True
    # teap_synth контракт для фронта.
    teap_synth = fin["teap_synth"]
    assert teap_synth["total_floor_area_sqm"] == round(5000.0 * 3.0, 1)
    assert teap_synth["residential_area_sqm"] > 0
    assert teap_synth["parking_spaces"] > 0
    assert teap_synth["apartments_count"] > 0
    # HEAVY caveat обязателен.
    assert "caveat" in fin
    assert "ОРИЕНТИРОВОЧНАЯ" in fin["caveat"]
    assert "градрегламент" in fin["caveat"]


def test_financial_price_source_district_fallback() -> None:
    """Нет market median → fallback к district median, source=district_reference."""
    fin = synthesize_parcel_financial(
        area_m2=5000.0,
        nspd_zoning=_zoning_far(),
        market_price={"deals_count": 0, "source": "no_data"},
        district_price_median=130_000.0,
        cadastral_value_rub=50_000_000.0,
        gate_verdict=_gate_ok(),
    )
    assert fin is not None
    assert fin["price_source"] == "district_reference"
    assert fin["price_is_calibrated"] is True
    assert fin["price_per_sqm_used"] == 130_000.0
    assert fin["housing_class_inferred"] == "comfort"  # 130k → comfort


def test_financial_price_source_class_norm_when_no_market() -> None:
    """Нет ни market, ни district цены → class_norm, class=comfort (None→comfort)."""
    fin = synthesize_parcel_financial(
        area_m2=5000.0,
        nspd_zoning=_zoning_far(),
        market_price=None,
        district_price_median=None,
        cadastral_value_rub=50_000_000.0,
        gate_verdict=_gate_ok(),
    )
    assert fin is not None
    assert fin["price_source"] == "class_norm"
    assert fin["price_is_calibrated"] is False
    assert fin["housing_class_inferred"] == "comfort"


# ── geo_radius_price: приоритет МЕЖДУ quarter-MV и district_reference ──────────


def test_financial_quarter_mv_wins_over_geo_radius() -> None:
    """Есть quarter-MV median → geo-радиус игнорируется (объективный приоритет MV)."""
    fin = synthesize_parcel_financial(
        area_m2=5000.0,
        nspd_zoning=_zoning_far(),
        market_price={"median": 150_000.0},
        geo_radius_price_median=200_000.0,
        district_price_median=140_000.0,
        cadastral_value_rub=50_000_000.0,
        gate_verdict=_gate_ok(),
    )
    assert fin is not None
    assert fin["price_source"] == "objective_district_median"
    assert fin["price_per_sqm_used"] == 150_000.0


def test_financial_price_source_geo_radius_when_no_quarter_mv() -> None:
    """Нет quarter-MV, но есть geo-радиус → source=objective_geo_radius, цена из него."""
    fin = synthesize_parcel_financial(
        area_m2=5000.0,
        nspd_zoning=_zoning_far(),
        market_price={"deals_count": 0, "source": "no_data"},
        geo_radius_price_median=185_000.0,
        district_price_median=140_000.0,
        cadastral_value_rub=50_000_000.0,
        gate_verdict=_gate_ok(),
    )
    assert fin is not None
    assert fin["price_source"] == "objective_geo_radius"
    assert fin["price_is_calibrated"] is True
    assert fin["price_per_sqm_used"] == 185_000.0
    assert fin["housing_class_inferred"] == "business"  # 185k → business (>=177k)


def test_financial_district_fallback_when_no_quarter_mv_no_geo_radius() -> None:
    """Нет quarter-MV и geo-радиуса, но есть district → district_reference (без регресса)."""
    fin = synthesize_parcel_financial(
        area_m2=5000.0,
        nspd_zoning=_zoning_far(),
        market_price={"deals_count": 0, "source": "no_data"},
        geo_radius_price_median=None,
        district_price_median=130_000.0,
        cadastral_value_rub=50_000_000.0,
        gate_verdict=_gate_ok(),
    )
    assert fin is not None
    assert fin["price_source"] == "district_reference"
    assert fin["price_per_sqm_used"] == 130_000.0


def test_financial_class_norm_when_all_price_sources_absent() -> None:
    """Все источники цены отсутствуют (incl. geo-радиус) → class_norm."""
    fin = synthesize_parcel_financial(
        area_m2=5000.0,
        nspd_zoning=_zoning_far(),
        market_price=None,
        geo_radius_price_median=None,
        district_price_median=None,
        cadastral_value_rub=50_000_000.0,
        gate_verdict=_gate_ok(),
    )
    assert fin is not None
    assert fin["price_source"] == "class_norm"
    assert fin["price_is_calibrated"] is False


# ── интеграция: реалистичный участок ──────────────────────────────────────────


def test_financial_realistic_parcel_real_dcf() -> None:
    """area 5000, max_far 3, max_floors 12, price 150000 → нормальный DCF (irr реал)."""
    fin = synthesize_parcel_financial(
        area_m2=5000.0,
        nspd_zoning=_zoning_far(max_far=3.0, max_floors=12),
        market_price={"median": 150_000.0},
        district_price_median=140_000.0,
        cadastral_value_rub=80_000_000.0,
        gate_verdict=_gate_ok(),
    )
    assert fin is not None
    # Прибыльный девелоперский поток → реальный IRR (не proxy).
    assert fin["irr_is_proxy"] is False
    assert fin["net_profit_rub"] > 0
    assert fin["npv_rub"] != 0.0
    assert fin["roi"] > 0
    # land_cost = кадастровая стоимость (не рыночная).
    assert fin["land_rub"] == 80_000_000.0
    # high_rise (12 эт.) → development_type прокинут в DCF.
    assert fin["development_type_inferred"] == "high_rise"
    # price калибрована по market median.
    assert fin["price_per_sqm_used"] == 150_000.0
    assert fin["price_source"] == "objective_district_median"


def test_synthesized_teap_matches_compute_financial_contract() -> None:
    """teap_synth поля совпадают с синтезированным TEAP (sanity на контракт)."""
    teap = synthesize_teap_from_buildability(
        area_m2=5000.0,
        max_far=3.0,
        max_building_pct=40.0,
        max_floors=12,
        housing_class="comfort",
    )
    assert isinstance(teap, TEAP)
    fin = synthesize_parcel_financial(
        area_m2=5000.0,
        nspd_zoning=_zoning_far(max_far=3.0, max_floors=12),
        market_price={"median": 150_000.0},
        district_price_median=140_000.0,
        cadastral_value_rub=50_000_000.0,
        gate_verdict=_gate_ok(),
    )
    assert fin is not None
    assert fin["teap_synth"]["total_floor_area_sqm"] == teap.total_floor_area_sqm
    assert fin["teap_synth"]["residential_area_sqm"] == teap.residential_area_sqm
    assert fin["teap_synth"]["apartments_count"] == teap.apartments_count
    assert fin["teap_synth"]["parking_spaces"] == teap.parking_spaces
