"""
Test de la logique géométrique du module insee_density SANS dépendre du vrai
téléchargement INSEE (impossible depuis ce bac à sable de développement, réseau
bloqué). On simule une mini-grille de carreaux 200x200 en Lambert93 avec une
population connue, et on vérifie que le calcul d'intersection pondérée donne
le résultat attendu analytiquement.
"""
import math
import geopandas as gpd
from shapely.geometry import Polygon, box
from shapely.ops import transform
from pyproj import Transformer

import insee_density as idm


def make_synthetic_grid(origin_x, origin_y, n=6, pop_per_cell=20):
    """Grille n x n de carreaux 200m, population constante par carreau."""
    rows = []
    for i in range(n):
        for j in range(n):
            x0 = origin_x + i * 200
            y0 = origin_y + j * 200
            geom = box(x0, y0, x0 + 200, y0 + 200)
            rows.append({"idcar_200m": f"c_{i}_{j}", "ind": pop_per_cell, "geometry": geom})
    return gpd.GeoDataFrame(rows, crs="EPSG:2154")


def test_geometry_pipeline():
    # Zone carrée de 400x400 m (= 0.16 km²) posée exactement sur 4 carreaux entiers
    origin_x, origin_y = 650000, 6860000  # coordonnées Lambert93 plausibles (France)
    zone_l93 = box(origin_x, origin_y, origin_x + 400, origin_y + 400)

    grid = make_synthetic_grid(origin_x - 200, origin_y - 200, n=6, pop_per_cell=20)

    zone_area_km2 = zone_l93.area / 1_000_000
    total_population = 0.0
    for _, row in grid.iterrows():
        inter = row.geometry.intersection(zone_l93)
        if inter.is_empty:
            continue
        overlap_ratio = inter.area / row.geometry.area
        total_population += row["ind"] * overlap_ratio

    densite = total_population / zone_area_km2

    # La zone 400x400 recouvre exactement 4 carreaux de 200x200 (aucun débordement)
    # -> population totale = 4 * 20 = 80 individus, surface = 0.16 km²
    expected_pop = 80.0
    expected_density = expected_pop / 0.16

    assert math.isclose(total_population, expected_pop, rel_tol=1e-6), total_population
    assert math.isclose(densite, expected_density, rel_tol=1e-6), densite
    print(f"OK — population={total_population}, densité={densite:.1f} ppl/km² (attendu {expected_density:.1f})")


def test_kml_polygon_extraction():
    # petit KML synthétique avec un placemark "Zone tampon"
    kml_content = """<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document>
<Placemark><name>Zone de vol</name>
  <Polygon><outerBoundaryIs><LinearRing><coordinates>
    -0.001,47.001,0 0.001,47.001,0 0.001,47.003,0 -0.001,47.003,0 -0.001,47.001,0
  </coordinates></LinearRing></outerBoundaryIs></Polygon>
</Placemark>
<Placemark><name>Zone tampon (buffer)</name>
  <Polygon><outerBoundaryIs><LinearRing><coordinates>
    -0.002,47.000,0 0.002,47.000,0 0.002,47.004,0 -0.002,47.004,0 -0.002,47.000,0
  </coordinates></LinearRing></outerBoundaryIs></Polygon>
</Placemark>
</Document></kml>"""
    import tempfile, os
    fd, path = tempfile.mkstemp(suffix=".kml")
    with os.fdopen(fd, "w", encoding="utf-8") as f:
        f.write(kml_content)

    polys = idm.extract_polygons_from_kml(path)
    assert len(polys) == 2, polys
    zone = idm.find_grc_zone(path)
    # doit avoir choisi le polygone "Zone tampon" (le plus grand des deux, bornes -0.002/-0.000)
    minx, miny, maxx, maxy = zone.bounds
    assert math.isclose(minx, -0.002, abs_tol=1e-9), zone.bounds
    print("OK — sélection correcte du polygone 'zone tampon' dans le KML")
    os.remove(path)


def test_max_cell_density_vs_average():
    """Vérifie que _max_cell_density retourne bien le pic (un seul carreau très
    peuplé au milieu de carreaux peu peuplés), et pas une moyenne diluée comme
    _weighted_density -- distinction essentielle Section B (densité MAXIMALE)
    vs Section D (densité MOYENNE)."""
    origin_x, origin_y = 650000, 6860000
    zone_l93 = box(origin_x, origin_y, origin_x + 1000, origin_y + 1000)  # 1 km²

    grid = make_synthetic_grid(origin_x - 200, origin_y - 200, n=8, pop_per_cell=2)
    # un seul carreau "hotspot" très peuplé au centre de la zone
    hotspot_x, hotspot_y = origin_x + 400, origin_y + 400
    for i, row in enumerate(grid.itertuples()):
        if row.geometry.bounds[0] == hotspot_x and row.geometry.bounds[1] == hotspot_y:
            grid.at[row.Index, "ind"] = 400  # 400 ind / 0.04 km² = 10000 ppl/km²

    # (on rejoue ici la logique de idm._max_cell_density / idm._weighted_density
    # sur une grille synthétique, sans dépendre du téléchargement INSEE réel)
    max_density = _run_max_cell_density(grid, zone_l93)
    avg_density = _run_weighted_density(grid, zone_l93)

    assert math.isclose(max_density, 400 / 0.04, rel_tol=1e-6), max_density
    assert avg_density < max_density / 10, (avg_density, max_density)
    print(f"OK — densité max={max_density:.0f} ppl/km² (hotspot) vs densité moyenne={avg_density:.1f} ppl/km² "
          f"(bien plus faible, comme attendu)")


def _run_max_cell_density(grid, zone_l93):
    max_density = 0.0
    for _, row in grid.iterrows():
        if row.geometry.intersects(zone_l93) and not row.geometry.intersection(zone_l93).is_empty:
            cell_density = row["ind"] / idm.CARREAU_AREA_KM2
            max_density = max(max_density, cell_density)
    return max_density


def _run_weighted_density(grid, zone_l93):
    zone_area_km2 = zone_l93.area / 1_000_000
    total_population = 0.0
    for _, row in grid.iterrows():
        inter = row.geometry.intersection(zone_l93)
        if inter.is_empty:
            continue
        overlap_ratio = inter.area / row.geometry.area
        total_population += row["ind"] * overlap_ratio
    return total_population / zone_area_km2


def test_5km_circle_geometry():
    """Vérifie que le cercle de confinement (rayon 5 km) a la bonne surface et
    que la pondération par recouvrement reste cohérente sur une grille dense."""
    from shapely.geometry import Point

    origin_x, origin_y = 650000, 6860000
    center = Point(origin_x, origin_y)
    circle = center.buffer(idm.RAYON_CONFINEMENT_M, resolution=64)

    expected_area_km2 = math.pi * (idm.RAYON_CONFINEMENT_M / 1000) ** 2
    actual_area_km2 = circle.area / 1_000_000
    # resolution=64 donne un polygone très proche du cercle exact (<0.1% d'écart)
    assert math.isclose(actual_area_km2, expected_area_km2, rel_tol=1e-3), (actual_area_km2, expected_area_km2)

    # grille dense (200m, pop uniforme) sur une zone qui couvre largement le cercle
    n_side_m = int(idm.RAYON_CONFINEMENT_M * 2.2)
    n = n_side_m // 200
    grid = make_synthetic_grid(origin_x - n_side_m // 2, origin_y - n_side_m // 2, n=n, pop_per_cell=8)
    # densité "vraie" du semis : 8 individus / (0.2*0.2 km²) = 200 ppl/km²
    true_density = 8 / (0.2 * 0.2)

    total_population = 0.0
    for _, row in grid.iterrows():
        inter = row.geometry.intersection(circle)
        if inter.is_empty:
            continue
        overlap_ratio = inter.area / row.geometry.area
        total_population += row["ind"] * overlap_ratio

    densite = total_population / actual_area_km2
    # avec un semis homogène couvrant largement le cercle, la densité pondérée
    # doit converger vers la densité vraie du semis (200 ppl/km²), à qqs % près
    assert math.isclose(densite, true_density, rel_tol=0.05), (densite, true_density)
    print(f"OK — cercle 5km : surface={actual_area_km2:.2f} km² (attendu {expected_area_km2:.2f}), "
          f"densité mesurée={densite:.1f} ppl/km² (semis homogène à {true_density:.1f})")


def test_outermost_zone_nested():
    """Zones emboîtées géographie ⊂ contingence ⊂ buffer, placemarks nommés de
    façon quelconque : on doit retenir la plus extérieure (buffer)."""
    import os, tempfile, math
    kml = """<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2"><Document>
<Placemark><name>Zone de vol</name><Polygon><outerBoundaryIs><LinearRing><coordinates>
  -0.001,47.001,0 0.001,47.001,0 0.001,47.002,0 -0.001,47.002,0 -0.001,47.001,0
</coordinates></LinearRing></outerBoundaryIs></Polygon></Placemark>
<Placemark><name>Contingence</name><Polygon><outerBoundaryIs><LinearRing><coordinates>
  -0.002,47.000,0 0.002,47.000,0 0.002,47.003,0 -0.002,47.003,0 -0.002,47.000,0
</coordinates></LinearRing></outerBoundaryIs></Polygon></Placemark>
<Placemark><name>Zone C</name><Polygon><outerBoundaryIs><LinearRing><coordinates>
  -0.004,46.998,0 0.004,46.998,0 0.004,47.005,0 -0.004,47.005,0 -0.004,46.998,0
</coordinates></LinearRing></outerBoundaryIs></Polygon></Placemark>
</Document></kml>"""
    fd, path = tempfile.mkstemp(suffix=".kml")
    os.write(fd, kml.encode()); os.close(fd)
    # aucun mot-clé "buffer" ici -> sélection géométrique de la zone la plus externe
    zone = idm.find_grc_zone(path, keywords=())
    minx, miny, maxx, maxy = zone.bounds
    assert math.isclose(minx, -0.004, abs_tol=1e-9) and math.isclose(maxx, 0.004, abs_tol=1e-9), zone.bounds
    print("OK — zone la plus extérieure (buffer) correctement sélectionnée sans nom explicite")
    os.remove(path)


def test_outermost_zone_named_buffer():
    """Si un placemark s'appelle explicitement 'Buffer', il est prioritaire."""
    import os, tempfile, math
    kml = """<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2"><Document>
<Placemark><name>Geographie</name><Polygon><outerBoundaryIs><LinearRing><coordinates>
  -0.003,46.999,0 0.003,46.999,0 0.003,47.004,0 -0.003,47.004,0 -0.003,46.999,0
</coordinates></LinearRing></outerBoundaryIs></Polygon></Placemark>
<Placemark><name>Buffer GRC</name><Polygon><outerBoundaryIs><LinearRing><coordinates>
  -0.002,47.000,0 0.002,47.000,0 0.002,47.003,0 -0.002,47.003,0 -0.002,47.000,0
</coordinates></LinearRing></outerBoundaryIs></Polygon></Placemark>
</Document></kml>"""
    fd, path = tempfile.mkstemp(suffix=".kml")
    os.write(fd, kml.encode()); os.close(fd)
    zone = idm.find_grc_zone(path)
    minx, miny, maxx, maxy = zone.bounds
    # doit renvoyer le placemark nommé "Buffer GRC" (bornes -0.002..0.002),
    # même si "Geographie" est géométriquement plus grand ici.
    assert math.isclose(minx, -0.002, abs_tol=1e-9), zone.bounds
    print("OK — placemark nommé 'Buffer' prioritaire sur la géométrie")
    os.remove(path)


if __name__ == "__main__":
    test_geometry_pipeline()
    test_kml_polygon_extraction()
    test_max_cell_density_vs_average()
    test_5km_circle_geometry()
    test_outermost_zone_nested()
    test_outermost_zone_named_buffer()
    print("Tous les tests (logique géométrique) passent.")
