"""Unit тесты для MVT тайлов GPS-треков (ET-008).

U-50: тайл z=10 с треками
U-51: упрощение на z=7
U-52: min-length фильтр
"""
import json
import pytest

from shapely.geometry import LineString
from shapely import wkb

from src.api.gps_tracks.mvt import build_gps_mvt, _simplify_coords, _wkb_to_coords


def _make_mock_row(
    track_id=1,
    activity_type="enduro",
    source_id="osm",
    length_m=8000.0,
    name="Test Track",
    coords=None,
    min_lon=37.60,
    min_lat=55.74,
    max_lon=37.65,
    max_lat=55.78,
):
    """Создаёт mock sqlite3.Row как словарь."""
    if coords is None:
        coords = [
            (min_lon, min_lat),
            ((min_lon + max_lon) / 2, (min_lat + max_lat) / 2),
            (max_lon, max_lat),
        ]

    geom_wkb = wkb.dumps(LineString(coords))

    # Имитируем sqlite3.Row через dict с поддержкой подписки
    class MockRow(dict):
        def __getitem__(self, key):
            return super().__getitem__(key)

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


# ─── U-50: тайл z=10 с треками ───────────────────────────────────────────────

def test_u50_tile_z10_with_tracks():
    """U-50: build_gps_mvt возвращает непустой тайл при наличии треков."""
    rows = [
        _make_mock_row(1, "enduro", "osm", length_m=8000),
        _make_mock_row(2, "moto", "osm", length_m=5000,
                       min_lon=37.61, min_lat=55.75, max_lon=37.62, max_lat=55.76),
    ]

    # Тайл z=10, x=620, y=320 — область Москвы
    result = build_gps_mvt(rows, z=10, x=620, y=320)

    assert isinstance(result, bytes)
    assert len(result) > 0


def test_u50_empty_rows_returns_empty_bytes():
    """U-50: пустой список строк возвращает b""."""
    result = build_gps_mvt([], z=10, x=620, y=320)
    assert result == b""


def test_u50_invalid_geom_row_skipped():
    """U-50: строка с невалидной геометрией пропускается."""
    class BadRow(dict):
        pass

    bad_row = BadRow({
        "id": 99,
        "activity_type": "other",
        "sources_json": '["osm"]',
        "external_urls_json": "[]",
        "length_m": 5000,
        "name": "bad",
        "geom": b"\x00\x01\x02",  # невалидный WKB
    })

    good_row = _make_mock_row(1, length_m=5000)

    result = build_gps_mvt([bad_row, good_row], z=10, x=620, y=320)
    # Не падает, плохая строка пропускается
    assert isinstance(result, bytes)


# ─── U-51: упрощение на z=7 ──────────────────────────────────────────────────

def test_u51_simplification_z7_reduces_points():
    """U-51: геометрия упрощается на малых зумах."""
    # Создаём трек из 20 точек
    coords = [(37.60 + i * 0.001, 55.74 + i * 0.0005) for i in range(20)]

    simplified = _simplify_coords(coords, z=7)

    # При z=7 tolerance=0.008, ожидаем меньше точек
    assert len(simplified) < len(coords)
    assert len(simplified) >= 2


def test_u51_no_simplification_z12():
    """U-51: на z=12 упрощение не применяется."""
    coords = [(37.60 + i * 0.001, 55.74 + i * 0.0005) for i in range(10)]
    result = _simplify_coords(coords, z=12)
    assert result == coords


def test_u51_simplification_z10_moderate():
    """U-51: на z=10 умеренное упрощение."""
    coords = [(37.60 + i * 0.0001, 55.74 + i * 0.0001) for i in range(30)]

    simplified_z10 = _simplify_coords(coords, z=10)
    simplified_z7 = _simplify_coords(coords, z=7)

    # z=7 должен сильнее упрощать, чем z=10
    assert len(simplified_z10) >= len(simplified_z7)


# ─── U-52: min-length фильтр ─────────────────────────────────────────────────

def test_u52_min_length_filter_z7():
    """U-52: на z<=7 треки короче 2000м отфильтровываются."""
    short_track = _make_mock_row(1, length_m=1500)  # меньше 2000м
    long_track = _make_mock_row(2, length_m=5000)   # больше 2000м

    result_with_short = build_gps_mvt([short_track, long_track], z=7, x=77, y=40)
    result_without_short = build_gps_mvt([long_track], z=7, x=77, y=40)

    # Результаты должны совпадать (короткий трек отфильтрован)
    assert result_with_short == result_without_short


def test_u52_no_min_length_filter_z10():
    """U-52: на z=10 нет min-length фильтра — все треки проходят."""
    short_track = _make_mock_row(1, length_m=100)
    long_track = _make_mock_row(2, length_m=5000)

    result_both = build_gps_mvt([short_track, long_track], z=10, x=620, y=320)
    result_long_only = build_gps_mvt([long_track], z=10, x=620, y=320)

    # При z=10 оба трека должны включаться (если геометрия пересекается с тайлом)
    # result_both может быть больше result_long_only если короткий трек в тайле
    assert isinstance(result_both, bytes)
    assert isinstance(result_long_only, bytes)


def test_u52_min_length_boundary():
    """U-52: трек ровно 2000м на z=7 проходит фильтр."""
    track_2000 = _make_mock_row(1, length_m=2000)
    track_1999 = _make_mock_row(2, length_m=1999)

    result_2000 = build_gps_mvt([track_2000], z=7, x=77, y=40)
    result_1999 = build_gps_mvt([track_1999], z=7, x=77, y=40)

    # track_1999 должен быть отфильтрован (строго меньше 2000)
    # track_2000 проходит (>= 2000 не выполняется для строгого фильтра < 2000)
    # По коду: if min_length_m > 0 and length_m < min_length_m → skip
    # 1999 < 2000 → skip, 2000 < 2000 → False → not skipped
    assert result_2000 != result_1999 or result_1999 == b""
