"""Tests for the oblast-D deals-headline-fallback (non-EKB accuracy gap).

Context: `listings` coverage is ~40k EKB / ~0 non-EKB (Нижний Тагил / Серов /
Каменск-Уральский). Before this fix, a non-EKB subject either:
  (a) leaked a DISTANT EKB listing median via the Tier S address-prefix
      fallback (no geo bound — see `_fetch_analogs` Tier S fix), or
  (b) surfaced an empty ("n/a") headline once (a) was fixed and the
      geo-bound radius tiers legitimately found 0 local listings.

This fallback builds the headline from the ДКП deal corridor (`dkp_raw`,
already city+street-scoped via `_resolve_target_city`/`_fetch_dkp_corridor`)
instead — honestly flagged 'low' confidence, deals-only.

These tests exercise `estimate_quality` end-to-end (real spine code, mocked
DB-facing helpers only) — the same pattern as test_estimator_radius_floor.py.
"""

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 = 45.0) -> dict[str, Any]:
    """An EKB analog — used only in the "EKB unaffected" control test."""
    return {
        "source": "cian",
        "source_url": "https://cian.ru/offer/1",
        "address": "ЕКБ, ул. Малышева, 30",
        "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_tagil():
    from app.services.geocoder import GeocodeResult

    return GeocodeResult(
        lat=57.9094,
        lon=59.9789,
        full_address="Свердловская обл., Нижний Тагил, ул. Ленина, 5",
        provider="nominatim",
    )


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

    return TradeInEstimateInput(
        address="Нижний Тагил, ул. Ленина, 5",
        area_m2=45.0,
        rooms=2,
        floor=5,
        total_floors=9,
    )


def _run_estimate(
    *,
    analogs: list[dict[str, Any]],
    dkp_raw: dict[str, Any] | None,
    geo: Any,
    payload: Any,
) -> Any:
    from app.services.estimator import estimate_quality

    db = MagicMock()

    async def _run() -> Any:
        with (
            patch("app.services.estimator.geocode", new=AsyncMock(return_value=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",
                # Post Part-1 (geo-bound Tier S) reality for a non-EKB town: the
                # radius/tier ladder legitimately returns NOTHING — tier='W' is
                # what the always-executed final fallback tier returns.
                return_value=(list(analogs), False, "W"),
            ),
            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)),
        ):
            return await estimate_quality(payload, db)

    return anyio.run(_run)


# ── Нижний Тагил: no local listings, deal corridor present ───────────────────


def test_non_ekb_empty_listings_uses_deals_headline() -> None:
    """0 local listings + a usable ДКП corridor → headline comes from deals.

    Mirrors the reported Нижний Тагил gap: deal_median ≈ 85 911 ₽/м² (accurate)
    vs the old EKB-leaked asking headline ≈ 186 461 (~6x over). After the fix,
    the headline must equal the deal corridor's median — nowhere near the
    EKB-range figure — and confidence must be honestly 'low' (deals-only, zero
    scraped analogs).
    """
    dkp_raw = {
        "count": 12,
        "low_ppm2": 70_000,
        "median_ppm2": 85_911,
        "high_ppm2": 100_000,
        "period_months": 12,
    }
    est = _run_estimate(
        analogs=[],  # 0 listings — the honest post-geo-bound-fix reality
        dkp_raw=dkp_raw,
        geo=_make_geo_tagil(),
        payload=_make_payload_tagil(),
    )

    assert est.median_price_per_m2 == 85_911, (
        f"headline={est.median_price_per_m2} must equal the deal corridor "
        f"median, not 0/n-a and nowhere near an EKB-range figure (~186k)"
    )
    assert est.median_price_per_m2 < 120_000, "must NOT be EKB-leaked (~186k)"
    assert est.n_analogs == 0, "honest: zero scraped-listing analogs were used"
    assert est.confidence == "low", "deals-only headline must be honestly low-confidence"
    assert est.median_price_rub == round(85_911 * 45.0)
    # Range should bracket the corridor's P10/P90, not collapse to a point.
    assert est.range_low_rub <= est.median_price_rub <= est.range_high_rub


def test_non_ekb_empty_listings_no_deals_stays_insufficient() -> None:
    """0 listings + NO deal corridor either → stays honest n/a (median=0).

    Guards against the fallback inventing a number when there is truly no
    signal at all (e.g. Каменск-Уральский with an unresolvable street).
    """
    est = _run_estimate(
        analogs=[],
        dkp_raw=None,
        geo=_make_geo_tagil(),
        payload=_make_payload_tagil(),
    )
    assert est.median_price_per_m2 == 0
    assert est.n_analogs == 0
    assert est.confidence == "low"


def test_non_ekb_thin_deal_corridor_below_min_n_stays_insufficient() -> None:
    """Deal corridor exists but below DEALS_HEADLINE_FALLBACK_MIN_N → no fallback.

    A single stale sold price should not become the town's headline.
    """
    dkp_raw = {
        "count": 1,
        "low_ppm2": 70_000,
        "median_ppm2": 85_911,
        "high_ppm2": 100_000,
        "period_months": 12,
    }
    est = _run_estimate(
        analogs=[],
        dkp_raw=dkp_raw,
        geo=_make_geo_tagil(),
        payload=_make_payload_tagil(),
    )
    assert est.median_price_per_m2 == 0
    assert est.n_analogs == 0


# ── EKB control: dense local listings → deals-fallback must NOT engage ───────


def test_ekb_with_dense_listings_ignores_deals_fallback() -> None:
    """EKB has plenty of local listings — the radius-path headline must win,
    NOT the deal corridor, even though dkp_raw is present (byte-green guard:
    EKB must stay on the existing listings-median path unconditionally).
    """
    from app.schemas.trade_in import TradeInEstimateInput
    from app.services.geocoder import GeocodeResult

    analogs = [
        _make_listing(price_per_m2=140_000.0),
        _make_listing(price_per_m2=145_000.0),
        _make_listing(price_per_m2=150_000.0),
    ]
    dkp_raw = {
        "count": 20,
        "low_ppm2": 120_000,
        "median_ppm2": 144_000,
        "high_ppm2": 160_000,
        "period_months": 12,
    }
    geo = GeocodeResult(
        lat=56.838,
        lon=60.595,
        full_address="Свердловская обл., Екатеринбург, ул. Малышева, 30",
        provider="nominatim",
    )
    payload = TradeInEstimateInput(
        address="ЕКБ, ул. Малышева, 30", area_m2=45.0, rooms=2, floor=5, total_floors=16
    )
    est = _run_estimate(analogs=analogs, dkp_raw=dkp_raw, geo=geo, payload=payload)

    # Headline built from the LISTINGS median (~145k), not silently replaced —
    # n_analogs must reflect the real listing count (deals-fallback never ran).
    assert est.n_analogs == len(analogs)
    assert 140_000 <= est.median_price_per_m2 <= 150_000
