"""Tests para build_geo_scatter."""

import math
import os
import sys

sys.path.insert(0, os.path.dirname(__file__))

from build_geo_scatter import build_geo_scatter

# Keys that a non-empty result dict must always contain.
_EXPECTED_KEYS = {
    "points", "n_total", "n_shown", "downsampled", "bbox", "aspect", "pad",
}


def test_geo_scatter_nube_espana():
    """Golden: nube en Espana -> points en orden [lon, lat], bbox, aspect>1, pad 5%."""
    # Cuatro puntos alrededor de Madrid (lat ~40, lon negativo).
    lats = [40.0, 41.0, 39.0, 40.5]
    lons = [-3.7, -3.0, -4.0, -3.5]
    r = build_geo_scatter(lats, lons)

    assert set(r.keys()) == _EXPECTED_KEYS

    # points en orden [x=lon, y=lat]: primer elemento lon (negativo), segundo lat (~40).
    assert r["points"] == [[-3.7, 40.0], [-3.0, 41.0], [-4.0, 39.0], [-3.5, 40.5]]
    for lon, lat in r["points"]:
        assert lon < 0.0          # longitudes de Espana son negativas
        assert 36.0 < lat < 44.0  # latitudes peninsulares

    # Sin downsampling: 4 < 2000.
    assert r["n_total"] == 4
    assert r["n_shown"] == 4
    assert r["downsampled"] is False

    # bbox correcto.
    assert r["bbox"] == {
        "lat_min": 39.0, "lat_max": 41.0,
        "lon_min": -4.0, "lon_max": -3.0,
    }

    # aspect = 1/cos(centroid_lat); centroid = 40.125 -> ~1.31 > 1.
    centroid_lat = (40.0 + 41.0 + 39.0 + 40.5) / 4.0
    expected_aspect = 1.0 / math.cos(math.radians(centroid_lat))
    assert r["aspect"] > 1.0
    assert abs(r["aspect"] - expected_aspect) < 1e-9
    assert abs(r["aspect"] - 1.305) < 0.02  # cos(40) ~ 0.77

    # pad 5% del rango (lon_range=1.0 -> 0.05 ; lat_range=2.0 -> 0.1).
    assert abs(r["pad"]["lon"] - 0.05) < 1e-9
    assert abs(r["pad"]["lat"] - 0.10) < 1e-9


def test_downsampling_determinista_y_reproducible():
    """Golden: 5000 puntos, max_points=2000 -> n_shown<=2000, downsampled, reproducible."""
    lats = [40.0 + (i % 100) * 0.01 for i in range(5000)]
    lons = [-3.0 - (i % 100) * 0.01 for i in range(5000)]

    r1 = build_geo_scatter(lats, lons, max_points=2000)

    assert r1["n_total"] == 5000
    assert r1["n_shown"] <= 2000
    assert r1["downsampled"] is True
    # step = ceil(5000/2000) = 3 -> len(pairs[::3]) = 1667.
    assert r1["n_shown"] == 1667

    # Determinista: dos llamadas con la misma entrada dan exactamente lo mismo.
    r2 = build_geo_scatter(lats, lons, max_points=2000)
    assert r1 == r2
    assert r1["points"] == r2["points"]

    # El primer punto del downsample es el primer par valido (step parte de 0).
    assert r1["points"][0] == [lons[0], lats[0]]


def test_listas_vacias_no_lanza():
    """Edge: listas vacias / None -> points [] sin lanzar."""
    r = build_geo_scatter([], [])
    assert r["points"] == []
    assert r["n_total"] == 0
    assert r["n_shown"] == 0
    assert r["downsampled"] is False
    assert r["bbox"] is None
    assert r["aspect"] == 1.0
    assert r["pad"] == {"lon": 0.0, "lat": 0.0}

    # None como entrada tampoco lanza.
    assert build_geo_scatter(None, None)["points"] == []
    assert build_geo_scatter([40.0], None)["n_total"] == 0
    assert build_geo_scatter(None, [-3.0])["n_total"] == 0


def test_un_solo_punto_pad_minimo_y_aspect_finito():
    """Edge: un solo punto -> pad minimo no cero, bbox degenerado, aspect finito."""
    r = build_geo_scatter([40.0], [-3.7])

    assert r["n_total"] == 1
    assert r["n_shown"] == 1
    assert r["points"] == [[-3.7, 40.0]]
    assert r["downsampled"] is False
    assert r["bbox"] == {
        "lat_min": 40.0, "lat_max": 40.0,
        "lon_min": -3.7, "lon_max": -3.7,
    }
    # rango 0 -> pad cae al floor minimo (no cero).
    assert r["pad"]["lon"] == 0.01
    assert r["pad"]["lat"] == 0.01
    # aspect finito y dentro del clamp.
    assert math.isfinite(r["aspect"])
    assert 0.3 <= r["aspect"] <= 5.0


def test_filtra_none_nan_y_fuera_de_rango():
    """Edge: pares con None/NaN/fuera de rango se descartan por indice."""
    nan = float("nan")
    inf = float("inf")
    #          i=0     i=1   i=2   i=3      i=4     i=5      i=6
    lats = [40.0, None, nan, 200.0, 41.0, 39.0, inf]
    lons = [-3.0, -3.5, -3.6, -3.7, 999.0, -4.0, -2.0]
    r = build_geo_scatter(lats, lons)

    # Validos solo i=0 (40,-3.0) e i=5 (39,-4.0):
    #   i=1 lat None, i=2 lat NaN, i=3 lat 200 fuera de rango,
    #   i=4 lon 999 fuera de rango, i=6 lat inf.
    assert r["n_total"] == 2
    assert r["points"] == [[-3.0, 40.0], [-4.0, 39.0]]
    assert r["bbox"] == {
        "lat_min": 39.0, "lat_max": 40.0,
        "lon_min": -4.0, "lon_max": -3.0,
    }


def test_latitud_alta_aspect_clamped():
    """Edge: latitudes ~85 -> aspect clamped <= 5.0."""
    r = build_geo_scatter([85.0, 85.0, 84.0], [10.0, 11.0, 9.0])
    # cos(~84.7) ~ 0.093 -> 1/0.093 ~ 10.7 -> clamp a 5.0.
    assert r["aspect"] <= 5.0
    assert r["aspect"] == 5.0
    assert math.isfinite(r["aspect"])
