"""Audit finding #1 — Tier S radius-fallback house-number boundary.

Old bug: `_fetch_analogs` Tier S-fallback matched `address ILIKE short_addr + '%'`
with NO boundary after the house number, so "ул. Ленина, 5%" collided with
"..., 50" / "..., 51" / "..., 500" / "..., 5а" (a DIFFERENT building one street
address away). MAX_ANALOGS_PER_ADDRESS=5 + the `len(tier_s) >= 3` threshold meant
a single stray building could poison the same-building median.

Fix reuses the anchor Tier A machinery (`_normalize_building_key` +
`_house_boundary_regex`, estimator.py ~1741) instead of a bare string prefix —
same numeric-boundary regex principle, now shared by both tiers.
"""

from __future__ import annotations

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

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

from app.services.estimator import _fetch_analogs, _house_boundary_regex


def _norm(addr: str) -> str:
    return addr.lower().replace("ё", "е")


# ── _house_boundary_regex: pure boundary semantics ───────────────────────────


def test_no_letter_matches_bare_number_and_corpus_slash() -> None:
    pat = _house_boundary_regex(5, None)
    assert re.search(pat, _norm("ул. Ленина, 5"))
    assert re.search(pat, _norm("ул. Ленина, 5/2"))  # корпус-слэш — тот же дом


def test_no_letter_rejects_longer_numbers_sharing_prefix() -> None:
    """Ядро бага: «Ленина, 5» не должен матчить 50/51/500/15 (соседние дома)."""
    pat = _house_boundary_regex(5, None)
    for other in ("ул. Ленина, 50", "ул. Ленина, 51", "ул. Ленина, 500", "ул. Ленина, 15"):
        assert not re.search(pat, _norm(other)), other


def test_no_letter_rejects_lettered_variant() -> None:
    """Без литеры (5) НЕ матчит корпус-литеру (5а) — разные дома (симметрично
    Tier A: «литера при наличии обязательна», 204г ≠ 204д ≠ 204)."""
    pat = _house_boundary_regex(5, None)
    assert not re.search(pat, _norm("ул. Ленина, 5а"))


def test_letter_requires_matching_letter() -> None:
    pat = _house_boundary_regex(204, "г")
    assert re.search(pat, _norm("8 Марта, 204г"))
    assert not re.search(pat, _norm("8 Марта, 204д"))
    assert not re.search(pat, _norm("8 Марта, 204"))


# ── _fetch_analogs Tier S-fallback: SQL uses boundary regex, not bare ILIKE ──


def _capture_calls(full_address: str | None) -> list[tuple[Any, ...]]:
    db = MagicMock()
    captured: list[tuple[Any, ...]] = []

    def side_effect(*args: Any, **kwargs: Any) -> MagicMock:
        captured.append(args)
        result = MagicMock()
        result.mappings.return_value.all.return_value = []
        return result

    db.execute.side_effect = side_effect
    _fetch_analogs(
        db,
        lat=56.84,
        lon=60.65,
        rooms=2,
        area=50.0,
        radius_m=1000,
        full_address=full_address,
        target_house_id=None,
    )
    return captured


def test_tier_s_fallback_uses_boundary_regex_not_bare_ilike() -> None:
    calls = _capture_calls("Екатеринбург, ул. Ленина, 5")
    assert calls, "Tier S-fallback должен выполнить хотя бы один запрос"
    sql_text, params = str(calls[0][0]), calls[0][1]
    assert "ILIKE" not in sql_text
    assert ":house_re" in sql_text
    assert ":street_like" in sql_text
    assert params["house_re"] == _house_boundary_regex(5, None)
    assert params["street_like"] == "%ленина%"


def test_tier_s_fallback_distance_m_no_longer_hardcoded_zero() -> None:
    """Второй audit-пункт: distance_m больше не 0.0-хардкод, а реальный ST_Distance."""
    calls = _capture_calls("Екатеринбург, ул. Ленина, 5")
    sql_text = str(calls[0][0])
    assert "0.0 AS distance_m" not in sql_text
    assert "ST_Distance(geom::geography" in sql_text


def test_tier_s_fallback_no_house_number_skips_block() -> None:
    """Адрес без номера дома → base_no=None → Tier S-fallback пропущен (не крашится,
    падаем дальше по Tier H/W)."""
    calls = _capture_calls("р-н Центр, улица Бориса Ельцина")
    for args in calls:
        assert ":house_re" not in str(args[0])


def test_tier_s_fallback_none_address_skips_block() -> None:
    calls = _capture_calls(None)
    for args in calls:
        assert ":house_re" not in str(args[0])
