"""Tests for POI weighted score service (B6).

Юнит-тесты для чистой функции — без DB.
"""

from app.services.site_finder.poi_score import (
    CATEGORY_WEIGHTS,
    PoiScoreResponse,
    _category_weight,
    compute_poi_weighted_top7,
)

# ── unit: _category_weight ─────────────────────────────────────────────────────


def test_category_weight_metro():
    """Метро имеет наибольший вес из всех категорий."""
    metro_w = _category_weight("metro_stop")
    for cat in CATEGORY_WEIGHTS:
        if cat != "metro_stop" and cat != "default":
            assert metro_w >= _category_weight(
                cat
            ), f"metro_stop weight {metro_w} должен быть >= {cat} weight {_category_weight(cat)}"


def test_category_weight_unknown_returns_default():
    w = _category_weight("unknown_category_xyz")
    assert w == CATEGORY_WEIGHTS["default"]


def test_category_weight_all_positive():
    """Все веса в CATEGORY_WEIGHTS должны быть положительными (B6 — ranking, не штраф)."""
    for cat, w in CATEGORY_WEIGHTS.items():
        assert w > 0, f"Вес {cat}={w} должен быть > 0"


# ── unit: weight formula ratio ─────────────────────────────────────────────────


def test_weight_formula_ratio():
    """Ближний объект той же категории должен иметь больший вес."""
    cat = "school"
    cw = _category_weight(cat)
    w_near = (1.0 / (100.0 + 100.0)) * cw  # 100м
    w_far = (1.0 / (1000.0 + 100.0)) * cw  # 1000м
    assert w_near > w_far


def test_weight_formula_category_dominates_at_equal_distance():
    """При одинаковом расстоянии метро должно быть впереди автобусной остановки."""
    dist = 500.0
    w_metro = (1.0 / (dist + 100.0)) * _category_weight("metro_stop")
    w_bus = (1.0 / (dist + 100.0)) * _category_weight("bus_stop")
    assert w_metro > w_bus


# ── unit: compute_poi_weighted_top7 with mock DB ───────────────────────────────


class _MockMappings:
    def __init__(self, data: list[dict]) -> None:
        self._data = data

    def all(self) -> list[dict]:
        return self._data  # type: ignore[return-value]


class _MockResult:
    def __init__(self, data: list[dict]) -> None:
        self._data = data

    def mappings(self) -> "_MockMappings":
        return _MockMappings(self._data)


class _MockDb:
    """Минимальный мок SQLAlchemy Session для тестирования без БД."""

    def __init__(self, rows: list[dict]) -> None:
        self._rows = rows

    def execute(self, *_args: object, **_kwargs: object) -> _MockResult:
        return _MockResult(self._rows)


def _make_row(name: str, category: str, distance_m: float) -> dict:
    return {
        "name": name,
        "category": category,
        "tags": {},
        "distance_m": distance_m,
    }


def test_top7_returns_at_most_7():
    rows = [_make_row(f"POI {i}", "school", float(i * 50)) for i in range(1, 20)]
    db = _MockDb(rows)
    result = compute_poi_weighted_top7(db, "66:41:0204016:10", 56.838, 60.605)
    assert isinstance(result, PoiScoreResponse)
    assert len(result.top_poi) <= 7


def test_top7_sorted_by_weight_desc():
    rows = [
        _make_row("Дальняя школа", "school", 1500.0),
        _make_row("Метро", "metro_stop", 300.0),
        _make_row("Близкая школа", "school", 100.0),
    ]
    db = _MockDb(rows)
    result = compute_poi_weighted_top7(db, "66:41:0204016:10", 56.838, 60.605)
    weights = [item.weight for item in result.top_poi]
    assert weights == sorted(weights, reverse=True), "top_poi должны быть по weight DESC"


def test_metro_beats_school_at_equal_distance():
    """Метро в 300м должно быть на первом месте перед школой в 300м (равное расстояние)."""
    rows = [
        _make_row("Школа №1", "school", 300.0),
        _make_row("Метро Чкаловская", "metro_stop", 300.0),
    ]
    db = _MockDb(rows)
    result = compute_poi_weighted_top7(db, "66:41:0204016:10", 56.838, 60.605)
    assert (
        result.top_poi[0].category == "metro_stop"
    ), "При равном расстоянии метро (category_weight=6.0) должно быть выше школы (5.0)"


def test_metro_first_when_close():
    """Метро в 50м должно быть на первом месте перед школой в 300м."""
    rows = [
        _make_row("Школа №1", "school", 300.0),
        _make_row("Метро Чкаловская", "metro_stop", 50.0),
    ]
    db = _MockDb(rows)
    result = compute_poi_weighted_top7(db, "66:41:0204016:10", 56.838, 60.605)
    assert result.top_poi[0].category == "metro_stop", (
        "Метро (weight=6.0) в 50м должно быть впереди школы (weight=5.0) в 300м — "
        f"metro_weight={(1/(50+100))*6:.5f} vs school_weight={(1/(300+100))*5:.5f}"
    )


def test_empty_db_returns_empty_top_poi():
    db = _MockDb([])
    result = compute_poi_weighted_top7(db, "66:41:0204016:10", 56.838, 60.605)
    assert result.top_poi == []
    assert result.cad_num == "66:41:0204016:10"
    assert result.radius_m == 2000


def test_address_built_from_tags():
    rows = [
        {
            "name": "Магазин",
            "category": "shop_small",
            "tags": {"addr:street": "ул. Ленина", "addr:housenumber": "10"},
            "distance_m": 200.0,
        }
    ]
    db = _MockDb(rows)
    result = compute_poi_weighted_top7(db, "test", 56.838, 60.605)
    assert result.top_poi[0].address == "ул. Ленина, 10"


def test_address_none_when_no_tags():
    rows = [_make_row("Парк", "park", 400.0)]
    db = _MockDb(rows)
    result = compute_poi_weighted_top7(db, "test", 56.838, 60.605)
    assert result.top_poi[0].address is None
