"""Tests for GET /api/v1/trade-in/street-deals endpoint.

Covers:
  - extract_street_name parsing (various address formats)
  - empty result when no DB rows match
  - correct aggregation (median/count) with fixture deals
"""

import os
import sys
from unittest.mock import MagicMock

# psycopg v3 driver (psycopg2 not installed)
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")

# WeasyPrint requires GTK — not present in CI/Windows. Stub before any app import.
_wp_mock = MagicMock()
sys.modules.setdefault("weasyprint", _wp_mock)
sys.modules.setdefault("weasyprint.CSS", _wp_mock)
sys.modules.setdefault("weasyprint.HTML", _wp_mock)

from datetime import date  # noqa: E402

import pytest  # noqa: E402
from fastapi import FastAPI  # noqa: E402
from fastapi.testclient import TestClient  # noqa: E402

# ── Unit: extract_street_name ─────────────────────────────────────────────────


def test_extract_street_name_basic() -> None:
    """Various address formats should yield the bare street name."""
    from app.services.estimator import extract_street_name

    cases = [
        (
            "Свердловская обл., г. Екатеринбург, ул. Космонавтов, 12",
            "Космонавтов",
        ),
        (
            "г. Екатеринбург, проспект Ленина, 50",
            "Ленина",
        ),
        (
            "Екатеринбург, ул. 8 Марта, 18",
            "8 Марта",
        ),
        (
            "Россия, Екатеринбург, пер. Красный, 4",
            "Красный",
        ),
        (
            "Екатеринбург, ул. Малышева, 1",
            "Малышева",
        ),
    ]
    for addr, expected in cases:
        result = extract_street_name(addr)
        assert result == expected, f"addr={addr!r}: expected {expected!r}, got {result!r}"


def test_extract_street_name_none_and_empty() -> None:
    from app.services.estimator import extract_street_name

    assert extract_street_name(None) is None
    assert extract_street_name("") is None


def test_extract_street_name_no_street_keyword() -> None:
    """Address without a recognisable street keyword — must not crash."""
    from app.services.estimator import extract_street_name

    result = extract_street_name("Industrial zone X")
    assert result is None or isinstance(result, str)


@pytest.mark.parametrize(
    "address,expected",
    [
        # Forward format (user-typed)
        ("Екатеринбург, ул. Космонавтов, 50", "Космонавтов"),
        ("Россия, Екатеринбург, ул. Малышева, 1", "Малышева"),
        ("проспект Ленина 50", "Ленина"),
        ("ул. Большая Конюшенная, 25", "Большая Конюшенная"),
        # Reverse format (from Nominatim geocoder)
        (
            "80, улица 8 Марта, Артек, Форум-Сити, Ленинский район, Екатеринбург,"
            " городской округ Екатеринбург, Свердловская область,"
            " Уральский федеральный округ, 640144, Россия",
            "8 Марта",
        ),
        (
            "50, улица Космонавтов, Орджоникидзевский район, Екатеринбург, Россия",
            "Космонавтов",
        ),
        # Edge cases
        ("", None),
        (None, None),
        ("just plain text", None),
        ("Москва", None),
    ],
)
def test_extract_street_name_parametrized(address: str | None, expected: str | None) -> None:
    """Parametrized cases covering forward, reverse (Nominatim), and edge cases."""
    from app.services.estimator import extract_street_name

    assert extract_street_name(address) == expected


# ── Helpers ───────────────────────────────────────────────────────────────────


@pytest.fixture()
def trade_in_app() -> FastAPI:
    """Minimal FastAPI app mounting only the trade-in router with DB overridden."""
    from app.api.v1 import trade_in as trade_in_module
    from app.core.db import get_db

    application = FastAPI()
    application.include_router(trade_in_module.router, prefix="/api/v1/trade-in")

    def _override_db():
        yield MagicMock()

    application.dependency_overrides[get_db] = _override_db
    return application


def _make_db_mock(rows) -> MagicMock:
    """DB session mock returning *rows* from .mappings().all()."""
    db = MagicMock()
    mapping_result = MagicMock()
    mapping_result.all.return_value = rows
    execute_result = MagicMock()
    execute_result.mappings.return_value = mapping_result
    db.execute.return_value = execute_result
    return db


def _make_deal_row(
    *,
    address: str = "Екатеринбург, Космонавтов",
    area_m2: float = 50.0,
    rooms: int = 2,
    floor: int | None = 5,
    total_floors: int | None = 9,
    price_rub: int = 5_000_000,
    price_per_m2: int = 100_000,
    deal_date: date | None = None,
    source: str = "rosreestr",
) -> dict:
    return {
        "address": address,
        "area_m2": area_m2,
        "rooms": rooms,
        "floor": floor,
        "total_floors": total_floors,
        "price_rub": price_rub,
        "price_per_m2": price_per_m2,
        "deal_date": deal_date or date(2025, 6, 1),
        "source": source,
    }


# ── Test: empty result when DB returns no rows ────────────────────────────────


def test_street_deals_returns_empty_when_no_match(trade_in_app: FastAPI) -> None:
    """Endpoint returns count=0 / empty deals list when DB has no matching rows."""
    db_mock = _make_db_mock([])

    from app.core.db import get_db

    def _override():
        yield db_mock

    trade_in_app.dependency_overrides[get_db] = _override

    client = TestClient(trade_in_app)
    resp = client.get(
        "/api/v1/trade-in/street-deals",
        params={
            "address": "г. Екатеринбург, ул. Космонавтов, 50",
            "area_m2": 50.0,
            "rooms": 2,
        },
    )
    assert resp.status_code == 200
    data = resp.json()
    assert data["count"] == 0
    assert data["deals"] == []
    assert data["street"] == "Космонавтов"
    assert data["median_price_rub"] == 0


# ── Test: aggregation with fixture deals ─────────────────────────────────────


def test_street_deals_aggregates_correctly(trade_in_app: FastAPI) -> None:
    """With 5 fake deals the endpoint computes correct count + median."""
    # price_per_m2 values sorted: [80k, 90k, 100k, 110k, 120k] → median = 100k
    # median_price_rub = 100k * 50m² = 5_000_000
    fixture_rows = [
        _make_deal_row(price_rub=6_000_000, price_per_m2=120_000, deal_date=date(2025, 10, 1)),
        _make_deal_row(price_rub=5_500_000, price_per_m2=110_000, deal_date=date(2025, 9, 1)),
        _make_deal_row(price_rub=5_000_000, price_per_m2=100_000, deal_date=date(2025, 8, 1)),
        _make_deal_row(price_rub=4_500_000, price_per_m2=90_000, deal_date=date(2025, 7, 1)),
        _make_deal_row(price_rub=4_000_000, price_per_m2=80_000, deal_date=date(2025, 6, 1)),
    ]

    db_mock = _make_db_mock(fixture_rows)

    from app.core.db import get_db

    def _override():
        yield db_mock

    trade_in_app.dependency_overrides[get_db] = _override

    client = TestClient(trade_in_app)
    resp = client.get(
        "/api/v1/trade-in/street-deals",
        params={
            "address": "г. Екатеринбург, ул. Космонавтов, 50",
            "area_m2": 50.0,
            "rooms": 2,
            "period_months": 12,
        },
    )
    assert resp.status_code == 200
    data = resp.json()

    assert data["street"] == "Космонавтов"
    assert data["count"] == 5
    assert data["median_price_per_m2"] == 100_000
    assert data["median_price_rub"] == 5_000_000  # 100k * 50m²
    assert data["range_low_rub"] == 4_000_000
    assert data["range_high_rub"] == 6_000_000
    # All 5 deals returned (top-10 threshold)
    assert len(data["deals"]) == 5
    # DB rows already ordered by deal_date DESC — first is most recent (2025-10-01)
    assert data["deals"][0]["price_rub"] == 6_000_000


# ── Test: word-boundary street match (Part A1 fix) ───────────────────────────


def test_street_regex_word_boundary_no_false_positive() -> None:
    """«Мира» must NOT match «улица Адмирала Макарова» (contains 'мира' as substring).

    We verify the pattern construction directly using Python re (\\b is equivalent
    to Postgres \\m/\\M for word-boundary checks on these inputs).
    """
    import re

    street = "Мира"
    pattern = r"\b" + re.escape(street) + r"\b"

    # Must NOT match — 'мира' appears inside 'Макарова' substring check skipped
    # but more critically — 'мира' appears inside 'Адмирала Макарова'
    false_positive_addr = "улица Адмирала Макарова"
    assert not re.search(
        pattern, false_positive_addr, re.IGNORECASE
    ), f"Pattern {pattern!r} should NOT match {false_positive_addr!r}"

    # Must match — exact word
    true_positive_addr = "улица Мира 5"
    assert re.search(
        pattern, true_positive_addr, re.IGNORECASE
    ), f"Pattern {pattern!r} should match {true_positive_addr!r}"


def test_street_regex_param_passed_to_db(trade_in_app: FastAPI) -> None:
    """Endpoint passes street_regex with word-boundary markers to DB execute()."""
    db_mock = _make_db_mock([])

    from app.core.db import get_db

    def _override():
        yield db_mock

    trade_in_app.dependency_overrides[get_db] = _override

    client = TestClient(trade_in_app)
    client.get(
        "/api/v1/trade-in/street-deals",
        params={
            "address": "г. Екатеринбург, ул. Мира, 10",
            "area_m2": 50.0,
            "rooms": 2,
        },
    )

    assert db_mock.execute.called, "db.execute should have been called"
    call_args = db_mock.execute.call_args
    params = call_args[0][1] if len(call_args[0]) > 1 else call_args[1].get("parameters", {})
    assert "street_regex" in params, f"street_regex not in params: {params}"
    regex_val = params["street_regex"]
    # Must contain word-boundary anchors
    assert (
        r"\m" in regex_val or r"\b" in regex_val or regex_val.startswith(r"\m")
    ), f"Expected word-boundary in regex, got: {regex_val!r}"
