"""Тесты для POST /api/v1/parcels/{cad_num}/competitors (Issue #112).

Mock-based — не требуют живой БД.
Паттерн mock DB: аналогично test_admin_cadastre.py — dependency_overrides[get_db].
"""

from __future__ import annotations

from unittest.mock import MagicMock

import pytest
from fastapi.testclient import TestClient

from app.main import app

# ── Фабрики mock-строк ────────────────────────────────────────────────────────


def _coord_row(lat: float = 56.838, lon: float = 60.605) -> MagicMock:
    """Строка центроида участка."""
    r = MagicMock()
    r.__getitem__ = lambda self, k: {"lat": lat, "lon": lon}[k]
    return r


def _obj_row(
    obj_id: int = 1,
    distance_m: float = 400.0,
    site_status: str = "sales",
    obj_class: str | None = "comfort",
    velocity: float = 5.0,
    flat_count: int | None = 200,
) -> MagicMock:
    r = MagicMock()
    r.__getitem__ = lambda self, k: {
        "obj_id": obj_id,
        "comm_name": f"ЖК-{obj_id}",
        "dev_name": "TestDev",
        "obj_class": obj_class,
        "latitude": 56.838 + distance_m / 1_000_000,
        "longitude": 60.605 + distance_m / 1_000_000,
        "flat_count": flat_count,
        "site_status": site_status,
        "distance_m": distance_m,
        "velocity_per_month": velocity,
    }[k]
    return r


def _price_row(obj_id: int, price: float) -> MagicMock:
    r = MagicMock()
    r.__getitem__ = lambda self, k: {"obj_id": obj_id, "avg_price_per_m2": price}[k]
    return r


# ── Построение mock DB ────────────────────────────────────────────────────────


def _make_db(
    coord: MagicMock | None = None,
    obj_rows: list[MagicMock] | None = None,
    price_rows: list[MagicMock] | None = None,
) -> MagicMock:
    """Сконструировать mock Session.

    Порядок вызовов execute:
    1. centroid query → coord
    2. competitors query → obj_rows
    3. avg_price query → price_rows
    """
    db = MagicMock()

    results: list[MagicMock] = []
    for rows, is_first in [
        (coord, True),
        (obj_rows or [], False),
        (price_rows or [], False),
    ]:
        result = MagicMock()
        if is_first:
            # centroid → .mappings().first()
            result.mappings.return_value.first.return_value = rows
        else:
            result.mappings.return_value.all.return_value = rows
        results.append(result)

    db.execute.side_effect = results
    return db


def _override_db(db: MagicMock):
    def _get_db_override():
        yield db

    return _get_db_override


# ── Тесты ─────────────────────────────────────────────────────────────────────


def test_competitors_basic() -> None:
    """3 конкурента → корректная форма ответа, сортировка по distance."""
    rows = [
        _obj_row(obj_id=1, distance_m=200.0, velocity=4.0),
        _obj_row(obj_id=2, distance_m=500.0, velocity=6.0),
        _obj_row(obj_id=3, distance_m=900.0, velocity=2.0),
    ]
    db = _make_db(coord=_coord_row(), obj_rows=rows)

    from app.core.db import get_db

    app.dependency_overrides[get_db] = _override_db(db)
    try:
        client = TestClient(app)
        resp = client.post(
            "/api/v1/parcels/66:41:0303161:123/competitors",
            json={"radius_km": 1.0, "time_window": "last_quarter"},
        )
        assert resp.status_code == 200, resp.text
        body = resp.json()
        assert "competitors" in body
        assert "summary" in body
        assert len(body["competitors"]) == 3
        # первый должен быть ближайшим
        assert body["competitors"][0]["obj_id"] == 1
        assert body["competitors"][0]["distance_m"] == pytest.approx(200.0)
        # все поля присутствуют
        first = body["competitors"][0]
        for key in (
            "obj_id",
            "comm_name",
            "dev_name",
            "obj_class",
            "distance_m",
            "lat",
            "lng",
            "stage",
            "flats_total",
            "flats_sold",
            "sold_pct",
            "velocity_per_month",
            "avg_price_per_m2",
            "is_active",
        ):
            assert key in first, f"missing key: {key}"
    finally:
        app.dependency_overrides.clear()


def test_competitors_summary_calc() -> None:
    """summary: total_competitors, active_count, weighted_avg_velocity корректны."""
    rows = [
        _obj_row(obj_id=1, site_status="sales", velocity=10.0),
        _obj_row(obj_id=2, site_status="construction", velocity=6.0),
        _obj_row(obj_id=3, site_status="completed", velocity=2.0),
    ]
    db = _make_db(coord=_coord_row(), obj_rows=rows)

    from app.core.db import get_db

    app.dependency_overrides[get_db] = _override_db(db)
    try:
        client = TestClient(app)
        resp = client.post(
            "/api/v1/parcels/66:41:0303161:5/competitors",
            json={"radius_km": 1.0, "time_window": "last_quarter"},
        )
        assert resp.status_code == 200, resp.text
        summary = resp.json()["summary"]
        assert summary["total_competitors"] == 3
        assert summary["active_count"] == 2  # sales + construction
        # avg velocity = (10+6+2)/3 = 6.0
        assert summary["weighted_avg_velocity"] == pytest.approx(6.0)
        assert summary["radius_km"] == pytest.approx(1.0)
        assert summary["time_window"] == "last_quarter"
    finally:
        app.dependency_overrides.clear()


def test_competitors_exclude_obj_ids() -> None:
    """exclude_obj_ids исключает указанные ЖК из результата."""
    rows = [
        _obj_row(obj_id=1, distance_m=100.0),
        _obj_row(obj_id=2, distance_m=200.0),
        _obj_row(obj_id=3, distance_m=300.0),
    ]
    db = _make_db(coord=_coord_row(), obj_rows=rows)

    from app.core.db import get_db

    app.dependency_overrides[get_db] = _override_db(db)
    try:
        client = TestClient(app)
        resp = client.post(
            "/api/v1/parcels/66:41:0303161:5/competitors",
            json={"radius_km": 1.0, "time_window": "last_quarter", "exclude_obj_ids": [2]},
        )
        assert resp.status_code == 200, resp.text
        ids = [c["obj_id"] for c in resp.json()["competitors"]]
        assert 2 not in ids
        assert 1 in ids
        assert 3 in ids
    finally:
        app.dependency_overrides.clear()


def test_competitors_obj_class_filter() -> None:
    """obj_class_filter=economy — SQL получает параметр; Python-сторона не ломается."""
    rows = [_obj_row(obj_id=10, obj_class="economy")]
    db = _make_db(coord=_coord_row(), obj_rows=rows)

    from app.core.db import get_db

    app.dependency_overrides[get_db] = _override_db(db)
    try:
        client = TestClient(app)
        resp = client.post(
            "/api/v1/parcels/66:41:0303161:5/competitors",
            json={"radius_km": 1.0, "time_window": "last_quarter", "obj_class_filter": "economy"},
        )
        assert resp.status_code == 200, resp.text
        comps = resp.json()["competitors"]
        assert len(comps) == 1
        assert comps[0]["obj_class"] == "economy"
    finally:
        app.dependency_overrides.clear()


def test_competitors_time_window_velocity() -> None:
    """time_window влияет на velocity_per_month (last_month vs last_year)."""
    # Здесь проверяем, что endpoint принимает оба варианта без ошибок
    # и возвращает velocity из mock-строки (DB-расчёт мокирован).
    rows_month = [_obj_row(obj_id=1, velocity=12.0)]
    rows_year = [_obj_row(obj_id=1, velocity=3.0)]

    from app.core.db import get_db

    # last_month
    db_m = _make_db(coord=_coord_row(), obj_rows=rows_month)
    app.dependency_overrides[get_db] = _override_db(db_m)
    try:
        client = TestClient(app)
        resp_m = client.post(
            "/api/v1/parcels/66:41:0303161:5/competitors",
            json={"radius_km": 1.0, "time_window": "last_month"},
        )
        assert resp_m.status_code == 200, resp_m.text
    finally:
        app.dependency_overrides.clear()

    # last_year
    db_y = _make_db(coord=_coord_row(), obj_rows=rows_year)
    app.dependency_overrides[get_db] = _override_db(db_y)
    try:
        client = TestClient(app)
        resp_y = client.post(
            "/api/v1/parcels/66:41:0303161:5/competitors",
            json={"radius_km": 1.0, "time_window": "last_year"},
        )
        assert resp_y.status_code == 200, resp_y.text
        v_month = resp_m.json()["competitors"][0]["velocity_per_month"]
        v_year = resp_y.json()["competitors"][0]["velocity_per_month"]
        # month velocity выше чем year в нашем моке
        assert v_month > v_year
    finally:
        app.dependency_overrides.clear()


def test_competitors_parcel_not_found_404() -> None:
    """Если центроид участка не найден → 404."""
    db = _make_db(coord=None)  # first() вернёт None

    from app.core.db import get_db

    app.dependency_overrides[get_db] = _override_db(db)
    try:
        client = TestClient(app)
        resp = client.post(
            "/api/v1/parcels/99:99:9999999:999/competitors",
            json={"radius_km": 1.0, "time_window": "last_quarter"},
        )
        assert resp.status_code == 404, resp.text
    finally:
        app.dependency_overrides.clear()


def test_competitors_empty_radius() -> None:
    """Нет конкурентов в радиусе → пустой список + summary с нулями."""
    db = _make_db(coord=_coord_row(), obj_rows=[])

    from app.core.db import get_db

    app.dependency_overrides[get_db] = _override_db(db)
    try:
        client = TestClient(app)
        resp = client.post(
            "/api/v1/parcels/66:41:0303161:5/competitors",
            json={"radius_km": 0.1, "time_window": "last_quarter"},
        )
        assert resp.status_code == 200, resp.text
        body = resp.json()
        assert body["competitors"] == []
        assert body["summary"]["total_competitors"] == 0
        assert body["summary"]["active_count"] == 0
        assert body["summary"]["weighted_avg_velocity"] == pytest.approx(0.0)
    finally:
        app.dependency_overrides.clear()


def test_competitors_sold_pct_null() -> None:
    """sold_pct и flats_sold — None (MVP: данные недоступны из domrf_kn_objects).

    Полный расчёт продаж требует JOIN с domrf_kn_flats COUNT WHERE status='sold'
    — отложен за пределы текущего PR.
    """
    rows = [_obj_row(obj_id=1, flat_count=200)]
    db = _make_db(coord=_coord_row(), obj_rows=rows)

    from app.core.db import get_db

    app.dependency_overrides[get_db] = _override_db(db)
    try:
        client = TestClient(app)
        resp = client.post(
            "/api/v1/parcels/66:41:0303161:5/competitors",
            json={"radius_km": 1.0, "time_window": "last_quarter"},
        )
        assert resp.status_code == 200, resp.text
        comp = resp.json()["competitors"][0]
        assert comp["flats_sold"] is None
        assert comp["sold_pct"] is None
        assert comp["flats_total"] == 200
    finally:
        app.dependency_overrides.clear()


def test_competitors_avg_price_populated() -> None:
    """avg_price_per_m2 не None если domrf_kn_flats возвращает строки с ценой.

    Регрессионный тест для Issue #227: фильтр status='sold' давал 0 строк
    (поле status в domrf_kn_flats 99.8% NULL). После фикса — убран, AVG
    считается по всем квартирам с known price_per_m2.
    """
    rows = [_obj_row(obj_id=1)]
    price_rows = [_price_row(obj_id=1, price=150_000.0)]
    db = _make_db(coord=_coord_row(), obj_rows=rows, price_rows=price_rows)

    from app.core.db import get_db

    app.dependency_overrides[get_db] = _override_db(db)
    try:
        client = TestClient(app)
        resp = client.post(
            "/api/v1/parcels/66:41:0303161:5/competitors",
            json={"radius_km": 1.0, "time_window": "last_quarter"},
        )
        assert resp.status_code == 200, resp.text
        comp = resp.json()["competitors"][0]
        assert comp["avg_price_per_m2"] == pytest.approx(
            150_000.0
        ), "avg_price_per_m2 должен быть не None — регрессия #227 status='sold' filter"
    finally:
        app.dependency_overrides.clear()


def test_competitors_is_active_flag() -> None:
    """is_active=True для sales/construction, False для completed/null."""
    rows = [
        _obj_row(obj_id=1, site_status="sales"),
        _obj_row(obj_id=2, site_status="construction"),
        _obj_row(obj_id=3, site_status="completed"),
    ]
    db = _make_db(coord=_coord_row(), obj_rows=rows)

    from app.core.db import get_db

    app.dependency_overrides[get_db] = _override_db(db)
    try:
        client = TestClient(app)
        resp = client.post(
            "/api/v1/parcels/66:41:0303161:5/competitors",
            json={"radius_km": 1.0, "time_window": "last_quarter"},
        )
        assert resp.status_code == 200, resp.text
        comps = {c["obj_id"]: c for c in resp.json()["competitors"]}
        assert comps[1]["is_active"] is True
        assert comps[2]["is_active"] is True
        assert comps[3]["is_active"] is False
    finally:
        app.dependency_overrides.clear()
