"""Unit-тесты оптимизатора программы застройки (#2181).

Тестируем чистую функцию program_optimizer.optimize_program (без БД, без app.main):
  • ранжир вариантов по NPV убыв. (тай-брейк ROI);
  • any_viable=False когда все комбо в минусе — и всё равно топ-N «наименее убыточных»;
  • кламп floors_grid по max_floors_allowed (10 → [5, 9]);
  • ЕДИНАЯ откалиброванная цена во все комбо (price_per_sqm_used одинакова);
  • сбой одного комбо не валит грид;
  • полный вызов быстрый (грубый порог 1 с — защита от деградации на порядки).

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

from __future__ import annotations

import os

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

import time
from unittest.mock import patch

from app.services.site_finder.program_optimizer import (
    _build_floors_grid,
    optimize_program,
)


def _big_profitable_kwargs() -> dict:
    """Крупный участок + высокая цена → как минимум часть комбо прибыльна."""
    return {
        "area_m2": 12_000.0,
        "max_far": 3.0,
        "max_building_pct": 40.0,
        "max_floors_allowed": 25,
        "land_cost_rub": 50_000_000.0,
        "market_price_per_sqm": 180_000.0,
        "price_source": "objective_geo_radius",
        "velocity_sqm_per_month": 900.0,
    }


# ── floors-grid кламп ─────────────────────────────────────────────────────────


def test_floors_grid_clamped_to_max_allowed() -> None:
    """max_floors_allowed=10 → [5, 9] (14 и 25 отсечены)."""
    assert _build_floors_grid(10) == [5, 9]


def test_floors_grid_none_returns_full_set() -> None:
    """Предел неизвестен → весь опорный набор."""
    assert _build_floors_grid(None) == [5, 9, 14, 25]


def test_floors_grid_below_min_falls_back_to_limit() -> None:
    """Предел ниже минимального опорного (3) → сам предел единственной этажностью."""
    assert _build_floors_grid(3) == [3]


def test_optimize_respects_floors_clamp() -> None:
    """max_floors_allowed=10 → в вариантах только этажи из [5, 9]."""
    kwargs = _big_profitable_kwargs()
    kwargs["max_floors_allowed"] = 10
    result = optimize_program(**kwargs)
    assert result is not None
    floors_seen = {v["floors"] for v in result["variants"]}
    assert floors_seen <= {5, 9}
    # 3 класса × 2 этажа = 6 комбо всего в гриде.
    assert result["grid_size"] == 6


# ── ранжир по NPV ─────────────────────────────────────────────────────────────


def test_variants_sorted_by_npv_desc() -> None:
    """Топ-N отсортирован по npv_rub убыв."""
    result = optimize_program(**_big_profitable_kwargs())
    assert result is not None
    npvs = [v["npv_rub"] for v in result["variants"]]
    assert npvs == sorted(npvs, reverse=True)
    assert len(result["variants"]) == 3  # top_n=3 по умолчанию


def test_profitable_site_any_viable_true() -> None:
    """Крупный участок + высокая цена → хотя бы один вариант в плюсе."""
    result = optimize_program(**_big_profitable_kwargs())
    assert result is not None
    assert result["any_viable"] is True
    assert any(v["npv_rub"] > 0 for v in result["variants"])


# ── all-negative: any_viable=False, но топ-N всё равно есть ────────────────────


def test_all_negative_any_viable_false_still_returns_top_n() -> None:
    """Крошечный участок + мизерная цена → все в минусе, any_viable=False, топ-3 есть."""
    result = optimize_program(
        area_m2=600.0,
        max_far=1.5,
        max_building_pct=30.0,
        max_floors_allowed=9,
        land_cost_rub=200_000_000.0,  # неадекватно дорогая земля топит все комбо
        market_price_per_sqm=40_000.0,  # мизерная цена
        price_source="objective_geo_radius",
        velocity_sqm_per_month=100.0,
    )
    assert result is not None
    assert result["any_viable"] is False
    assert all(v["npv_rub"] <= 0 for v in result["variants"])
    # «Наименее убыточные» — всё равно топ-3 и отсортированы по NPV убыв.
    assert len(result["variants"]) == 3
    npvs = [v["npv_rub"] for v in result["variants"]]
    assert npvs == sorted(npvs, reverse=True)


# ── ЕДИНАЯ цена во все комбо ───────────────────────────────────────────────────


def test_fixed_price_across_all_combos() -> None:
    """market_price_per_sqm един во всех комбо: price_per_sqm_used одинакова везде."""
    kwargs = _big_profitable_kwargs()
    kwargs["market_price_per_sqm"] = 165_000.0
    result = optimize_program(**kwargs)
    assert result is not None
    prices = {v["price_per_sqm_used"] for v in result["variants"]}
    assert prices == {165_000.0}
    sources = {v["price_source"] for v in result["variants"]}
    assert sources == {"objective_geo_radius"}


def test_no_market_price_uses_class_norm() -> None:
    """Рынка нет → каждый комбо на нормативе своего класса (цены РАЗНЫЕ по классам)."""
    kwargs = _big_profitable_kwargs()
    kwargs["market_price_per_sqm"] = None
    kwargs["price_source"] = "class_norm"
    result = optimize_program(**kwargs)
    assert result is not None
    # Норматив зависит от класса → в гриде минимум 2 разных цены (эконом 110k vs бизнес 210k).
    all_prices = {v["price_per_sqm_used"] for v in result["variants"]}
    assert all(v["price_source"] == "class_norm" for v in result["variants"])
    assert len(all_prices) >= 1  # top-3 может оказаться одним классом; проверяем норму ниже
    for v in result["variants"]:
        assert v["price_per_sqm_used"] in (110_000.0, 145_000.0, 210_000.0)


# ── устойчивость: сбой одного комбо не валит грид ─────────────────────────────


def test_one_combo_failure_does_not_break_grid() -> None:
    """compute_financial кидает на 'business' → остальные комбо считаются, грид не падает."""
    real_compute = None
    from app.services.site_finder import program_optimizer as mod

    real_compute = mod.compute_financial

    def _flaky(*args, **kwargs):  # type: ignore[no-untyped-def]
        if kwargs.get("housing_class") == "business":
            raise ValueError("boom")
        return real_compute(*args, **kwargs)

    with patch.object(mod, "compute_financial", side_effect=_flaky):
        result = optimize_program(**_big_profitable_kwargs())

    assert result is not None
    # business (× 4 этажа) выпал → в гриде остались только econom + comfort.
    assert all(v["housing_class"] != "business" for v in result["variants"])
    assert result["grid_size"] > 0  # грид выжил


def test_no_area_returns_none() -> None:
    """Нет площади → None (нечего считать)."""
    kwargs = _big_profitable_kwargs()
    kwargs["area_m2"] = None
    assert optimize_program(**kwargs) is None


def test_no_zoning_numbers_returns_none() -> None:
    """Ни far, ни (pct+floors) → ни один комбо не даёт ТЭП → None."""
    result = optimize_program(
        area_m2=8000.0,
        max_far=None,
        max_building_pct=None,
        max_floors_allowed=None,
        land_cost_rub=0.0,
        market_price_per_sqm=150_000.0,
        price_source="objective_geo_radius",
        velocity_sqm_per_month=None,
    )
    assert result is None


# ── caveat / метаданные ───────────────────────────────────────────────────────


def test_result_carries_caveat_and_metadata() -> None:
    """Результат несёт honest-caveat + grid_size + структуру каждого варианта."""
    result = optimize_program(**_big_profitable_kwargs())
    assert result is not None
    assert isinstance(result["caveat"], str) and result["caveat"]
    assert result["grid_size"] == 12  # 3 класса × 4 этажа
    v = result["variants"][0]
    for key in (
        "housing_class",
        "floors",
        "development_type",
        "npv_rub",
        "irr",
        "roi",
        "net_profit_rub",
        "residential_sqm",
        "apartments_count",
        "price_per_sqm_used",
        "price_source",
        "schedule_is_default",
    ):
        assert key in v


# ── производительность ────────────────────────────────────────────────────────


def test_full_grid_fast_enough_for_sync_request() -> None:
    """Полный грид достаточно быстр для синхронного вызова в теле запроса.

    Локально 12 комбо ≈ 15 мс; порог 1000 мс намеренно грубый — ловим
    деградацию НА ПОРЯДКИ (случайный I/O, БД, квадратичный взрыв), а не
    миллисекунды: на нагруженном shared CI-раннере тот же вызов занимал
    199 мс и валил жёсткий порог 100 мс (flaky, run 3917).
    """
    kwargs = _big_profitable_kwargs()
    start = time.perf_counter()
    result = optimize_program(**kwargs)
    elapsed_ms = (time.perf_counter() - start) * 1000.0
    assert result is not None
    assert elapsed_ms < 1000.0, f"optimize_program took {elapsed_ms:.1f} ms (>1000 ms)"
