"""Performance-тест PERF-Z5-01 (ET-012, ADR-016, REQ-F-13).

Запускается отдельным джобом::

    pytest -m perf tests/performance/test_gps_mvt_z5_perf.py -v

В обычный CI-gate не входит (см. pyproject.toml addopts: '-m "not perf"').

Цель: проверить, что после понижения GPS_TRACKS_MIN_ZOOM до 5
``build_gps_mvt(rows, 5, x, y)`` укладывается в бюджеты по NFR-01 / M-6:

* avg ≤ 200 мс на 500 треках,
* p95 ≤ 500 мс на 500 треках.

Замечание о масштабе: CI-runner у gitea-actions ≈ 2 vCPU; на dev-машине
показатели обычно лучше. Если на CI-runner avg/p95 не сходятся —
ужесточить ``limit`` в ``build_gps_mvt`` (TRZ §3 REQ-F-03, ADR-016 §T).
"""
import json
import math
import time

import pytest
from shapely import wkb
from shapely.geometry import LineString

from src.api.gps_tracks.mvt import build_gps_mvt

pytestmark = pytest.mark.perf


def _make_row(track_id: int, length_m: float, lon_center: float, lat_center: float):
    """Создаёт mock-row с 30-точечной геометрией ~10 км."""
    n_points = 30
    coords = []
    for i in range(n_points):
        # Линия наискосок, ~0.1° по диагонали ≈ 8-11 км.
        t = i / (n_points - 1)
        coords.append(
            (lon_center - 0.05 + t * 0.1, lat_center - 0.05 + t * 0.1)
        )

    geom = wkb.dumps(LineString(coords))

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

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


def _tile_for(z: int, lon: float, lat: float) -> tuple[int, int]:
    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)
    return max(0, min(n - 1, x)), max(0, min(n - 1, y))


def _percentile(values: list[float], pct: float) -> float:
    """Простая реализация перцентиля (linear interpolation)."""
    if not values:
        return 0.0
    sorted_vals = sorted(values)
    k = (len(sorted_vals) - 1) * pct
    f = int(k)
    c = min(f + 1, len(sorted_vals) - 1)
    if f == c:
        return sorted_vals[f]
    return sorted_vals[f] + (sorted_vals[c] - sorted_vals[f]) * (k - f)


# ─── PERF-Z5-01: build_gps_mvt z=5 на 500 треках ────────────────────────────

def test_perf_z5_01_500_tracks_under_budget():
    """REQ-F-13 / PERF-Z5-01: avg <= 200 мс, p95 <= 500 мс на 500 треках."""
    lon, lat = 37.6, 55.7
    x, y = _tile_for(5, lon, lat)

    # 500 треков длиной 12-25 км по ЦФО (все проходят min_length=10 км).
    rows = []
    for i in range(500):
        length_m = 12000 + (i * 137 % 13000)  # 12000..25000
        # Лёгкое смещение центра, чтобы DB-row были не идентичными.
        dlon = ((i * 13) % 100 - 50) / 1000.0  # ±0.05°
        dlat = ((i * 23) % 100 - 50) / 1000.0
        rows.append(_make_row(i, length_m, lon + dlon, lat + dlat))

    # Прогрев — один холодный прогон, не учитываем в статистике.
    build_gps_mvt(rows, z=5, x=x, y=y)

    timings = []
    for _ in range(10):
        t0 = time.perf_counter()
        build_gps_mvt(rows, z=5, x=x, y=y)
        timings.append((time.perf_counter() - t0) * 1000.0)  # мс

    avg = sum(timings) / len(timings)
    p95 = _percentile(timings, 0.95)

    # Прикрепляем замеры в отчёт (видно при pytest -v -s).
    print(
        f"\nPERF-Z5-01: avg={avg:.1f}ms, p95={p95:.1f}ms, "
        f"min={min(timings):.1f}ms, max={max(timings):.1f}ms"
    )

    assert avg <= 200, f"avg {avg:.1f}ms > 200ms (M-6 нарушена)"
    assert p95 <= 500, f"p95 {p95:.1f}ms > 500ms (M-6 нарушена)"


# ─── PERF-Z5-02: 5000 треков (стресс) ───────────────────────────────────────

def test_perf_z5_02_5000_tracks_stress():
    """REQ-F-13 / PERF-Z5-02: p95 <= 1500 мс при БД 5000 треков (стресс).

    Симулирует прогноз роста БД до 5k треков. Если не проходит — нужно
    рассматривать смену стратегии (pre-rendering / heat-map; см. ADR-016 §P).
    """
    lon, lat = 37.6, 55.7
    x, y = _tile_for(5, lon, lat)

    rows = []
    for i in range(5000):
        length_m = 1000 + (i * 137 % 50000)  # 1..50 км, разные длины
        dlon = ((i * 13) % 100 - 50) / 100.0  # ±0.5°
        dlat = ((i * 23) % 100 - 50) / 100.0
        rows.append(_make_row(i, length_m, lon + dlon, lat + dlat))

    # Прогрев
    build_gps_mvt(rows, z=5, x=x, y=y)

    timings = []
    for _ in range(5):
        t0 = time.perf_counter()
        build_gps_mvt(rows, z=5, x=x, y=y)
        timings.append((time.perf_counter() - t0) * 1000.0)

    p95 = _percentile(timings, 0.95)
    print(
        f"\nPERF-Z5-02: p95={p95:.1f}ms, "
        f"min={min(timings):.1f}ms, max={max(timings):.1f}ms"
    )

    assert p95 <= 1500, f"p95 {p95:.1f}ms > 1500ms"
