"""Unit-тесты zoom-tier в build_gps_mvt (ET-012, ADR-016).

Покрытие из 04-test-plan.yaml / 02-trz.md REQ-F-09:
  UT-Z5-01 — треки < 10 км отфильтровываются на z=5.
  UT-Z5-02 — limit=1500 на z=5.
  UT-Z6-01 — треки < 5 км отфильтровываются на z=6.
  UT-Z6-02 — limit=2000 на z=6.
  UT-Z7-01 — регрессия: min_length=2000, limit=3000 на z=7.
  UT-Z8-01 — регрессия: нет min_length, limit=8000 на z=8.
  UT-Z12-01 — регрессия: нет min_length, limit=25000 на z=12.

Все тесты используют mock-rows с фиксированной геометрией, помещённой
в тестовый тайл (см. ``_tile_for``). После build_gps_mvt MVT декодируется
mapbox_vector_tile.decode и подсчитывается число features в layer
``gps_tracks``.
"""
import json
import math

import mapbox_vector_tile
from shapely import wkb
from shapely.geometry import LineString

from src.api.gps_tracks.mvt import build_gps_mvt


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

def _tile_for(z: int, lon: float, lat: float) -> tuple[int, int]:
    """Возвращает (x, y) Web-Mercator-тайла для координат на зуме z.

    Использует ту же формулу, что и обратное преобразование в mvt._tile_to_bbox,
    но в прямую сторону: lon/lat → x/y.
    """
    n = 2 ** z
    x = int((lon + 180.0) / 360.0 * n)
    y_rad = math.radians(lat)
    y = int((1 - math.asinh(math.tan(y_rad)) / math.pi) / 2 * n)
    # На границе мира clamp
    return max(0, min(n - 1, x)), max(0, min(n - 1, y))


def _make_row(
    track_id: int,
    length_m: float,
    *,
    lon_center: float = 37.0,
    lat_center: float = 55.0,
    span: float = 0.005,
    activity_type: str = "enduro",
    source_id: str = "osm",
):
    """Создаёт mock sqlite3.Row-словарь с маленьким треком вокруг центра.

    span задан в градусах. По умолчанию ~500 м — линия безопасно лежит
    внутри тайлов z>=5 над выбранной точкой.
    """
    coords = [
        (lon_center - span, lat_center - span / 2),
        (lon_center, lat_center),
        (lon_center + span, lat_center + span / 2),
    ]
    geom = wkb.dumps(LineString(coords))

    class _Row(dict):
        def __getitem__(self, key):
            return super().__getitem__(key)

    return _Row({
        "id": track_id,
        "activity_type": activity_type,
        "sources_json": json.dumps([source_id]),
        "external_urls_json": json.dumps([]),
        "length_m": length_m,
        "name": f"Track {track_id}",
        "geom": geom,
    })


def _decode_features(mvt_bytes: bytes) -> list:
    """Декодирует MVT и возвращает список features в layer gps_tracks.

    Если тайл пуст (b"") — возвращает [].
    """
    if not mvt_bytes:
        return []
    decoded = mapbox_vector_tile.decode(mvt_bytes)
    layer = decoded.get("gps_tracks")
    if not layer:
        return []
    return layer.get("features", [])


# ─── UT-Z5-01: треки < 10 км отфильтровываются ──────────────────────────────

def test_ut_z5_01_short_tracks_filtered():
    """REQ-F-09 / UT-Z5-01: на z=5 проходят только треки длиной >= 10 км.

    Из 10 треков [500..120000] должны попасть в MVT ровно 6
    (длины 12000, 15000, 25000, 50000, 80000, 120000).
    """
    lengths = [500, 2000, 3000, 8000, 12000, 15000, 25000, 50000, 80000, 120000]
    lon, lat = 37.0, 55.0
    x, y = _tile_for(5, lon, lat)

    rows = [
        _make_row(i, length_m=length, lon_center=lon, lat_center=lat)
        for i, length in enumerate(lengths, start=1)
    ]

    mvt = build_gps_mvt(rows, z=5, x=x, y=y)
    features = _decode_features(mvt)

    # Ожидаем 6 features — треки длиной >= 10000 м.
    assert len(features) == 6
    # Все попавшие — с length_km >= 10.0
    for feat in features:
        assert feat["properties"]["length_km"] >= 10.0


# ─── UT-Z5-02: limit=1500 на z=5 ────────────────────────────────────────────

def test_ut_z5_02_limit_1500():
    """REQ-F-09 / UT-Z5-02: на z=5 cap=1500 при большом числе длинных треков.

    Все 2000 треков проходят min_length=10000 (15 км), но в MVT уходит
    только первые 1500 (build_gps_mvt брейкает цикл по len(features) >= limit).
    """
    lon, lat = 37.0, 55.0
    x, y = _tile_for(5, lon, lat)

    rows = [
        _make_row(i, length_m=15000, lon_center=lon, lat_center=lat)
        for i in range(1, 2001)
    ]

    mvt = build_gps_mvt(rows, z=5, x=x, y=y)
    features = _decode_features(mvt)

    assert len(features) == 1500


# ─── UT-Z6-01: треки < 5 км отфильтровываются ───────────────────────────────

def test_ut_z6_01_short_tracks_filtered():
    """REQ-F-09 / UT-Z6-01: на z=6 проходят только треки длиной >= 5 км.

    Из [1000, 3000, 5000, 7000, 10000] должны попасть 3 (5000, 7000, 10000).
    """
    lengths = [1000, 3000, 5000, 7000, 10000]
    lon, lat = 37.0, 55.0
    x, y = _tile_for(6, lon, lat)

    rows = [
        _make_row(i, length_m=length, lon_center=lon, lat_center=lat)
        for i, length in enumerate(lengths, start=1)
    ]

    mvt = build_gps_mvt(rows, z=6, x=x, y=y)
    features = _decode_features(mvt)

    assert len(features) == 3
    for feat in features:
        assert feat["properties"]["length_km"] >= 5.0


# ─── UT-Z6-02: limit=2000 на z=6 ────────────────────────────────────────────

def test_ut_z6_02_limit_2000():
    """REQ-F-09 / UT-Z6-02: на z=6 cap=2000 при 2500 треках >= 5 км."""
    lon, lat = 37.0, 55.0
    x, y = _tile_for(6, lon, lat)

    rows = [
        _make_row(i, length_m=6000, lon_center=lon, lat_center=lat)
        for i in range(1, 2501)
    ]

    mvt = build_gps_mvt(rows, z=6, x=x, y=y)
    features = _decode_features(mvt)

    assert len(features) == 2000


# ─── UT-Z7-01: регрессия — min_length=2000, limit=3000 ──────────────────────

def test_ut_z7_01_regression():
    """REQ-F-09 / UT-Z7-01: поведение z=7 не изменилось (min_length=2000)."""
    lengths = [1000, 2000, 3000, 5000]
    lon, lat = 37.0, 55.0
    x, y = _tile_for(7, lon, lat)

    rows = [
        _make_row(i, length_m=length, lon_center=lon, lat_center=lat)
        for i, length in enumerate(lengths, start=1)
    ]

    mvt = build_gps_mvt(rows, z=7, x=x, y=y)
    features = _decode_features(mvt)

    # 1000 < 2000 → отфильтрован; 2000, 3000, 5000 — попадают.
    assert len(features) == 3
    for feat in features:
        assert feat["properties"]["length_km"] >= 2.0


def test_ut_z7_01_limit_3000():
    """REQ-F-09 / UT-Z7-01 (доп.): cap=3000 на z=7."""
    lon, lat = 37.0, 55.0
    x, y = _tile_for(7, lon, lat)

    rows = [
        _make_row(i, length_m=4000, lon_center=lon, lat_center=lat)
        for i in range(1, 3500)
    ]

    mvt = build_gps_mvt(rows, z=7, x=x, y=y)
    features = _decode_features(mvt)

    assert len(features) == 3000


# ─── UT-Z8-01: регрессия — нет min_length, limit=8000 ───────────────────────

def test_ut_z8_01_regression_no_min_length():
    """REQ-F-09 / UT-Z8-01: на z=8 любые треки проходят."""
    lengths = [500, 1000, 2000, 5000]
    lon, lat = 37.0, 55.0
    x, y = _tile_for(8, lon, lat)

    rows = [
        _make_row(i, length_m=length, lon_center=lon, lat_center=lat)
        for i, length in enumerate(lengths, start=1)
    ]

    mvt = build_gps_mvt(rows, z=8, x=x, y=y)
    features = _decode_features(mvt)

    assert len(features) == 4


# ─── UT-Z12-01: регрессия — limit=25000, без min_length ─────────────────────

def test_ut_z12_01_regression_no_filtering():
    """REQ-F-09 / UT-Z12-01: на z=12 любая длина проходит, малое число фич."""
    lon, lat = 37.0, 55.0
    x, y = _tile_for(12, lon, lat)

    rows = [
        _make_row(i, length_m=100 * i, lon_center=lon, lat_center=lat)
        for i in range(1, 101)
    ]

    mvt = build_gps_mvt(rows, z=12, x=x, y=y)
    features = _decode_features(mvt)

    assert len(features) == 100
