"""#1186 — query-side guard: novostroyki строки не попадают в comp-пул вторички.

Тесты статические (SQL-string inspection) + поведенческие (_fetch_analogs mock).

Каноничный предикат: (listing_segment IS NULL OR listing_segment = 'vtorichka')
NULL = legacy вторичка до миграции 011 — отбрасывать нельзя.
"""

from __future__ import annotations

import inspect
import os
import re
from typing import Any
from unittest.mock import MagicMock

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

from app.services import estimator as est_mod
from app.tasks import asking_to_sold_ratio as ratio_mod

# ── Canonical guard predicate ─────────────────────────────────────────────────

_GUARD = "(listing_segment IS NULL OR listing_segment = 'vtorichka')"
_GUARD_RE = re.compile(
    r"\((?:\w+\.)?listing_segment\s+IS\s+NULL\s+OR\s+(?:\w+\.)?listing_segment\s*=\s*'vtorichka'\)",
    re.IGNORECASE,
)


def _norm(s: str) -> str:
    """Drop -- comments и склей пробелы для устойчивого поиска."""
    no_comments = re.sub(r"--[^\n]*", "", s)
    return re.sub(r"\s+", " ", no_comments).strip()


# ── estimator.py static checks ────────────────────────────────────────────────


def test_common_where_has_canonical_guard() -> None:
    """_COMMON_WHERE используется Tier S и Tier H — должен содержать канон-предикат."""
    assert _GUARD_RE.search(
        est_mod._COMMON_WHERE
    ), "_COMMON_WHERE lacks novostroyki guard — Tier S/H comp set contaminated"


def test_common_where_no_old_neq_form() -> None:
    """Старый вариант listing_segment <> 'novostroyki' запрещён — не охватывает иные сегменты."""
    assert "listing_segment <> 'novostroyki'" not in est_mod._COMMON_WHERE
    assert "listing_segment != 'novostroyki'" not in est_mod._COMMON_WHERE


def test_tier_w_inline_has_canonical_guard() -> None:
    """Tier W использует inline SQL, а не _COMMON_WHERE — нужен собственный guard."""
    # Получаем исходник функции _fetch_analogs.
    src = inspect.getsource(est_mod._fetch_analogs)
    # Tier W — inline блок (не через _COMMON_WHERE).
    assert _GUARD_RE.search(src), "_fetch_analogs Tier W inline SQL lacks novostroyki guard"
    # Старой формы быть не должно.
    assert "listing_segment <> 'novostroyki'" not in src
    assert "listing_segment != 'novostroyki'" not in src


def test_tier_a_anchor_gated_python_side_not_sql_guard() -> None:
    """#1774: Tier A больше НЕ хардкодит SQL-guard — он делает gated Python-side
    relaxation (впускает novostroyki только если в доме есть вторичка/NULL).

    Assert: (a) Tier C всё ещё несёт канон-guard в SQL, (b) Tier A делает
    Python-side gating — источник содержит hardened secondary_count/primary_count
    gate (#1774, после code-review).
    """
    src = inspect.getsource(est_mod._fetch_anchor_comps)
    # (a) Tier C SQL guard сохранён (поиск находит единственное вхождение — Tier C).
    assert _GUARD_RE.search(src), "Tier C anchor SQL lost the canonical novostroyki guard"
    # (b) Tier A теперь gated Python-side (#1774).
    # Hardened gate: secondary_count ≥ primary_count (заменил has_secondary).
    assert "secondary_count" in src, "Tier A lacks secondary_count Python-side gate (#1774)"
    assert "primary_count" in src, "Tier A lacks primary_count comparison in hardened gate (#1774)"


def test_tier_c_anchor_has_canonical_guard_not_segment_param() -> None:
    """Tier C anchor — канон-предикат, не CAST(:segment AS text) (тот вариант отбрасывал NULL)."""
    src = inspect.getsource(est_mod._fetch_anchor_comps)
    # Canonical guard present (Tier C; Tier A guard снят в #1774 → gated Python-side).
    assert _GUARD_RE.search(src)
    # Старый параметрический вариант убран.
    assert "listing_segment = CAST(:segment AS text)" not in src


# ── asking_to_sold_ratio.py static checks ─────────────────────────────────────

_REDERIVE_SQL_TEXT = str(ratio_mod._REDERIVE_SQL.text)


def test_ask_side_cte_has_guard() -> None:
    """ask_side CTE в _REDERIVE_SQL (per-rooms asking медиана) — guard обязателен."""
    assert _GUARD_RE.search(
        _REDERIVE_SQL_TEXT
    ), "ask_side CTE in _REDERIVE_SQL lacks novostroyki guard"


def test_ask_global_cte_has_guard() -> None:
    """ask_global CTE в _REDERIVE_SQL (global fallback asking медиана) — guard обязателен."""
    # _REDERIVE_SQL содержит два `ask_global`-блока; ищем оба через count.
    matches = len(_GUARD_RE.findall(_REDERIVE_SQL_TEXT))
    assert (
        matches >= 2
    ), f"Expected ≥2 guard occurrences in _REDERIVE_SQL (ask_side + ask_global), got {matches}"


# ── Поведенческие тесты: _fetch_analogs (mock DB) ────────────────────────────


def _make_listing(
    *,
    source: str = "avito",
    address: str = "ул. Ленина, 1",
    distance_m: float = 100.0,
    relevance_score: float = 0.1,
    price_rub: float = 5_000_000.0,
    area_m2: float = 40.0,
    rooms: int = 2,
    listing_segment: str | None = None,
) -> dict[str, Any]:
    """Минимальный listing-dict, имитирующий вывод DB mapping."""
    return {
        "source": source,
        "source_url": f"https://{source}.ru/offer/1",
        "address": address,
        "lat": 56.838,
        "lon": 60.595,
        "rooms": rooms,
        "area_m2": area_m2,
        "floor": 3,
        "total_floors": 9,
        "price_rub": price_rub,
        "price_per_m2": price_rub / area_m2,
        "listing_date": None,
        "days_on_market": 15,
        "photo_urls": [],
        "scraped_at": None,
        "distance_m": distance_m,
        "relevance_score": relevance_score,
        "building_cadastral_number": None,
        "listing_segment": listing_segment,
    }


def _make_db_mock(rows: list[dict[str, Any]]) -> MagicMock:
    """Session mock: db.execute().mappings().all() возвращает rows."""
    db = MagicMock()
    db.execute.return_value.mappings.return_value.all.return_value = rows
    return db


def test_fetch_analogs_sql_guard_present_novostroyki_excluded() -> None:
    """SQL guard корректно записан: listing_segment='novostroyki' не должен присутствовать
    в WHERE-предикате (guard зарыт в SQL, а не в Python post-filter).

    Тест проверяет СТАТИКУ SQL внутри _fetch_analogs (Tier W path), а не runtime фильтрацию —
    runtime-фильтрацию сделала бы только интеграционная БД. Убеждаемся, что кто-то в будущем
    не переписал SQL, убрав guard.
    """
    src = inspect.getsource(est_mod._fetch_analogs)
    # Guard должен присутствовать хотя бы один раз в теле функции.
    assert _GUARD_RE.search(
        src
    ), "_fetch_analogs SQL no longer contains novostroyki guard — guard was removed!"


def test_null_segment_listing_not_excluded_by_guard() -> None:
    """listing_segment=NULL (legacy вторичка до м.011) должны ПОПАСТЬ в comps.

    Поведенческий тест: mock возвращает строку с segment=None, _fetch_analogs
    должен включить её в результат (_stratify_candidates не отфильтровывает NULL).
    """
    from app.services.estimator import _fetch_analogs

    null_seg_listing = _make_listing(listing_segment=None, distance_m=50.0, relevance_score=0.05)
    db = _make_db_mock([null_seg_listing])

    result, _fallback, _tier = _fetch_analogs(
        db, lat=56.838, lon=60.595, rooms=2, area=40.0, radius_m=1000
    )

    # NULL-segment listing должен присутствовать в результате (не отброшен Python-стороной).
    assert any(
        r.get("source") == "avito" for r in result
    ), "NULL-segment listing was unexpectedly excluded from comp set"


def test_vtorichka_segment_listing_included() -> None:
    """listing_segment='vtorichka' — явный вторичный сегмент — должен попасть в comps."""
    from app.services.estimator import _fetch_analogs

    vtor_listing = _make_listing(listing_segment="vtorichka", distance_m=80.0)
    db = _make_db_mock([vtor_listing])

    result, _, _ = _fetch_analogs(db, lat=56.838, lon=60.595, rooms=2, area=40.0, radius_m=1000)
    assert len(result) >= 1, "vtorichka-segment listing was unexpectedly excluded"


# ── Негативные assert'ы: запрещённые формы guard'а во всём estimator'е ────────


def _estimator_full_src() -> str:
    """Полный исходник модуля estimator (для статической проверки всех SQL-блоков)."""
    return inspect.getsource(est_mod)


def test_no_bare_vtorichka_eq_without_null_guard() -> None:
    """Запрет голого `listing_segment = 'vtorichka'` без IS NULL OR части.

    Такой вариант отбросит NULL (legacy вторичку до миграции 011) — баг охвата.
    """
    src = _estimator_full_src()
    # Ищем вхождения = 'vtorichka' которым НЕ предшествует IS NULL OR на той же строке.
    bad_matches = [
        line
        for line in src.splitlines()
        if "= 'vtorichka'" in line and "IS NULL OR" not in line and "--" not in line.lstrip()[:3]
    ]
    assert not bad_matches, (
        "Found bare `= 'vtorichka'` without IS NULL OR guard in estimator.py:\n"
        + "\n".join(bad_matches)
    )


def test_no_neq_novostroyki_form_in_estimator() -> None:
    """Запрет формы `listing_segment <> 'novostroyki'` / `!= 'novostroyki'` в estimator.

    Эта форма пропускает прочие сегменты (коммерция и т.п.) и не охватывает NULL.
    Канон: IS NULL OR = 'vtorichka'.
    """
    src = _estimator_full_src()
    assert (
        "<> 'novostroyki'" not in src
    ), "estimator.py contains deprecated `<> 'novostroyki'` form — use canonical guard"
    assert (
        "!= 'novostroyki'" not in src
    ), "estimator.py contains deprecated `!= 'novostroyki'` form — use canonical guard"


def test_anchor_comps_no_dead_listing_segment_param() -> None:
    """_fetch_anchor_comps не должна принимать listing_segment как параметр.

    После #1186 guard хардкожен в SQL — параметр убран, чтобы caller не мог
    случайно передать 'novostroyki' или другой сегмент и обойти guard.
    """
    import inspect as _inspect

    sig = _inspect.signature(est_mod._fetch_anchor_comps)
    assert "listing_segment" not in sig.parameters, (
        "_fetch_anchor_comps still has `listing_segment` parameter — "
        "it was removed in #1186 review fixup; guard is hardcoded in SQL"
    )


def test_fetch_anchor_comps_tier_c_canonical_guard_not_parametric() -> None:
    """Tier C в _fetch_anchor_comps использует канон-guard, а не параметрическое сравнение.

    Параметрическая форма `listing_segment = CAST(:segment AS text)` исключала NULL
    и давала caller'у возможность выбрать произвольный сегмент (включая novostroyki).
    """
    src = inspect.getsource(est_mod._fetch_anchor_comps)
    assert "CAST(:segment AS text)" not in src, (
        "_fetch_anchor_comps Tier C still uses parametric segment filter — "
        "must be hardcoded canonical guard"
    )
    # И канон-guard на месте (Tier C-блок).
    assert _GUARD_RE.search(
        src
    ), "_fetch_anchor_comps lacks canonical guard after removing parametric form"
