Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

facett-map/tests/paint_order_pixels.rs

//! **THE declared paint order, asserted on PIXELS** (`facett_map::paint_order`).
//!
//! # What the owner reported, 2026-08-22
//!
//! On the live 2D map: the basemap "draws in the order things were read in, and paints
//! over what should be on top". Four mechanisms produced that (see the
//! `facett_map::paint_order` module docs); this file is the guard for the cure, and it
//! is deliberately written against the **rendered framebuffer** rather than against the
//! sorted `Vec<DrawFeature>`.
//!
//! That distinction is the whole point (LAW 2). A `feats[0].order < feats[1].order`
//! assertion is a state round-trip: it re-reads the very key the sort was given and can
//! pass on a painter that ignores the sort entirely, on a GPU lane that re-orders the
//! stream behind it, and on a split that drops the callback in the wrong slot. Only the
//! pixels know which polygon ended up on top.
//!
//! # The three claims
//!
//! 1. **`input_order_cannot_change_a_pixel`** — the same two overlapping features fed
//!    in the two possible orders must render byte-identically. This is the reported
//!    symptom stated as a property.
//! 2. **`a_bridge_paints_over_the_water_and_a_tunnel_under_it`** — OSM `bridge` /
//!    `tunnel` / `layer`, which were read NOWHERE before this change, decide the stack
//!    at the crossing pixel.
//! 3. **`the_gpu_slot_lands_in_the_declared_stack`** — the CPU/GPU seam: when the lane
//!    owns one class's fills and the CPU painter keeps another's, the callback slot
//!    goes at the lane's own rank FLOOR, so a CPU-painted building still lands over
//!    GPU-painted water. It used to go at a hardcoded `LayerKind::Line.default_z()`,
//!    which put every GPU fill above every CPU fill whatever the class.
//!
//! # Proven RED (2026-08-22, oden, RTX 4090)
//!
//! Each claim was watched fail before it was trusted — see the `RED:` note above each
//! test for the exact edit that reddens it.
//!
//! Run: `cargo test -p facett-map --features wgpu --test paint_order_pixels`

use egui_kittest::Harness;
use facett_map::layer::{LatLon, MapFacet, MapTheme, MapView, WaysLayer};
use facett_map::{Way, WayKind};

/// The ONE GPU gate for this crate's device-backed tests: a missing adapter becomes a
/// SKIP row in the matrix and a panic under CI, never a silent `ok`.
mod common;

/// A closed axis-aligned ring at `(lon, lat)` of half-width `w`, tagged `tags`.
///
/// Built through [`Way::from_lonlat_with`] — the SAME constructor korp's served and
/// tiled paths use — so the tags this test writes are parsed by the same code the live
/// map runs, including `paint_order::parse_osm_z`.
fn ring(id: i64, lon: f64, lat: f64, w: f64, tags: &[(&str, &str)]) -> Way {
    let t: Vec<(String, String)> = tags.iter().map(|(k, v)| ((*k).to_string(), (*v).to_string())).collect();
    let r = vec![
        (lon - w, lat - w),
        (lon + w, lat - w),
        (lon + w, lat + w),
        (lon - w, lat + w),
        (lon - w, lat - w),
    ];
    Way::from_lonlat_with(id, &r, &t, &facett_map::MapPalette::cartographic(false))
        .expect("a 5-vertex closed ring is a way")
}

/// A straight two-point line at latitude `lat`, tagged `tags`.
fn line(id: i64, lon0: f64, lon1: f64, lat: f64, tags: &[(&str, &str)]) -> Way {
    let t: Vec<(String, String)> = tags.iter().map(|(k, v)| ((*k).to_string(), (*v).to_string())).collect();
    Way::from_lonlat_with(id, &[(lon0, lat), (lon1, lat)], &t, &facett_map::MapPalette::cartographic(false))
        .expect("a 2-vertex line is a way")
}

fn facet(ways: Vec<Way>) -> MapFacet {
    let view = MapView::new(LatLon::new(59.33, 18.06), 13.0, (520.0, 400.0));
    MapFacet::new("paint-order", view, MapTheme::default())
        .with_layer(Box::new(WaysLayer::new("osm-ways", ways)))
}

// **The one-device-at-a-time turn lives in `common`** — `common/one_gpu.rs`, reached as
// `common::one_gpu` because this file already says `mod common;`. Declaring a second
// `#[path]` copy at the crate root of this test binary would compile two DIFFERENT
// statics out of one file, and two mutexes over one device serialise nothing.

/// Render a facet through the real wgpu backend and hand back the framebuffer as
/// `(width, height, rgba)`.
fn render(mut f: MapFacet) -> (u32, u32, Vec<u8>) {
    let _turn = common::one_gpu::hold();
    let mut harness = Harness::builder().with_size(egui::vec2(520.0, 400.0)).build_ui(move |ui| {
        f.ui(ui);
    });
    harness.run_steps(3);
    let img = harness.render().expect("wgpu render produced an image");
    (img.width(), img.height(), img.as_raw().clone())
}

/// The rendered colour at a fraction of the pane (`0.0..1.0` in each axis).
fn px_at(w: u32, h: u32, buf: &[u8], fx: f32, fy: f32) -> [u8; 4] {
    let x = ((fx * w as f32) as u32).min(w - 1);
    let y = ((fy * h as f32) as u32).min(h - 1);
    let i = ((y * w + x) * 4) as usize;
    [buf[i], buf[i + 1], buf[i + 2], buf[i + 3]]
}

fn near(a: [u8; 4], b: [u8; 4], tol: i32) -> bool {
    (0..3).all(|i| (i32::from(a[i]) - i32::from(b[i])).abs() <= tol)
}

// ─────────────────────────────────────────────────────────────────────────────

/// **THE reported bug, as a property.** Two overlapping features, fed in the two
/// possible input orders, must produce the SAME framebuffer.
///
/// The fixture is chosen so the old code genuinely disagreed with itself: a `landuse`
/// ring and a `water` ring of the same size, overlapping. Both are `DrawGeom::Polygon`
/// with the identical old `z` (`LayerKind::Polygon.default_z()`), so the old stable
/// sort left the winner of the overlap to whichever was pushed second — i.e. to the
/// order the tile merge happened to append them in.
///
/// RED: revert `features_for_cpu` to `feats.sort_by_key(|f| f.z)` with the per-kind
/// `LayerKind::default_z()` — the two renders then differ on ~7 800 of 208 000 pixels
/// (measured on oden, 2026-08-22), because the overlap paints in whichever colour came
/// last.
#[test]
fn input_order_cannot_change_a_pixel() {
    // Through `device_or_skip`, not a bare `headless_device(..).is_none()`
    // (lane ST, map2d-S9/S10). The bare form returned silently: no stderr line,
    // no matrix row, and a host that DECLARES it has a GPU did not fail. A skip
    // that leaves no mark is indistinguishable from a pass in every readout the
    // suite produces.
    if common::device_or_skip(
        "input_order_cannot_change_a_pixel",
        "that two feature orderings rasterise to the SAME pixels — the paint order is the declared lane stack, not the order features arrived in",
    )
    .is_none()
    {
        return;
    }
    let mk = || {
        (
            ring(1, 18.055, 59.330, 0.010, &[("landuse", "forest")]),
            ring(2, 18.065, 59.330, 0.010, &[("natural", "water")]),
        )
    };
    let (a, b) = mk();
    let (w0, h0, forward) = render(facet(vec![a, b]));
    let (a, b) = mk();
    let (w1, h1, reversed) = render(facet(vec![b, a]));

    assert_eq!((w0, h0), (w1, h1), "same pane size both ways");
    // NON-VACUOUS: the fixture must actually draw two colours that overlap, or
    // "identical" is the identity trap — two blank panes are also identical.
    let distinct: std::collections::HashSet<[u8; 4]> =
        forward.as_chunks::<4>().0.iter().map(|c| [c[0], c[1], c[2], c[3]]).collect();
    assert!(
        distinct.len() >= 3,
        "the fixture must render background + BOTH fills ({} distinct colours found) — \
         otherwise an identical pair proves nothing",
        distinct.len()
    );

    let diff = forward.as_chunks::<4>().0.iter().zip(reversed.as_chunks::<4>().0).filter(|(a, b)| a != b).count();
    assert_eq!(
        diff,
        0,
        "swapping the INPUT order changed {diff} of {} pixels — the stack is still decided \
         by the order the ways were read in, which is exactly what the owner reported",
        (w0 * h0) as usize
    );

    nornir_testmatrix::functional_status(
        "facett-map",
        "input_order_cannot_change_a_pixel",
        diff == 0,
        &format!("{diff} differing pixels of {} over {} distinct colours", w0 * h0, distinct.len()),
    );
}

/// **OSM `bridge` / `tunnel` decide the crossing pixel.** They were read nowhere in
/// `facett-map`, `facett-osm` or `korp/src/map_paint.rs` before 2026-08-22, so a
/// viaduct and the river under it had nothing but their classes to go on — and a road
/// is a LINE, so it went over the water whether it was a bridge or a tunnel.
///
/// RED: drop the `w.z_order.effective_layer() * LAYER_STEP` term from
/// `paint_order::way_rank` — the tunnel then paints over the water exactly as the
/// bridge does and the second assertion fails.
#[test]
fn a_bridge_paints_over_the_water_and_a_tunnel_under_it() {
    // Through `device_or_skip`, not a bare `headless_device(..).is_none()`
    // (lane ST, map2d-S9/S10). The bare form returned silently: no stderr line,
    // no matrix row, and a host that DECLARES it has a GPU did not fail. A skip
    // that leaves no mark is indistinguishable from a pass in every readout the
    // suite produces.
    if common::device_or_skip(
        "a_bridge_paints_over_the_water_and_a_tunnel_under_it",
        "that a bridge's pixels land ON TOP of the water it crosses and a tunnel's land UNDER it — the layer= tag reaching the raster, not just the sort",
    )
    .is_none()
    {
        return;
    }
    // A wide lake with a road straight across its middle.
    let lake = || ring(1, 18.060, 59.330, 0.020, &[("natural", "water")]);
    let road = |tags: &[(&str, &str)]| line(2, 18.040, 18.080, 59.330, tags);

    let bridge_tags: &[(&str, &str)] = &[("highway", "motorway"), ("bridge", "yes")];
    let tunnel_tags: &[(&str, &str)] = &[("highway", "motorway"), ("tunnel", "yes")];

    // Water must be painted LAST in the input in both cases, so that "whoever was read
    // last wins" — the old behaviour — would put the lake over the road every time.
    let (w, h, with_bridge) = render(facet(vec![road(bridge_tags), lake()]));
    let (_, _, with_tunnel) = render(facet(vec![road(tunnel_tags), lake()]));

    // The centre of the pane is the middle of the lake, on the road's line.
    let bridge_px = px_at(w, h, &with_bridge, 0.5, 0.5);
    let tunnel_px = px_at(w, h, &with_tunnel, 0.5, 0.5);
    // A point inside the lake but well off the road — the water reference colour.
    let water_px = px_at(w, h, &with_tunnel, 0.5, 0.30);

    assert!(
        near(tunnel_px, water_px, 6),
        "a TUNNEL under the lake must leave the water's own colour on the crossing pixel: \
         got {tunnel_px:?} at the crossing vs {water_px:?} in open water"
    );
    assert!(
        !near(bridge_px, water_px, 6),
        "a BRIDGE over the lake must show the ROAD on the crossing pixel, not the water: \
         got {bridge_px:?}, water is {water_px:?}"
    );
    assert!(
        bridge_px != tunnel_px,
        "bridge and tunnel must not render the same crossing pixel ({bridge_px:?}) — \
         that is the state the map was in when the `layer` tag was read nowhere"
    );

    nornir_testmatrix::functional_status(
        "facett-map",
        "bridge_over_water_tunnel_under",
        !near(bridge_px, water_px, 6) && near(tunnel_px, water_px, 6),
        &format!("crossing px: bridge {bridge_px:?}, tunnel {tunnel_px:?}, open water {water_px:?}"),
    );
}

/// **The CPU/GPU seam.** The GPU callback is ONE opaque slot, so where it goes in the
/// CPU list decides the whole cross-lane stack.
///
/// It went at a hardcoded `LayerKind::Line.default_z()` — "the lane draws lines" —
/// which stopped being true when the fill lane landed: a lake the GPU took was drawn in
/// the LINE slot, **above every CPU-painted fill whatever its class**, and
/// `way_lane_plan_scan` decides per viewport which layers the lane takes, so the
/// stacking changed with what had been loaded.
///
/// The applied output here is the SPLIT INDEX the frame computes — the number that
/// says how many CPU features paint before the callback — taken from the same code
/// path `MapFacet::ui` runs (`gpu_slot_split_for_test`), over the real feature list.
///
/// RED: put the split back to `feats.partition_point(|f| f.order.rank < LINE_BAND)`
/// (the old `Line.default_z()` behaviour) — the CPU-painted building then falls BELOW
/// the callback and the last assertion fires.
///
/// **Gated on `wgpu`, and it has to be.** `MapFacet::gpu_slot_split_for_test` is itself
/// `#[cfg(feature = "wgpu")]` (layer.rs:2883), and facett-map's `default` closure is
/// empty — so before this attribute existed, a plain `cargo test -p facett-map` did not
/// COMPILE at all: `E0599: no method named gpu_slot_split_for_test`. Every other test in
/// this file went down with it, including the CPU-only pixel proofs, which is worse than
/// the ordering bug the file was written for. The rest of the file stays in the default
/// green; only the arm that reaches the wgpu-gated hook is gated.
#[cfg(feature = "wgpu")]
#[test]
fn the_gpu_slot_lands_in_the_declared_stack() {
    // Layer 0: water — the lane will own its fills.
    // Layer 1: a building — the CPU painter keeps it.
    let water = ring(1, 18.060, 59.330, 0.020, &[("natural", "water")]);
    let building = ring(2, 18.060, 59.330, 0.004, &[("building", "yes")]);
    assert_eq!(water.style.kind, WayKind::Water, "fixture: layer 0 really is water");
    assert_eq!(building.style.kind, WayKind::Building, "fixture: layer 1 really is a building");

    let view = MapView::new(LatLon::new(59.33, 18.06), 13.0, (520.0, 400.0));
    let f = MapFacet::new("seam", view, MapTheme::default())
        .with_layer(Box::new(WaysLayer::new("osm-water", vec![water])))
        .with_layer(Box::new(WaysLayer::new("osm-buildings", vec![building])));

    // The lane takes layer 0's FILLS only. `features_for_cpu` therefore drops the water
    // polygon and keeps the building — the exact configuration the item names.
    let feats = f.features_for_cpu_for_test(&[0], false, true);
    assert_eq!(feats.len(), 1, "the CPU keeps exactly the building (the lane took the water)");
    assert_eq!(feats[0].id, 2, "and it is the BUILDING that stayed on the CPU");

    let (floor, split) = f.gpu_slot_split_for_test(&[0], false, true, &feats);
    let floor = floor.expect("the lane owns the water fill, so it has a rank floor");
    assert_eq!(
        floor,
        facett_map::way_rank(&ring(1, 18.060, 59.330, 0.020, &[("natural", "water")])),
        "the slot floor is the WATER's declared rank — the lowest thing the lane draws"
    );
    assert!(
        floor < feats[0].order.rank,
        "sanity: water ranks under buildings ({floor} < {})",
        feats[0].order.rank
    );
    assert_eq!(
        split, 0,
        "ZERO CPU features may paint before the callback: the building ranks ABOVE the \
         water the lane owns, so it must land in the tail — a split of {split} would put \
         the CPU building UNDER the GPU water, which is the tangle on the live map"
    );

    nornir_testmatrix::functional_status(
        "facett-map",
        "gpu_slot_lands_in_the_declared_stack",
        split == 0,
        &format!("lane floor {floor}, building rank {}, split {split}", feats[0].order.rank),
    );
}

/// **The GPU lane's own stream order.** The three pixel claims above exercise the CPU
/// painter (a headless `Harness` installs no `RenderState`, so `renderer_installed()`
/// is false and the frame never places a callback). The device lane has to be pinned
/// separately, and its applied output is the **vertex stream** — the exact bytes
/// `draw_indirect` pulls, in the exact order the rasteriser consumes them.
///
/// Two things are asserted, both on the baked bytes:
///
/// 1. swapping the input order does not move a single byte of `tri_vertices`;
/// 2. the WATER's triangles come before the BUILDING's, i.e. the declared class order
///    survives into the stream.
///
/// This is where the tile lane's tangle lived: `add_gpu_tiled_callback` groups by
/// `Way::tile_key`, and the renderer draws all tiles' fills, then all tiles' outlines,
/// then all tiles' roads — so ordering *inside* the stream is the only class order the
/// tile lane has.
///
/// RED: delete the `fills.sort_unstable_by_key(..)` in `build_gpu_fill_geometry` — the
/// two bakes then differ on every vertex from the first way onwards and claim 2 fails
/// outright.
#[cfg(feature = "wgpu")]
#[test]
fn the_gpu_bake_emits_the_declared_order_whatever_the_input_order() {
    use facett_map::gpu::{build_gpu_fill_geometry, ways_origin};
    use facett_map::tessellate::FillTessCache;

    let water = ring(1, 18.060, 59.330, 0.020, &[("natural", "water")]);
    let building = ring(2, 18.060, 59.330, 0.004, &[("building", "yes")]);
    let origin = ways_origin([&water, &building].into_iter()).expect("non-empty fixture");

    // One cache per way so the ear clip is the same triangulation in both bakes.
    let bake = |ws: [&Way; 2]| {
        let cache = FillTessCache::new(2);
        let items: Vec<_> = ws
            .iter()
            .enumerate()
            .filter_map(|(i, w)| cache.tris(i, &w.lonlat, &w.holes).map(|t| (*w, w.style.lod as u32, t)))
            .collect();
        build_gpu_fill_geometry(items.into_iter(), origin)
    };

    let forward = bake([&water, &building]);
    let reversed = bake([&building, &water]);

    assert!(!forward.tri_vertices.is_empty(), "the fixture must actually bake triangles");
    assert_eq!(forward.tri_meta.len(), 2, "both rings reached the stream");
    assert_eq!(
        forward.tri_vertices, reversed.tri_vertices,
        "the baked fill stream must not depend on the order the ways were handed over — \
         that dependence is what makes the tile lane's stacking change with what was loaded"
    );
    assert_eq!(forward.outline_vertices, reversed.outline_vertices, "and neither must the outlines");

    // And the order is the DECLARED one, not just a stable one: water under buildings.
    // `tri_meta` spans are emitted in stream order, so the first span is what draws first.
    //
    // The discriminator is the vertex COLOUR, not the vertex count: both fixtures are
    // 5-point squares and therefore bake the SAME number of triangles, so a
    // count-based check would read `true` whichever way round the stream came out —
    // a guard that could never go red (LAW 2, the identity-value trap).
    let want = |c: egui::Color32| {
        let [r, g, b, a] = c.to_array();
        [f32::from(r) / 255.0, f32::from(g) / 255.0, f32::from(b) / 255.0, f32::from(a) / 255.0]
    };
    let water_col = want(water.fill.expect("the lake is filled"));
    let building_col = want(building.fill.expect("the footprint is filled"));
    assert_ne!(water_col, building_col, "fixture: the two fills must be DIFFERENT colours");
    let first_col = forward.tri_vertices[0].col;
    let water_first = first_col == water_col;
    assert!(
        forward.tri_meta[0].vert_start < forward.tri_meta[1].vert_start,
        "spans must tile the stream in draw order"
    );
    assert!(
        water_first,
        "the WATER's triangles must be the first span in the stream (drawn first = under \
         the building); the stream opens with {first_col:?}, water is {water_col:?} and the \
         building is {building_col:?}"
    );

    nornir_testmatrix::functional_status(
        "facett-map",
        "gpu_bake_emits_the_declared_order",
        forward.tri_vertices == reversed.tri_vertices && water_first,
        &format!(
            "{} tri verts, spans {:?}",
            forward.tri_vertices.len(),
            forward.tri_meta.iter().map(|m| (m.vert_start, m.vert_count)).collect::<Vec<_>>()
        ),
    );
}

/// **A ditch is not a river.** `style.rs` classified every `waterway=*` through ONE
/// branch — river, stream, ditch, drain, canal, weir, dam all got width 1.0, the same
/// blue, `is_area: true` and `Lod::Region` — while the `highway` classes directly above
/// have had a four-rung ladder all along. `Lod::Region` starts at camera **6**
/// (`LOD_TIER_ZOOMS = [0, 6, 12, 15]`), so every agricultural ditch in the extract drew
/// from a regional overview outwards, at a zoom where it is far under a pixel.
///
/// The applied output is the PIXELS at a Region-tier camera: the river must be on the
/// canvas and the ditch must not. A `feats.len()` or an `assert_eq!(lod, Detail)` would
/// be a state round-trip — it would re-read the constant the classifier was just given.
///
/// RED: put the classifier back to the single `if has("waterway")` arm at
/// `Lod::Region` — the ditch then draws at camera 8 and the second assertion fires.
#[test]
fn a_ditch_is_not_drawn_at_a_region_zoom_and_a_river_is() {
    // Through `device_or_skip`, not a bare `headless_device(..).is_none()`
    // (lane ST, map2d-S9/S10). The bare form returned silently: no stderr line,
    // no matrix row, and a host that DECLARES it has a GPU did not fail. A skip
    // that leaves no mark is indistinguishable from a pass in every readout the
    // suite produces.
    if common::device_or_skip(
        "a_ditch_is_not_drawn_at_a_region_zoom_and_a_river_is",
        "that the zoom filter drops a ditch and keeps a river at a region zoom — the min-zoom gate reaching the raster",
    )
    .is_none()
    {
        return;
    }
    // Camera 8.0 → `view_lod_tier` = Region (the `[0, 6, 12, 15]` ladder). One water
    // line straight across the pane, drawn alone so the canvas is background + it.
    let across = |id: i64, tags: &[(&str, &str)]| line(id, 17.6, 18.5, 59.330, tags);
    let at_region = |ways: Vec<Way>| {
        let view = MapView::new(LatLon::new(59.33, 18.06), 8.0, (520.0, 400.0));
        render(MapFacet::new("water", view, MapTheme::default()).with_layer(Box::new(WaysLayer::new("osm-water", ways))))
    };

    let river = across(1, &[("waterway", "river")]);
    let ditch = across(2, &[("waterway", "ditch")]);
    assert_eq!(river.style.kind, WayKind::Water, "fixture: a river is water");
    assert_eq!(ditch.style.kind, WayKind::Water, "fixture: so is a ditch");

    let (w, h, empty) = at_region(vec![]);
    let (_, _, with_river) = at_region(vec![across(1, &[("waterway", "river")])]);
    let (_, _, with_ditch) = at_region(vec![across(2, &[("waterway", "ditch")])]);

    let ink = |buf: &[u8]| buf.as_chunks::<4>().0.iter().zip(empty.as_chunks::<4>().0).filter(|(a, b)| a != b).count();
    let river_px = ink(&with_river);
    let ditch_px = ink(&with_ditch);

    assert!(
        river_px > 0,
        "a RIVER must be on the canvas at a regional zoom — it painted {river_px} pixels of {}",
        (w * h) as usize
    );
    assert_eq!(
        ditch_px, 0,
        "a DITCH must NOT be on the canvas at a regional zoom — it painted {ditch_px} pixels. \
         Every field drain in the extract drawing from camera 6 outwards is the blue hairball."
    );
    // And the same ditch DOES draw once the camera earns it, so this is a tier and not
    // a way of never drawing ditches at all.
    let deep = MapView::new(LatLon::new(59.33, 18.06), 15.5, (520.0, 400.0));
    let (_, _, blank_deep) = render(MapFacet::new("water", deep, MapTheme::default()));
    let deep = MapView::new(LatLon::new(59.33, 18.06), 15.5, (520.0, 400.0));
    let (_, _, ditch_deep) = render(
        MapFacet::new("water", deep, MapTheme::default())
            .with_layer(Box::new(WaysLayer::new("osm-water", vec![across(2, &[("waterway", "ditch")])]))),
    );
    let deep_px = ditch_deep.as_chunks::<4>().0.iter().zip(blank_deep.as_chunks::<4>().0).filter(|(a, b)| a != b).count();
    assert!(
        deep_px > 0,
        "the ditch must appear once the camera reaches its tier (zoom 15.5) — otherwise this \
         test passes on a classifier that simply lost the ditch"
    );

    nornir_testmatrix::functional_status(
        "facett-map",
        "waterway_tiering_river_vs_ditch",
        river_px > 0 && ditch_px == 0 && deep_px > 0,
        &format!("at z8: river {river_px} px, ditch {ditch_px} px; at z15.5: ditch {deep_px} px"),
    );
}

// ─────────────────────────────────────────────────────────────────────────────
// LANE GG #6 — a bare `layer` on an AREA buried the road network
// ─────────────────────────────────────────────────────────────────────────────

/// **A `landuse=grass layer=1` RING MUST NOT COVER A GROUND ROAD.**
///
/// # The defect
///
/// `way_rank`'s dominant term is `effective_layer · LAYER_STEP`, and `LAYER_STEP` is
/// `100_000` — chosen so no combination of band, class and tier can climb out of its own
/// layer. That is right for the thing `layer` exists to express: a deck over a river, a
/// tunnel under a street. It is a disaster for a **mis-tag**, because a single careless
/// `layer=1` on an opaque polygon lifts it a hundred thousand rank units — past every
/// road, every path, every label — and OSM is full of them.
///
/// So `landuse=grass layer=1` paints over the road that crosses it, and there is nothing
/// the class order can do about it. That is one of the "terrain over roads" symptoms
/// directly, and it is the one that survives every fix to the GPU depth stack, because it
/// is not a depth bug at all: the declared order genuinely says the grass is on top.
///
/// # The rule, and why it is not "ignore `layer` on areas"
///
/// An area may legitimately be off the ground — a `man_made=bridge` deck, a building
/// passage, an underground car park — and OSM says so with `bridge=*` or `tunnel=*`
/// **alongside** the `layer`. A bare `layer` on an area says only *"put me somewhere
/// else"* without saying what it is, and that is the shape of the mis-tag.
///
/// So: **an AREA honours `layer` only when it also says why** (`bridge` or `tunnel`).
/// A bare `layer` on an area is ignored. LINES are untouched — a `layer=1` road really is
/// a flyover, and that is exactly the case
/// `a_bridge_paints_over_the_water_and_a_tunnel_under_it` guards.
#[test]
fn a_bare_layer_on_a_landuse_ring_cannot_bury_the_road_crossing_it() {
    let grass = ring(1, 18.060, 59.330, 0.020, &[("landuse", "grass"), ("layer", "1")]);
    let road = line(2, 18.040, 18.080, 59.330, &[("highway", "motorway")]);
    assert!(grass.fill.is_some(), "fixture: the landuse ring must be an AREA");
    assert!(road.fill.is_none(), "fixture: the road must be a LINE");
    assert_eq!(grass.z_order.layer, 1, "fixture: the layer tag really parsed");
    assert!(
        !grass.z_order.bridge && !grass.z_order.tunnel,
        "fixture: a BARE layer — the ring says 'put me elsewhere' without saying what it is"
    );

    let road_colour = road.stroke.to_array();
    let grass_colour = grass.fill.expect("an area fill").to_array();
    assert!(
        !near(road_colour, grass_colour, 24),
        "fixture: the two inks must be distinguishable ({road_colour:?} vs {grass_colour:?})"
    );

    // The grass is fed LAST, so "whoever was read last wins" also hands it the pixel.
    let (w, h, buf) = render(facet(vec![road, grass]));
    let crossing = px_at(w, h, &buf, 0.5, 0.5);
    let open_grass = px_at(w, h, &buf, 0.5, 0.35);

    assert!(
        near(open_grass, grass_colour, 24),
        "the grass did not paint where no road crosses it ({open_grass:?} vs \
         {grass_colour:?}) — nothing is being covered, so nothing is proved"
    );
    assert!(
        near(crossing, road_colour, 40),
        "a bare `layer=1` on a landuse ring must not bury the motorway crossing it — got \
         {crossing:?}, the road is {road_colour:?}, the grass is {grass_colour:?}. \
         `effective_layer · LAYER_STEP` is 100 000 rank units, so ONE careless tag \
         outranks the whole road network and no class order can answer it."
    );
}

/// **A `man_made=bridge` AREA KEEPS its layer** — the rule above must not throw the real
/// decks out with the mis-tags.
///
/// Without this the fix is "areas have no layer", which is a different and wrong claim: a
/// bridge deck is a polygon that genuinely IS above the water it spans, and burying it
/// would trade one visible defect for another one nobody is looking at.
#[test]
fn an_area_that_says_why_it_is_raised_keeps_its_layer() {
    // `man_made=bridge` alone does not classify to a drawable way (`from_lonlat_with`
    // answers `None`), so the deck is tagged the way a mapped bridge surface actually is:
    // a real polygon class PLUS the `bridge` that says why it is raised. The rule under
    // test reads `OsmZ`, so that pair is exactly the condition.
    let deck = ring(1, 18.060, 59.330, 0.020, &[("landuse", "grass"), ("bridge", "yes"), ("layer", "1")]);
    let ground = ring(2, 18.060, 59.330, 0.030, &[("landuse", "grass")]);
    assert!(deck.z_order.bridge, "fixture: the deck says WHY it is raised");
    assert!(
        facett_map::way_rank(&deck) > facett_map::way_rank(&ground),
        "a bridge deck AREA must still outrank the ground it spans — deck {} vs ground {}. \
         The GG #6 rule ignores a BARE layer on an area; an area that declares a bridge or \
         a tunnel is saying what it is, and that is the case `layer` exists for.",
        facett_map::way_rank(&deck),
        facett_map::way_rank(&ground),
    );
}