"""Tests for Fix 2: radius-path нижний floor от DKP-коридора (анти-undershoot).

Симметрично corridor-clamp (верхний потолок): если median_ppm2 < dkp_low_ppm2 × factor
и anchor_tier is None → поднимаем до floor. Только radius-путь. Без dkp_raw → no-op.

Тесты:
- radius median ниже dkp_low × factor → median поднята до floor
- radius median выше dkp_low × factor → no-op (медиана не изменена)
- dkp_raw is None → no-op (нет базы для floor)
- anchor-путь (anchor_tier != None) → не затронут floor'ом
"""

from __future__ import annotations

import os
from datetime import UTC, datetime
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch

import anyio

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


# ── helpers ──────────────────────────────────────────────────────────────────


def _make_listing(*, price_per_m2: float, area_m2: float = 50.0) -> dict[str, Any]:
    return {
        "source": "cian",
        "source_url": "https://cian.ru/offer/1",
        "address": "ЕКБ, ул. Учителей, 18",
        "lat": 56.838,
        "lon": 60.595,
        "rooms": 2,
        "area_m2": area_m2,
        "floor": 5,
        "total_floors": 16,
        "price_rub": price_per_m2 * area_m2,
        "price_per_m2": price_per_m2,
        "listing_date": datetime(2026, 5, 1),
        "days_on_market": 10,
        "photo_urls": [],
        "scraped_at": datetime(2026, 5, 20, tzinfo=UTC),
        "distance_m": 150.0,
        "relevance_score": 0.1,
    }


def _make_geo():
    from app.services.geocoder import GeocodeResult

    return GeocodeResult(
        lat=56.838,
        lon=60.595,
        full_address="Свердловская обл., Екатеринбург, ул. Учителей, 18",
        provider="nominatim",
    )


def _make_payload():
    from app.schemas.trade_in import TradeInEstimateInput

    return TradeInEstimateInput(
        address="ЕКБ, ул. Учителей, 18",
        area_m2=50.0,
        rooms=2,
        floor=5,
        total_floors=16,
    )


def _run_estimate(
    analogs: list[dict[str, Any]],
    dkp_raw: dict[str, Any] | None,
    *,
    radius_floor_factor: float = 0.8,
) -> Any:
    from app.core.config import settings
    from app.services.estimator import estimate_quality

    db = MagicMock()
    payload = _make_payload()

    async def _run() -> Any:
        with (
            patch("app.services.estimator.geocode", new=AsyncMock(return_value=_make_geo())),
            patch("app.services.estimator.dadata_clean_address", new=AsyncMock(return_value=None)),
            patch("app.services.estimator.match_house_readonly", return_value=None),
            patch("app.services.estimator.get_house_metadata", new=AsyncMock(return_value=None)),
            patch(
                "app.services.estimator._fetch_analogs",
                return_value=(list(analogs), False, "S"),
            ),
            patch("app.services.estimator._fetch_deals", return_value=[]),
            patch(
                "app.services.estimator._get_or_fetch_imv_cached",
                new=AsyncMock(return_value=None),
            ),
            patch(
                "app.services.estimator._get_or_fetch_yandex_valuation_cached",
                new=AsyncMock(return_value=None),
            ),
            patch(
                "app.services.estimator.estimate_via_cian_valuation",
                new=AsyncMock(return_value=None),
            ),
            patch("app.services.estimator._fetch_dkp_corridor", return_value=dkp_raw),
            patch("app.services.estimator._get_asking_sold_ratio", return_value=(None, None)),
            patch.object(settings, "estimate_radius_floor_factor", radius_floor_factor),
        ):
            return await estimate_quality(payload, db)

    return anyio.run(_run)


# ── тест 1: radius median ниже floor → поднята ───────────────────────────────


def test_radius_median_below_dkp_floor_is_lifted() -> None:
    """Radius median = 80k, dkp_low = 150k, factor 0.8 → floor = 120k → median поднята."""
    # analogs с median ~80k
    analogs = [
        _make_listing(price_per_m2=75_000.0),
        _make_listing(price_per_m2=80_000.0),
        _make_listing(price_per_m2=85_000.0),
    ]
    dkp_raw = {
        "count": 15,
        "low_ppm2": 150_000,
        "median_ppm2": 180_000,
        "high_ppm2": 220_000,
        "period_months": 12,
    }
    est = _run_estimate(analogs, dkp_raw, radius_floor_factor=0.8)

    floor_ppm2 = 150_000 * 0.8  # 120_000
    assert (
        est.median_price_per_m2 >= floor_ppm2
    ), f"median_ppm2={est.median_price_per_m2} должна быть >= floor={floor_ppm2}"


# ── тест 2: radius median выше floor → no-op ─────────────────────────────────


def test_radius_median_above_dkp_floor_unchanged() -> None:
    """Radius median = 200k > dkp_low × factor = 150k × 0.8 = 120k → no-op."""
    analogs = [
        _make_listing(price_per_m2=190_000.0),
        _make_listing(price_per_m2=200_000.0),
        _make_listing(price_per_m2=210_000.0),
    ]
    dkp_raw = {
        "count": 15,
        "low_ppm2": 150_000,
        "median_ppm2": 180_000,
        "high_ppm2": 220_000,
        "period_months": 12,
    }
    est = _run_estimate(analogs, dkp_raw, radius_floor_factor=0.8)

    # floor = 150k × 0.8 = 120k; median = 200k > floor → no-op, медиана не изменяется
    # Медиана должна остаться в диапазоне аналогов (190-210k), а не подняться к floor.
    floor_ppm2 = 150_000 * 0.8  # 120_000
    assert est.median_price_per_m2 > floor_ppm2, "median должна быть выше floor (no-op)"
    # Медиана соответствует аналогам (~200k), а не floor
    assert (
        180_000 <= est.median_price_per_m2 <= 220_000
    ), f"median_ppm2={est.median_price_per_m2} должна остаться в диапазоне аналогов (no-op)"


# ── тест 3: dkp_raw is None → no-op ─────────────────────────────────────────


def test_no_dkp_raw_no_floor() -> None:
    """Без dkp_raw floor не применяется — median остаётся radius-значением."""
    analogs = [
        _make_listing(price_per_m2=75_000.0),
        _make_listing(price_per_m2=80_000.0),
        _make_listing(price_per_m2=85_000.0),
    ]
    est = _run_estimate(analogs, dkp_raw=None, radius_floor_factor=0.8)

    # median ~80k, без dkp_raw floor не поднимает
    assert (
        est.median_price_per_m2 < 100_000
    ), f"median_ppm2={est.median_price_per_m2} без dkp_raw не должна расти"
