"""#2265 gap — live QA 2026-07-03: якорный пул (Tier A/C) не дедупился (regression).

PR #2273 ввёл `_dedup_cross_source` и применил его к РАДИУСНОМУ списку `listings`
внутри `_price_from_inputs` (см. test_estimator_dedup_cross_source_2087.py — 9+
тестов покрывают именно эту ветку и НЕ трогаются здесь).

Живой QA (2026-07-03, POST /api/v1/trade-in/estimate, "Рассветная 5", area=33,
rooms=1, analog_tier=micro_radius) воспроизвёл тот же класс бага на ДРУГОМ пути:
`_fetch_anchor_comps` (same_building / micro_radius, свой raw SQL) никогда не
проходил через `_dedup_cross_source` — физлот "ул. Сыромолотова, 11" (33.6 м²,
эт. 3, 4.0М, cadastral_number пуст) пришёл дважды (source=domklik + source=cian)
и оба раза попал в показанные analogs, а n_analogs считался по этой же
недедупленной anchor-выборке (согласован сам с собой, но нечестен по сути —
считает один физлот дважды).

Фикс: `_price_from_inputs` дедупит `anchor_comps` ТЕМ ЖЕ `_dedup_cross_source`
сразу при входе — до `_compute_same_building_anchor` (даёт n) и до
`anchor_comps_used = anchor_comps` (даёт показанные analogs). Оба места читают
один и тот же (теперь задедупленный) список → n_analogs синхронен с analogs
автоматически, без отдельной синхронизации.

Harness: переиспользует `_run_estimate`/`_make_payload` из test_same_building_anchor.py
(тот же паттерн, что test_estimator_ghost_anchor_radius_empty_1871.py) — полный
estimate-путь со всеми I/O застабленными, `_fetch_anchor_comps` форсирован на
заданный anchor_comps/tier.
"""

from __future__ import annotations

import importlib.util
import os
from pathlib import Path
from typing import Any

# Settings требует DATABASE_URL при инициализации (fail-fast, C-3).
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db")

import pytest

_ANCHOR_TEST = Path(__file__).parent / "test_same_building_anchor.py"
_spec = importlib.util.spec_from_file_location("_anchor_harness_dedup_2265", _ANCHOR_TEST)
assert _spec is not None and _spec.loader is not None
_h = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_h)


def _comp(
    *,
    source: str,
    address: str,
    area: float,
    price_rub: int,
    floor: int,
    rooms: int = 1,
    cad: str | None = None,
) -> dict[str, Any]:
    """Мимикрирует _anchor_comp_from_row (#694) — те же ключи, что даёт SQL-строка."""
    ppm2 = round(price_rub / area)
    return {
        "price_per_m2": ppm2,
        "area_m2": area,
        "rooms": rooms,
        "floor": floor,
        "total_floors": 5,
        "address": address,
        "source": source,
        "source_url": None,
        "price_rub": price_rub,
        "listing_date": None,
        "days_on_market": None,
        "photo_urls": None,
        "lat": None,
        "lon": None,
        "building_cadastral_number": cad,  # пуст у обоих source (живой прод-кейс)
    }


def _payload():  # type: ignore[no-untyped-def]
    return _h._make_payload(area=33.0, rooms=1)


# Живой физлот (QA 2026-07-03): "ул. Сыромолотова, 11", 33.6 м², эт. 3, 4.0М,
# cadastral_number пуст, встречается ДВАЖДЫ (source=domklik, source=cian) —
# байт-в-байт та же пара форматов адреса, что в
# test_dedup_collapses_three_source_formats (2087-тесте, но там дедуп проверяется
# напрямую на _dedup_cross_source, не через якорный путь _price_from_inputs).
_DUP_LOT_CIAN = _comp(
    source="cian",
    address="р-н Кировский, мкр. ЖБИ, улица Сыромолотова, 11",
    area=33.6,
    price_rub=4_000_000,
    floor=3,
)
_DUP_LOT_DOMKLIK = _comp(
    source="domklik",
    address="Россия, Екатеринбург, ЖБИ м-н, улица Сыромолотова",
    area=33.6,
    price_rub=4_000_000,
    floor=3,
)

# 3 доп. уникальных компла того же дома/подъезда — ppm2 близки к дубль-лоту
# (119k-125k), MAD-clip (k=3.5) их не тронет; вместе с дубль-лотом дают
# estimate_sb_min_comps=4 unique комплов ПОСЛЕ дедупа → якорь срабатывает.
_OTHER_COMPS = [
    _comp(
        source="avito", address="улица Сыромолотова, 13", area=40.0, price_rub=5_000_000, floor=5
    ),
    _comp(
        source="yandex", address="улица Сыромолотова, 15", area=45.0, price_rub=5_500_000, floor=2
    ),
    _comp(source="cian", address="улица Сыромолотова, 17", area=50.0, price_rub=6_000_000, floor=6),
]

_ANCHOR_COMPS_WITH_DUP = [_DUP_LOT_CIAN, _DUP_LOT_DOMKLIK, *_OTHER_COMPS]  # 5 raw, 4 unique

# Минимальный изолированный репро: РОВНО тот физлот из QA (#2265), встреченный
# дважды (cian+domklik), БЕЗ доп. комплов и БЕЗ MAD-clip-эффектов (два одинаковых
# ppm2 → MAD=0 → ничего не отсекается). min_comps монкипатчится до 1 в тесте, чтобы
# изолировать именно дедуп-гейт, не threshold-гейт по числу комплов.
_MINIMAL_DUP_PAIR = [_DUP_LOT_CIAN, _DUP_LOT_DOMKLIK]


def test_anchor_path_dedups_cross_source_duplicate_lot(monkeypatch: pytest.MonkeyPatch) -> None:
    """Regression #2265 gap: якорный пул (Tier A) дедупится ТАК ЖЕ, как радиусный.

    Живой прод-физлот "ул. Сыромолотова, 11" (33.6 м², эт. 3, 4.0М, cadnum пуст)
    пришёл дважды (cian + domklik) — точная пара из QA-репро.

    До фикса: `_price_from_inputs` не дедупил anchor_comps → якорь строится на 2
    сырых комплах (один физлот дважды) → est.analogs показывает дубль ДВАЖДЫ (n=2).
    После фикса: дедуп схлопывает пару в 1 представителя ДО _compute_same_building_
    anchor → est.analogs содержит физлот ровно ОДИН раз (n=1).
    """
    from app.core.config import settings

    with monkeypatch.context() as m:
        m.setattr(settings, "estimate_sb_min_comps", 1)
        est = _h._run_estimate(anchor_comps=_MINIMAL_DUP_PAIR, anchor_tier="A", payload=_payload())

    assert len(est.analogs) == 1  # НЕ 2 — дубль-лот схлопнут в 1 представителя
    dup_ppm2 = round(4_000_000 / 33.6)
    assert est.analogs[0].price_per_m2 == dup_ppm2
    # представитель дубль-группы — один из {cian, domklik} (tie-break детерминирован
    # в _dedup_rep_key, нам важен сам факт единственности, не кто именно победил).
    assert est.analogs[0].source in ("cian", "domklik")


def test_anchor_path_n_analogs_matches_shown_count(monkeypatch: pytest.MonkeyPatch) -> None:
    """Честность счётчика: n_analogs в anchor-пути описывает ИМЕННО показанные
    (постдедуп) analogs, а не сырой недедупленный anchor-пул. До фикса n_analogs=2
    при одном реальном физлоте (дубль посчитан дважды) — самосогласованно, но
    нечестно. После фикса n_analogs=1, синхронно с analogs."""
    from app.core.config import settings

    with monkeypatch.context() as m:
        m.setattr(settings, "estimate_sb_min_comps", 1)
        est = _h._run_estimate(anchor_comps=_MINIMAL_DUP_PAIR, anchor_tier="A", payload=_payload())

    assert est.n_analogs == len(est.analogs) == 1


def test_anchor_path_dedup_also_applies_larger_pool() -> None:
    """Контроль на более крупном пуле (5 raw / 4 unique, дефолтный min_comps=4):
    дедуп срабатывает не только в вырожденном 2-элементном случае."""
    est = _h._run_estimate(anchor_comps=_ANCHOR_COMPS_WITH_DUP, anchor_tier="A", payload=_payload())
    assert len(est.analogs) == 4
    assert est.n_analogs == 4
    dup_ppm2 = round(4_000_000 / 33.6)
    assert [a.price_per_m2 for a in est.analogs].count(dup_ppm2) == 1


def test_anchor_path_no_false_merge_of_distinct_units(monkeypatch: pytest.MonkeyPatch) -> None:
    """Контроль анти-катастрофы: БЕЗ дубля (4 явно различных физлота) дедуп не
    трогает выборку — n_analogs/analogs остаются 4/4, ничего лишнего не схлопнуто."""
    comps = [*_OTHER_COMPS, _DUP_LOT_CIAN]
    est = _h._run_estimate(anchor_comps=comps, anchor_tier="A", payload=_payload())
    assert len(est.analogs) == 4
    assert est.n_analogs == 4
    shown_ppm2 = sorted(a.price_per_m2 for a in est.analogs)
    expected_ppm2 = sorted(c["price_per_m2"] for c in comps)
    assert shown_ppm2 == expected_ppm2


if __name__ == "__main__":  # pragma: no cover
    raise SystemExit(pytest.main([__file__, "-q"]))
