"""#2207 — estimate_quality не должен блокировать event loop.

`estimate_quality` выпускает ~28 СИНХРОННЫХ psycopg PostGIS-запросов (multi-tier
analogs ×4, deals, price_trend, anchor comps, INSERT+commit и т.д.). На проде это
один uvicorn worker → одна медленная оценка раньше стопила ВЕСЬ event loop,
включая /health. Фикс #2207: каждый sync-кластер вынесен через
``asyncio.to_thread(...)``.

Тест патчит ОДИН тяжёлый sync-хелпер (`_fetch_analogs`) на блокирующий
``time.sleep(0.3)`` и запускает `estimate_quality` конкурентно с лёгкой
корутиной (``asyncio.sleep(0.05)``). Если бы loop блокировался, лёгкая корутина
не смогла бы возобновиться до конца sync-сна; мы ассертим, что она завершается
у ~0.05с — задолго до оценки, — что доказывает свободный loop.

Стиль зеркалит tests/test_estimator_client_coords.py.
"""

import asyncio
import contextlib
import os
import time

# Settings requires DATABASE_URL at init time. Set dummy DSN before any app import.
os.environ.setdefault("DATABASE_URL", "postgresql://test:test@localhost/test_db")

from unittest.mock import AsyncMock, MagicMock, patch


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

    return TradeInEstimateInput(
        address="ЕКБ, ул. Учителей, 18",
        area_m2=38.8,
        rooms=1,
        floor=4,
        total_floors=16,
        # in-EKB coords → geocode() пропускается (Variant A), сеть не трогаем.
        lat=56.838,
        lon=60.595,
    )


# Тяжёлый sync-хелпер: имитирует медленный PostGIS-запрос блокирующим сном.
def _slow_fetch_analogs(*_args, **_kwargs):
    time.sleep(0.3)
    return ([], False, "W")


def _downstream_patches():
    """Оффлайн-моки так, чтобы estimate_quality дошёл до конца детерминированно."""
    return (
        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", side_effect=_slow_fetch_analogs),
        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._get_asking_sold_ratio", return_value=(None, None)),
    )


def test_estimate_quality_does_not_block_event_loop() -> None:
    """Медленный sync-путь оценки не должен starve'ить event loop."""
    from app.services.estimator import estimate_quality

    db = MagicMock()
    payload = _make_payload()

    async def _run() -> dict[str, float]:
        with contextlib.ExitStack() as stack:
            for cm in _downstream_patches():
                stack.enter_context(cm)

            timings: dict[str, float] = {}
            start = time.perf_counter()

            async def light() -> None:
                await asyncio.sleep(0.05)
                timings["light"] = time.perf_counter() - start

            async def heavy() -> None:
                await estimate_quality(payload, db)
                timings["estimate"] = time.perf_counter() - start

            await asyncio.gather(light(), heavy())
            return timings

    timings = asyncio.run(_run())

    # Лёгкая корутина завершилась ДО оценки в целом.
    assert timings["light"] < timings["estimate"], timings
    # И — ключевое — возобновилась у ~0.05с, а НЕ после 0.3с sync-сна
    # (было бы >=0.3, если бы loop блокировался). Потолок с запасом на CI-jitter.
    assert timings["light"] < 0.25, timings
    # Оценка реально ждала sync-работу в потоке(ах): >=0.3с (>=1 sleep).
    assert timings["estimate"] >= 0.3, timings
