main
Colby Knox AI Flow 0.2.78: bottom fill under any number; bridge 1.13.87 note 212acc2
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635
//! The Fusion lane: a run whose board is live in Autodesk Fusion instead of KiCad.
//!
//! The run names its board `fusion:<exact board document name>` (`start --remote-board fusion:<doc>`).
//! Every `kicad_*` verb the flow sends for that board is answered here by the Fusion bridge's own
//! verbs (fusion_routing_state, fusion_route_net, fusion_move_parts, fusion_place_vias,
//! fusion_apply_pours, fusion_pour_state, fusion_routing_drc, the view and window verbs), with the
//! replies reshaped into the KiCad bridge's, so planning, landing, the ledger, the gates and the
//! capture work unchanged. Fusion is the board of record: every read is a fresh Fusion export, every
//! mutation is guarded by Fusion's board id and revision and read back.
//!
//! Coordinates: the plan is in the KiCad frame (y down, see aiflow_board::eagle); Fusion is y up, so
//! y is negated both ways here and nowhere else. Rotation is the same number in both.
use serde_json::{json, Value};
use std::collections::HashMap;
use std::sync::Mutex;

pub const PREFIX: &str = "fusion:";
static BOARD_ID: Mutex<Option<String>> = Mutex::new(None);
static DOC: Mutex<Option<String>> = Mutex::new(None);

/// The Fusion document a flow board name points at, if it is a Fusion board.
pub fn doc_of(board: &str) -> Option<String> { board.strip_prefix(PREFIX).map(str::to_string) }

/// Set for a Fusion run (so verbs that carry no board, like the window and viewer verbs, go to Fusion too).
pub fn set_run_board(board: &str) { if let Some(d) = doc_of(board) { *DOC.lock().unwrap() = Some(d); } }
pub fn active() -> Option<String> { DOC.lock().unwrap().clone() }

/// Issue #203 (Zoltán): the board's bottom copper number. 16 on older Fusion boards, 304 (named "Bottom") in
/// Fusion's newer board format. Read from the live board source once per command (`ensure_bottom`);
/// AIFLOW_FUSION_BOTTOM overrides.
static BOTTOM: Mutex<Option<i64>> = Mutex::new(None);

pub fn set_bottom(n: i64) { *BOTTOM.lock().unwrap() = Some(n); }

/// The bottom copper number of a live board source, remembered for this command.
pub fn note_board_source(text: &str) { if let Some(n) = aiflow_board::eagle::bottom_number(text) { set_bottom(n); } }

fn bottom() -> i64 {
    if let Some(n) = std::env::var("AIFLOW_FUSION_BOTTOM").ok().and_then(|v| v.trim().parse::<i64>().ok()) { return n; }
    BOTTOM.lock().unwrap().unwrap_or(16)
}

/// Read the board's bottom copper number from the live board when this command has not yet.
fn ensure_bottom(doc: &str, call: &dyn Fn(&str, &Value) -> Value) {
    if BOTTOM.lock().unwrap().is_some() || std::env::var("AIFLOW_FUSION_BOTTOM").is_ok() { return; }
    let r = call("fusion_board_source", &json!({"expectDocument": doc}));
    match r["source"].as_str() { Some(t) => note_board_source(t), None => set_bottom(16) }
}

fn is_bottom(n: i64) -> bool { n == bottom() || n == 16 || n == 304 }

/// A flow layer as the bridge takes it: top and bottom by Fusion's number, an inner layer by the
/// standard's name, which the bridge maps through the board's own stack (fusion-bridge 1.13+). None
/// for anything that is not copper.
fn layer_arg(l: &Value) -> Option<Value> {
    let text = l.as_str().map(str::to_string).or_else(|| l.as_i64().map(|n| n.to_string()))?;
    match text.as_str() {
        "F.Cu" | "1" => Some(json!(1)),
        "B.Cu" | "16" | "304" => Some(json!(bottom())),
        s => {
            if let Ok(n) = s.parse::<i64>() { return (2..=15).contains(&n).then(|| json!(n)); }
            let n: u32 = s.strip_prefix("In")?.strip_suffix(".Cu")?.parse().ok()?;
            (n >= 1).then(|| json!(s))
        }
    }
}

/// The inner layers a Fusion reply names: Fusion's layer number to the standard's name, from the
/// standard's own items beside the planner's (tracks and zones carry both layer and layerId).
fn layer_names(f: &Value) -> HashMap<i64, String> {
    let mut names = HashMap::new();
    for key in ["tracks", "zones"] {
        for item in f[key].as_array().into_iter().flatten() {
            if let (Some(n), Some(l)) = (item["layerId"].as_i64(), item["layer"].as_str()) { names.insert(n, l.to_string()); }
        }
    }
    names
}

/// One copper layer's standard name. A bridge too old to name it gets the bridge's own default.
fn layer_label(n: i64, names: &HashMap<i64, String>) -> String {
    match n { 1 => "F.Cu".into(), n if is_bottom(n) => "B.Cu".into(), _ => names.get(&n).cloned().unwrap_or_else(|| format!("In{}.Cu", n - 1)) }
}

/// The id a Fusion pour has in the zone state and the plan board (aiflow_board::eagle::zone_uuid).
fn pour_zone_id(p: &Value) -> String {
    aiflow_board::eagle::zone_uuid(p["net"].as_str().unwrap_or(""), p["layer"].as_i64().unwrap_or(1), &outline_points(&p["outline"]))
}

fn outline_points(v: &Value) -> Vec<(f64, f64)> {
    v.as_array().into_iter().flatten().filter_map(|p| Some((p[0].as_f64()?, p[1].as_f64()?))).collect()
}

/// A pour's layer as the plan sent it, under the name the zone state uses.
fn plan_layer(l: &Value, names: &HashMap<i64, String>) -> String {
    l.as_i64().map(|n| layer_label(n, names)).or_else(|| l.as_str().map(str::to_string)).unwrap_or_default()
}

/// The same outline within the bridge's own tolerance, whichever vertex each starts at.
fn same_outline(a: &Value, b: &Value) -> bool {
    let sorted = |v: &Value| { let mut p = outline_points(v); p.sort_by(|x, y| x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal)); p };
    let (a, b) = (sorted(a), sorted(b));
    a.len() == b.len() && a.iter().zip(&b).all(|(p, q)| (p.0 - q.0).abs() <= 0.011 && (p.1 - q.1).abs() <= 0.011)
}

/// Every layer a route names that is not copper; these are refused, never drawn on a default layer.
fn bad_route_layers(args: &Value) -> Vec<String> {
    let mut out = Vec::new();
    let mut look = |l: &Value| { if !l.is_null() && layer_arg(l).is_none() { out.push(l.to_string()); } };
    look(&args["layer"]);
    for w in args["points"].as_array().into_iter().flatten() { look(&w["layer"]); }
    for p in args["paths"].as_array().into_iter().flatten() {
        for w in p.as_array().into_iter().flatten() { look(&w["layer"]); }
    }
    out
}
fn flip(p: &Value) -> Value { json!([p[0].as_f64().unwrap_or(0.0), -p[1].as_f64().unwrap_or(0.0)]) }

/// A flow waypoint ("R1.1", [x,y], {x,y,layer} or {pad,layer}) as the Fusion bridge's waypoint.
fn waypoint(w: &Value) -> Value {
    if let Some(pad) = w.as_str() { return json!({"pad": pad}); }
    if w.is_array() { return flip(w); }
    let mut o = serde_json::Map::new();
    if let Some(pad) = w["pad"].as_str() { o.insert("pad".into(), json!(pad)); }
    if let (Some(x), Some(y)) = (w["x"].as_f64(), w["y"].as_f64()) { o.insert("x".into(), json!(x)); o.insert("y".into(), json!(-y)); }
    if let Some(l) = layer_arg(&w["layer"]) { o.insert("layer".into(), l); }
    Value::Object(o)
}

/// The live board in the KiCad bridge's kicad_routing_state shape (plan frame).
fn item_id(net: &Value, kind: char, i: usize) -> String { format!("fusion:{}:{kind}{i}", net.as_str().unwrap_or("")) }

/// The net an item id names (`fusion:<net>:s3`); nets may hold ':' so split from the right.
fn net_of_id(id: &str) -> Option<String> { id.strip_prefix("fusion:").and_then(|r| r.rsplit_once(':')).map(|(n, _)| n.to_string()) }

pub fn to_kicad_state(f: &Value) -> Value {
    if f["success"].as_bool() != Some(true) { return f.clone(); }
    if let Some(id) = f["boardId"].as_str() { *BOARD_ID.lock().unwrap() = Some(id.to_string()); }
    // item ids name the net: Fusion removes copper a whole net at a time (fusion_ripup_nets)
    // every copper layer, inner ones by the standard's name (a wire on any other layer is not copper)
    let names = layer_names(f);
    let segs: Vec<Value> = f["segments"].as_array().cloned().unwrap_or_default().iter().filter(|s| s["layer"].as_i64().is_some_and(|n| (1..=16).contains(&n))).enumerate().map(|(i, s)| json!({
        "start": {"x": s["start"][0], "y": -s["start"][1].as_f64().unwrap_or(0.0)}, "end": {"x": s["end"][0], "y": -s["end"][1].as_f64().unwrap_or(0.0)},
        "layer": layer_label(s["layer"].as_i64().unwrap_or(1), &names), "net_name": s["net"], "width": s["width"], "uuid": item_id(&s["net"], 's', i)})).collect();
    let vias: Vec<Value> = f["vias"].as_array().cloned().unwrap_or_default().iter().enumerate().map(|(i, v)| json!({
        "position": {"x": v["at"][0], "y": -v["at"][1].as_f64().unwrap_or(0.0)}, "net_name": v["net"], "size": v["diameter"], "drill": v["drill"], "uuid": item_id(&v["net"], 'v', i)})).collect();
    // fusion-bridge 1.12+ returns the EDA Bridge Standard's pads (name is the pad's own name, ref the
    // part) with the planner's "REF.PAD" under key; older bridges put "REF.PAD" in name
    let pads: Vec<Value> = f["pads"].as_array().cloned().unwrap_or_default().iter().map(|p| json!({
        "name": if p["key"].is_string() { p["key"].clone() } else { p["name"].clone() }, "netName": p["net"].as_str().unwrap_or(""), "x": p["at"][0], "y": -p["at"][1].as_f64().unwrap_or(0.0)})).collect();
    // unconnected from the copper itself (1.12+) is right even before Fusion recomputes its ratsnest;
    // airwireCount is Fusion's ratsnest as exported
    let unconnected = if f["unconnected"].is_u64() { f["unconnected"].clone() } else { f["airwireCount"].clone() };
    json!({"success": true, "revision": f["revision"], "boardId": f["boardId"], "segments": segs, "vias": vias, "pads": pads,
           "unconnected": unconnected, "remainingNets": f["remainingNets"], "elements": f["elements"], "source": "fusion"})
}

/// Answer a kicad_* verb for a Fusion board through the Fusion bridge. `call` sends one verb.
pub fn translate(verb: &str, args: &Value, doc: &str, call: &dyn Fn(&str, &Value) -> Value) -> Value {
    // issue #203: the verbs that name copper layers learn the board's bottom number first
    if matches!(verb, "kicad_routing_state" | "kicad_route_net" | "kicad_apply_zone_plan" | "kicad_zone_state" | "kicad_routing_validate") { ensure_bottom(doc, call); }
    let state = || call("fusion_routing_state", &json!({"expectDocument": doc}));
    let ids = |st: &Value| (st["boardId"].as_str().unwrap_or("").to_string(), st["revision"].as_str().unwrap_or("").to_string());
    let guarded = |rev: Option<&str>| -> Result<(String, String), Value> {
        let st = state();
        if st["success"].as_bool() != Some(true) { return Err(st); }
        let (id, live) = ids(&st);
        if let Some(r) = rev { if !r.is_empty() && r != live { return Err(json!({"success": false, "errorCode": "stale_board", "error": "the live Fusion board changed since the revision this step read", "revision": live})); } }
        Ok((id, live))
    };
    match verb {
        "kicad_routing_state" => to_kicad_state(&state()),
        "kicad_route_net" => {
            let bad = bad_route_layers(args);
            if !bad.is_empty() { return json!({"success": false, "errorCode": "unknown_layer", "error": format!("{} is not a copper layer: use F.Cu, In<n>.Cu or B.Cu. Nothing was routed.", bad.join(", "))}); }
            let (id, rev) = match guarded(args["expectedRevision"].as_str()) { Ok(x) => x, Err(e) => return e };
            // clearance is a floor the bridge raises to the board's own rules; without one it
            // assumes 0.2 mm, which refused legal 0.15 mm copper the KiCad gate had passed
            let mut a = json!({"expectDocument": doc, "boardId": id, "expectedRevision": rev, "net": args["net"], "width": args["width"].as_f64().unwrap_or(0.25), "clearance": args["clearance"].as_f64().unwrap_or(0.0001)});
            if let Some(l) = layer_arg(&args["layer"]) { a["layer"] = l; }
            if let Some(v) = args["viaSize"].as_f64() { a["viaDiameter"] = json!(v); }
            if let Some(v) = args["viaDrill"].as_f64() { a["viaDrill"] = json!(v); }
            if let Some(ps) = args["paths"].as_array() { a["paths"] = json!(ps.iter().map(|p| json!(p.as_array().cloned().unwrap_or_default().iter().map(waypoint).collect::<Vec<_>>())).collect::<Vec<_>>()); }
            else if let Some(ps) = args["points"].as_array() { a["points"] = json!(ps.iter().map(waypoint).collect::<Vec<_>>()); }
            let mut r = call("fusion_route_net", &a);
            if r["success"].as_bool() == Some(true) { r["revision"] = r["revision"].clone(); }
            r
        }
        "kicad_move_footprint" => {
            let (id, rev) = match guarded(args["expectedRevision"].as_str()) { Ok(x) => x, Err(e) => return e };
            let parts: Vec<Value> = args["refs"].as_array().cloned().unwrap_or_default().iter().map(|m| json!({"ref": m["ref"], "x": m["x"], "y": -m["y"].as_f64().unwrap_or(0.0), "rotation": if m["eagleRot"].is_string() { m["eagleRot"].clone() } else { json!(m["rotation"].as_f64().unwrap_or(0.0)) }})).collect();
            call("fusion_move_parts", &json!({"expectDocument": doc, "boardId": id, "expectedRevision": rev, "parts": parts}))
        }
        "kicad_apply_zone_plan" => {
            let (id, rev) = match guarded(args["expectedRevision"].as_str()) { Ok(x) => x, Err(e) => return e };
            let mut pours = Vec::new(); let mut vias = Vec::new(); let mut skipped = Vec::new();
            for z in args["zones"].as_array().cloned().unwrap_or_default() {
                if z["type"].as_str() == Some("keepout") { skipped.push(json!({"name": z["name"], "why": "keepout zones are not landed in Fusion; the pours are drawn clear of them"})); continue; }
                if z["kind"].as_str() == Some("via") || z.get("via").is_some() { vias.push(z); continue; }
                let pts: Vec<Value> = z["polygon"].as_array().cloned().unwrap_or_default().iter().map(flip).collect();
                if pts.len() < 3 { skipped.push(json!({"name": z["name"], "why": "no polygon (an outline pour must be resolved to points before landing)"})); continue; }
                let prio = z["priority"].as_i64().unwrap_or(0);
                for l in z["layers"].as_array().cloned().unwrap_or_default() {
                    let Some(ln) = layer_arg(&l) else {
                        return json!({"success": false, "errorCode": "unknown_layer", "error": format!("{} wants a pour on {l}, which is not a copper layer: use F.Cu, In<n>.Cu or B.Cu. Nothing was landed.", z["name"].as_str().unwrap_or("A zone"))});
                    };
                    pours.push(json!({"net": z["net"], "layer": ln, "points": pts, "isolate": z["clearance"].as_f64().unwrap_or(0.25), "rank": (6 - prio).clamp(1, 6), "width": z["minThickness"].as_f64().unwrap_or(0.25), "thermals": z["connection"].as_str() != Some("solid")}));
                }
            }
            let owned: Vec<String> = args["ownedItemIds"].as_array().into_iter().flatten().filter_map(|v| v.as_str().map(str::to_string)).collect();
            if pours.is_empty() && owned.is_empty() { return json!({"success": true, "mutated": false, "skipped": skipped, "note": "nothing to pour in Fusion"}); }
            let before = call("fusion_pour_state", &json!({"expectDocument": doc}));
            if before["success"].as_bool() != Some(true) { return before; }
            let names = layer_names(&before);
            let in_plan = |p: &Value| pours.iter().any(|q| q["net"] == p["net"] && plan_layer(&q["layer"], &names) == layer_label(p["layer"].as_i64().unwrap_or(1), &names) && same_outline(&q["points"], &p["outline"]));
            let rows = before["pours"].as_array().cloned().unwrap_or_default();
            let was_here: Vec<String> = rows.iter().map(pour_zone_id).collect();
            let stale: Vec<Value> = rows.iter().filter(|p| !in_plan(p) && owned.contains(&pour_zone_id(p))).cloned().collect();
            let stale_ids: Vec<String> = stale.iter().map(pour_zone_id).collect();
            // the old pour goes first: Fusion's DELETE picks by a point on the outline, so with a new pour
            // sharing that point it can take the new one instead (and the bridge's count still matches)
            let mut rev = rev;
            let mut id = id;
            if !stale.is_empty() {
                let targets: Vec<Value> = stale.iter().map(|p| match p["id"].as_str() { Some(i) => json!({"id": i}), None => json!({"net": p["net"], "layer": p["layer"], "points": p["outline"]}) }).collect();
                let gone = call("fusion_remove_pours", &json!({"expectDocument": doc, "boardId": id, "expectedRevision": rev, "pours": targets}));
                if gone["success"].as_bool() != Some(true) {
                    let why = gone["error"].as_str().unwrap_or("no reason given").to_string();
                    return json!({"success": false, "mutated": gone["mutated"], "errorCode": gone["errorCode"], "error": format!("{} old pour(s) the plan no longer describes could not be removed, so nothing new was landed: {why}", stale.len()), "removeReply": gone});
                }
                (id, rev) = match guarded(None) { Ok(x) => x, Err(e) => return e };
            }
            let mut r = if pours.is_empty() { json!({"success": true, "mutated": !stale.is_empty()}) } else { call("fusion_apply_pours", &json!({"expectDocument": doc, "boardId": id, "expectedRevision": rev, "pours": pours})) };
            r["skipped"] = json!(skipped);
            if !stale.is_empty() { r["mutated"] = json!(true); r["removedIds"] = json!(stale_ids); }
            if r["success"].as_bool() != Some(true) {
                // the plan did not land and the old copper is gone: put the old pours back
                if !stale.is_empty() {
                    let back: Vec<Value> = stale.iter().map(|p| json!({"net": p["net"], "layer": p["layer"], "points": p["outline"], "isolate": p["isolate"], "rank": p["rank"], "width": p["width"], "thermals": p["thermals"], "orphans": p["orphans"]})).collect();
                    let restored = guarded(None).map(|(id, rev)| call("fusion_apply_pours", &json!({"expectDocument": doc, "boardId": id, "expectedRevision": rev, "pours": back})));
                    let ok = matches!(&restored, Ok(v) if v["success"].as_bool() == Some(true));
                    if let Some(o) = r.as_object_mut() { if ok { o.remove("removedIds"); } }
                    r["oldPoursRestored"] = json!(ok);
                }
                return r;
            }
            let after = call("fusion_pour_state", &json!({"expectDocument": doc}));
            if after["success"].as_bool() != Some(true) { r["note"] = json!("the pours landed, but the pour state could not be read back"); return r; }
            let rows = after["pours"].as_array().cloned().unwrap_or_default();
            // read the result back by id: the bridge's own check counts pours per net and layer
            let still: Vec<&String> = stale_ids.iter().filter(|i| rows.iter().any(|p| pour_zone_id(p) == **i)).collect();
            let missing: Vec<&Value> = pours.iter().filter(|q| !rows.iter().any(|p| q["net"] == p["net"] && plan_layer(&q["layer"], &names) == layer_label(p["layer"].as_i64().unwrap_or(1), &names) && same_outline(&q["points"], &p["outline"]))).collect();
            if !still.is_empty() || !missing.is_empty() {
                return json!({"success": false, "mutated": true, "errorCode": "readback_mismatch", "error": format!("after landing, {} old pour(s) are still on the board and {} planned pour(s) are not", still.len(), missing.len()), "stillThere": still, "missing": missing.iter().map(|q| q["net"].clone()).collect::<Vec<_>>(), "pours": rows});
            }
            // the run owns what it drew and what it owned already; a pour the board started with stays the board's, even when it matches
            r["itemIds"] = json!(rows.iter().filter(|p| in_plan(p)).map(pour_zone_id).filter(|i| !was_here.contains(i) || owned.contains(i)).collect::<Vec<_>>());
            if !stale.is_empty() { r["deletionVerified"] = json!(true); }
            r
        }
        "kicad_zone_state" => {
            let r = call("fusion_pour_state", &json!({"expectDocument": doc}));
            if r["success"].as_bool() != Some(true) { return r; }
            let by = &r["filledAreaByLayerMm2"];
            let names = layer_names(&r);
            // every copper layer's fill, inner ones by the standard's name; top and bottom always reported
            let mut filled = serde_json::Map::new();
            filled.insert("F.Cu".into(), json!(by["1"].as_f64().unwrap_or(0.0)));
            // the bottom under whichever number the reply keys it (the board's own, 16, or Fusion's raw 304; fusion-bridge 1.13.87 reports 16)
            let bot: f64 = { let mut keys = vec![bottom().to_string(), "16".to_string(), "304".to_string()]; keys.dedup(); keys.iter().filter_map(|k| by[k.as_str()].as_f64()).sum() };
            filled.insert("B.Cu".into(), json!(bot));
            for (k, v) in by.as_object().into_iter().flatten() {
                if let Ok(n) = k.parse::<i64>() { if n != 1 && !is_bottom(n) { filled.insert(layer_label(n, &names), json!(v.as_f64().unwrap_or(0.0))); } }
            }
            let zones: Vec<Value> = r["pours"].as_array().cloned().unwrap_or_default().iter().map(|p| { let l = layer_label(p["layer"].as_i64().unwrap_or(1), &names);
                // KiCad's zone shape, which measure and the analysis read: type copper, layers[], fills[]
                json!({"id": pour_zone_id(p), "name": format!("{} pour", p["net"].as_str().unwrap_or("")), "net": p["net"], "layer": l, "layers": [l], "type": "copper", "filledMm2": p["filledMm2"], "filled": p["filled"], "fills": [{"layer": l, "areaMm2": p["filledMm2"]}]}) }).collect();
            json!({"success": true, "revision": r["revision"], "revisionUsable": true, "filledAreaByLayerMm2": filled, "zones": zones, "unfilled": r["unfilled"], "readbackEvidence": "Fusion's own fill (polygonfilldetails) from a fresh export"})
        }
        "kicad_routing_validate" => {
            let (id, rev) = match guarded(None) { Ok(x) => x, Err(e) => return e };
            let r = call("fusion_routing_drc", &json!({"expectDocument": doc, "boardId": id, "expectedRevision": rev}));
            let d = &r["drc"];
            json!({"success": r["success"], "unconnected": d["airwireCount"].as_u64().unwrap_or(r["airwireCount"].as_u64().unwrap_or(0)), "errors": d["errorCount"], "warnings": d["warningCount"], "clean": d["clean"], "drc": d, "source": "Autodesk DRC in Fusion"})
        }
        "kicad_model_check" => {
            let st = state();
            if st["success"].as_bool() != Some(true) { return st; }
            let els = st["elements"].as_array().cloned().unwrap_or_default();
            let with: Vec<&Value> = els.iter().filter(|e| e["has3d"].as_bool() == Some(true)).collect();
            let without: Vec<Value> = els.iter().filter(|e| e["has3d"].as_bool() != Some(true)).map(|e| json!({"footprint": e["package"], "references": [e["name"]], "models": [{"reference": "(no 3D package bound in the library)", "exists": false}]})).collect();
            json!({"success": true, "data": {"counts": {"withModel": with.len(), "missingModel": without.len(), "unresolvedModel": 0, "noModelExpected": 0}, "footprintsWithWarnings": without, "source": "Fusion: each element's bound package3d"}})
        }
        "kicad_remove_route" => {
            // Fusion takes copper up a whole net at a time: the ids must name every track and via
            // of each net they touch, or nothing is removed
            let st = state();
            if st["success"].as_bool() != Some(true) { return st; }
            let (id, live) = ids(&st);
            if let Some(r) = args["expectedRevision"].as_str() { if !r.is_empty() && r != live { return json!({"success": false, "errorCode": "stale_board", "error": "the live Fusion board changed since the revision this step read", "revision": live}); } }
            let want: Vec<String> = args["itemIds"].as_array().cloned().unwrap_or_default().iter().filter_map(|v| v.as_str().map(str::to_string)).collect();
            let mut nets: Vec<String> = want.iter().filter_map(|i| net_of_id(i)).collect();
            nets.sort(); nets.dedup();
            if nets.is_empty() { return json!({"success": false, "errorCode": "no_item_ids", "error": "no Fusion item ids (fusion:<net>:s<n>) to remove"}); }
            let k = to_kicad_state(&st);
            let all: Vec<String> = k["segments"].as_array().into_iter().flatten().chain(k["vias"].as_array().into_iter().flatten())
                .filter(|o| nets.iter().any(|n| o["net_name"].as_str() == Some(n))).filter_map(|o| o["uuid"].as_str().map(str::to_string)).collect();
            let partial: Vec<&String> = all.iter().filter(|i| !want.contains(i)).collect();
            if !partial.is_empty() { return json!({"success": false, "errorCode": "partial_net_unsupported", "error": format!("Fusion removes whole nets; {} item(s) of {:?} were not named", partial.len(), nets), "_hint": "Name every track and via of the net (adom-aiflow unroute --net <net> does), or leave it."}); }
            let r = call("fusion_ripup_nets", &json!({"expectDocument": doc, "boardId": id, "expectedRevision": live, "nets": nets}));
            if r["success"].as_bool() == Some(true) { json!({"success": true, "removed": want.len(), "nets": nets, "revision": r["revision"]}) } else { r }
        }
        "kicad_status" | "kicad_version" => call("fusion_readiness", &json!({})),
        "kicad_open_board" | "kicad_window_info" => {
            // only opening the board switches the view; asking for the window (every landing does) must not,
            // or each call re-raises the bridge's "Switching to the 2D board" panel over Fusion
            if verb == "kicad_open_board" { let _ = call("fusion_show_2d_board", &json!({"documentName": doc})); }
            let hwnd = main_hwnd(call);
            if verb == "kicad_open_board" { json!({"success": hwnd.is_some(), "hwnd": hwnd}) }
            else { json!({"success": true, "data": {"editors": hwnd.map(|h| vec![json!({"kind": "pcb_editor", "hwnd": h}), json!({"kind": "3d_viewer", "hwnd": h})]).unwrap_or_default()}}) }
        }
        // "Zoom to Selected Objects" after a net select: the select already framed the board, and a WINDOW
        // now would end the open SHOW and drop the highlight
        "kicad_send_key" if args["menu"].as_str() == Some("Zoom to Selected Objects") => json!({"success": true, "note": "framed by the select (Fusion keeps the highlight only while SHOW is open)"}),
        "kicad_send_key" => { fit_board(doc, call); json!({"success": true}) }
        "kicad_select_net" => {
            // Fusion highlights a net with SHOW; what was lit is counted from the board source, the way
            // KiCad's select answers, so the walkthrough knows a net with no copper from a lit one
            let net = args["net"].as_str().unwrap_or("");
            let src = call("fusion_board_source", &json!({"expectDocument": doc}));
            let text = src["source"].as_str().or_else(|| src["data"]["source"].as_str()).unwrap_or("").to_string();
            let sig = text.split(&format!("<signal name=\"{net}\"")).nth(1).map(|t| t[..t.find("</signal>").unwrap_or(t.len())].to_string()).unwrap_or_default();
            let (wires, polys, vias, pads) = (sig.matches("<wire ").count(), sig.matches("<polygon ").count(), sig.matches("<via ").count(), sig.matches("<contactref ").count());
            // no closing semicolon: Fusion keeps a SHOW highlight only while the command stays open (checked
            // live 2026-10-07; `SHOW 'X';` lit nothing). The next SHOW replaces it, `SHOW;` clears it.
            let arg = if net.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '+' || c == '.') { net.to_string() } else { format!("'{net}'") };
            fit_board(doc, call);  // frame first: any later command would end the SHOW and its highlight
            let r = call("fusion_electron_run", &json!({"command": format!("SHOW {arg}")}));
            json!({"success": r["success"], "selected": wires + polys + vias + pads, "byKind": {"tracks": wires, "zones": polys, "vias": vias, "pads": pads}, "error": r["error"]})
        }
        "kicad_select_clear" => call("fusion_electron_run", &json!({"command": "SHOW;"})),
        "kicad_open_3d_viewer" => { let r = call("fusion_show_3d_board", &json!({"documentName": doc})); json!({"success": r["success"], "hwnd": main_hwnd(call)}) }
        "kicad_close_3d_viewer" => call("fusion_show_2d_board", &json!({"documentName": doc})),
        "kicad_3d_view" => {
            // the scripted walkthrough, through Fusion's own camera
            let mut log = Vec::new();
            for (a, extra) in [("iso", json!({})), ("orbit", json!({"degrees": 60})), ("orbit", json!({"degrees": 60})), ("orbit", json!({"degrees": 60})), ("zoom", json!({"factor": 1.8})), ("orbit", json!({"degrees": 90})), ("fit", json!({}))] {
                let mut x = extra.clone(); x["action"] = json!(a);
                log.push(call("fusion_view", &x)["success"].clone());
                std::thread::sleep(std::time::Duration::from_millis(args["pauseMs"].as_u64().unwrap_or(700)));
            }
            json!({"success": true, "steps": log})
        }
        "kicad_screenshot_all" => {
            // Fusion's viewport paints whenever it is shown; report the main window as rendered
            let h = main_hwnd(call);
            json!({"success": true, "data": {"screenshots": h.map(|h| vec![json!({"hwnd": h, "canvas": {"rendered": true, "distinctColors": 64}})]).unwrap_or_default()}})
        }
        other => json!({"success": false, "errorCode": "unsupported_in_fusion", "error": format!("{other} has no Fusion lane equivalent yet"), "_hint": "File it on adom/adom-aiflow with the step that needed it."}),
    }
}

/// Hydrogen's main window on the box (the human's workspace: chat, webviews).
pub fn hydrogen_hwnd(call: &dyn Fn(&str, &Value) -> Value) -> Option<i64> {
    let w = call("desktop_list_windows", &json!({}));
    let out = w.get("output").and_then(|o| o.as_str()).and_then(|s| serde_json::from_str::<Value>(s).ok()).unwrap_or(w.clone());
    let list = out["data"]["windows"].as_array().or(out["windows"].as_array()).cloned().unwrap_or_default();
    list.iter().find(|x| x["image"].as_str() == Some("hydrogen.exe") && x["kind"].as_str() == Some("normal")).and_then(|x| x["hwnd"].as_i64())
}

/// Frame the board, not everything: Fusion's own fit includes any stray item off the board (a part not
/// placed yet, a text), which shrinks the board to a thumbnail. The view is set to the board outline
/// (the dimension layer) with a margin; plain fit when the outline cannot be read.
pub fn fit_board(doc: &str, call: &dyn Fn(&str, &Value) -> Value) -> bool {
    let src = call("fusion_board_source", &json!({"expectDocument": doc}));
    let text = src["source"].as_str().or_else(|| src["data"]["source"].as_str()).unwrap_or("");
    let mut xs = Vec::new(); let mut ys = Vec::new();
    for w in text.split("<wire ").skip(1) {
        let w = &w[..w.find('>').unwrap_or(w.len())];
        if !w.contains("layer=\"20\"") { continue; }
        let num = |k: &str| w.split(&format!("{k}=\"")).nth(1).and_then(|r| r.split('"').next()).and_then(|v| v.parse::<f64>().ok());
        for (a, b) in [("x1", "y1"), ("x2", "y2")] { if let (Some(x), Some(y)) = (num(a), num(b)) { xs.push(x); ys.push(y); } }
    }
    if xs.is_empty() { let _ = call("fusion_view", &json!({"action": "fit", "space": "2d"})); return false; }
    let (x0, x1) = (xs.iter().cloned().fold(f64::MAX, f64::min), xs.iter().cloned().fold(f64::MIN, f64::max));
    let (y0, y1) = (ys.iter().cloned().fold(f64::MAX, f64::min), ys.iter().cloned().fold(f64::MIN, f64::max));
    let m = ((x1 - x0).max(y1 - y0) * 0.12).max(1.0);
    let cmd = format!("WINDOW ({:.3}mm {:.3}mm) ({:.3}mm {:.3}mm);", x0 - m, y0 - m, x1 + m, y1 + m);
    call("fusion_electron_run", &json!({"command": cmd}))["success"].as_bool().unwrap_or(false)
}

/// Fusion's main window on the test box.
pub fn main_hwnd(call: &dyn Fn(&str, &Value) -> Value) -> Option<i64> {
    let w = call("desktop_list_windows", &json!({}));
    let out = w.get("output").and_then(|o| o.as_str()).and_then(|s| serde_json::from_str::<Value>(s).ok()).unwrap_or(w.clone());
    let list = out["data"]["windows"].as_array().or(out["windows"].as_array()).cloned().unwrap_or_default();
    list.iter().find(|x| x["image"].as_str() == Some("Fusion360.exe") && x["kind"].as_str() == Some("normal") && x["title"].as_str().map(|t| t.contains("Autodesk Fusion")).unwrap_or(false)).and_then(|x| x["hwnd"].as_i64())
}

#[cfg(test)]
mod tests {
    use super::*;
    /// Issue #142: a bottom part's move carries the MR angle land_moves worked out; a plain number would flip it.
    #[test]
    fn a_bottom_part_is_sent_its_mirrored_rotation() {
        let sent = std::cell::RefCell::new(Vec::<(String, Value)>::new());
        let call = |v: &str, a: &Value| -> Value { sent.borrow_mut().push((v.to_string(), a.clone())); json!({"success": true, "boardId": "B", "revision": "r1"}) };
        let _ = translate("kicad_move_footprint", &json!({"expectedRevision": "r1", "refs": [{"ref": "U1", "x": 10.0, "y": -8.0, "rotation": 90.0, "eagleRot": "MR270"}]}), "Doc-brd", &call);
        let (v, a) = sent.borrow().last().cloned().unwrap();
        assert_eq!(v, "fusion_move_parts");
        assert_eq!(a["parts"][0], json!({"ref": "U1", "x": 10.0, "y": 8.0, "rotation": "MR270"}));
    }
    #[test]
    fn route_net_waypoints_and_moves_flip_y_and_keep_rotation() {
        let sent = std::cell::RefCell::new(Vec::<(String, Value)>::new());
        let call = |v: &str, a: &Value| -> Value {
            sent.borrow_mut().push((v.to_string(), a.clone()));
            match v { "fusion_routing_state" => json!({"success": true, "boardId": "B", "revision": "r1", "segments": [], "vias": [], "pads": [{"name": "R1.1", "net": "A", "at": [5.0, 4.5]}]}), _ => json!({"success": true, "revision": "r2"}) }
        };
        let st = translate("kicad_routing_state", &json!({}), "Doc-brd", &call);
        assert_eq!(st["pads"][0]["y"], json!(-4.5));
        let r = translate("kicad_route_net", &json!({"expectedRevision": "r1", "net": "A", "width": 0.3, "layer": "F.Cu", "paths": [["R1.1", [6.0, -4.5], {"x": 6.0, "y": -2.0, "layer": "B.Cu"}]]}), "Doc-brd", &call);
        assert_eq!(r["success"], true);
        let (v, a) = sent.borrow().last().cloned().unwrap();
        assert_eq!(v, "fusion_route_net");
        assert_eq!(a["paths"][0], json!([{"pad": "R1.1"}, [6.0, 4.5], {"x": 6.0, "y": 2.0, "layer": 16}]));
        assert_eq!(a["boardId"], "B"); assert_eq!(a["layer"], 1);
        let _ = translate("kicad_move_footprint", &json!({"expectedRevision": "r1", "refs": [{"ref": "U1", "x": 10.0, "y": -8.0, "rotation": 90.0}]}), "Doc-brd", &call);
        let (v, a) = sent.borrow().last().cloned().unwrap();
        assert_eq!(v, "fusion_move_parts"); assert_eq!(a["parts"][0], json!({"ref": "U1", "x": 10.0, "y": 8.0, "rotation": 90.0}));
        // a stale revision is refused before anything is sent
        let n = sent.borrow().len();
        let r = translate("kicad_move_footprint", &json!({"expectedRevision": "old", "refs": []}), "Doc-brd", &call);
        assert_eq!(r["errorCode"], "stale_board"); assert_eq!(sent.borrow().len(), n + 1, "only the state read went out");
    }
    /// A bridge stub recording what was sent: a four-layer board with an AGND pour on layer 15 (In2.Cu).
    fn four_layer(sent: &std::cell::RefCell<Vec<(String, Value)>>, v: &str, a: &Value) -> Value {
        sent.borrow_mut().push((v.to_string(), a.clone()));
        match v {
            "fusion_routing_state" => json!({"success": true, "boardId": "B", "revision": "r1", "vias": [], "pads": [],
                "segments": [{"net": "A", "layer": 15, "width": 0.2, "start": [0.0, 1.0], "end": [2.0, 1.0]}, {"net": "A", "layer": 1, "width": 0.2, "start": [0.0, 0.0], "end": [1.0, 0.0]}, {"net": "", "layer": 19, "width": 0.0, "start": [0.0, 0.0], "end": [1.0, 0.0]}],
                "tracks": [{"layer": "In2.Cu", "layerId": 15, "net": "A"}]}),
            "fusion_pour_state" => json!({"success": true, "revision": "r1", "filledAreaByLayerMm2": {"1": 10.0, "15": 40.5},
                "pours": [{"net": "GND", "layer": 1, "filledMm2": 10.0, "filled": true}, {"net": "AGND", "layer": 15, "filledMm2": 40.5, "filled": true}],
                "zones": [{"net": "GND", "layer": "F.Cu", "layerId": 1}, {"net": "AGND", "layer": "In2.Cu", "layerId": 15}], "unfilled": []}),
            _ => json!({"success": true, "revision": "r2"}),
        }
    }
    #[test]
    fn inner_copper_is_measured_and_read_under_its_own_name() {
        let sent = std::cell::RefCell::new(Vec::new());
        let call = |v: &str, a: &Value| four_layer(&sent, v, a);
        let z = translate("kicad_zone_state", &json!({}), "Doc-brd", &call);
        assert_eq!(z["filledAreaByLayerMm2"], json!({"F.Cu": 10.0, "B.Cu": 0.0, "In2.Cu": 40.5}), "an inner pour is never counted as top copper");
        assert_eq!(z["zones"][1]["layer"], "In2.Cu"); assert_eq!(z["zones"][1]["fills"][0]["layer"], "In2.Cu");
        let st = translate("kicad_routing_state", &json!({}), "Doc-brd", &call);
        let layers: Vec<&str> = st["segments"].as_array().unwrap().iter().map(|s| s["layer"].as_str().unwrap()).collect();
        assert_eq!(layers, ["In2.Cu", "F.Cu"], "inner tracks kept, the airwire layer is not copper");
        // a bridge too old to name layers still does not call an inner pour top copper
        assert_eq!(layer_label(15, &HashMap::new()), "In14.Cu");
    }
    #[test]
    fn inner_routes_go_to_the_bridge_by_name_and_a_bad_layer_is_refused() {
        let sent = std::cell::RefCell::new(Vec::new());
        let call = |v: &str, a: &Value| four_layer(&sent, v, a);
        let r = translate("kicad_route_net", &json!({"expectedRevision": "r1", "net": "A", "layer": "In1.Cu", "paths": [["R1.1", {"x": 1.0, "y": -1.0, "layer": "In2.Cu"}, {"x": 2.0, "y": -1.0, "layer": "F.Cu"}]]}), "Doc-brd", &call);
        assert_eq!(r["success"], true);
        let (v, a) = sent.borrow().last().cloned().unwrap();
        assert_eq!(v, "fusion_route_net"); assert_eq!(a["layer"], "In1.Cu");
        assert_eq!(a["paths"][0][1], json!({"x": 1.0, "y": 1.0, "layer": "In2.Cu"})); assert_eq!(a["paths"][0][2]["layer"], 1);
        let n = sent.borrow().len();
        for bad in [json!({"net": "A", "layer": "F.SilkS"}), json!({"net": "A", "layer": "F.Cu", "points": [{"x": 1.0, "y": 1.0, "layer": "In0.Cu"}]}), json!({"net": "A", "paths": [[{"pad": "R1.1", "layer": "17"}]]})] {
            let r = translate("kicad_route_net", &bad, "Doc-brd", &call);
            assert_eq!(r["errorCode"], "unknown_layer", "{bad}");
        }
        assert_eq!(sent.borrow().len(), n, "a refused route sends nothing, not even a default-layer draw");
    }
    #[test]
    fn an_inner_pour_lands_by_name_beside_the_outer_ones() {
        let sent = std::cell::RefCell::new(Vec::new());
        let landed = std::cell::RefCell::new(Vec::<Value>::new());
        let call = |v: &str, a: &Value| {
            let r = four_layer(&sent, v, a);
            if v == "fusion_apply_pours" {
                for p in a["pours"].as_array().into_iter().flatten() { landed.borrow_mut().push(pour_row(p["net"].as_str().unwrap(), if p["layer"] == 1 { 1 } else { 2 }, p["points"].clone())); }
            }
            if v == "fusion_pour_state" { return json!({"success": true, "pours": landed.borrow().clone(), "zones": [{"layerId": 2, "layer": "In1.Cu"}]}); }
            r
        };
        let square = json!([[0.0, 0.0], [5.0, 0.0], [5.0, 5.0], [0.0, 5.0]]);
        let r = translate("kicad_apply_zone_plan", &json!({"zones": [{"name": "top", "net": "GND", "layers": ["F.Cu"], "polygon": square}, {"name": "plane", "net": "PGND", "layers": ["In1.Cu"], "polygon": square}]}), "Doc-brd", &call);
        assert_eq!(r["success"], true, "{r}");
        let (v, a) = sent.borrow().iter().rfind(|(v, _)| v == "fusion_apply_pours").cloned().unwrap();
        assert_eq!(v, "fusion_apply_pours");
        assert_eq!(a["pours"][0]["layer"], 1); assert_eq!(a["pours"][1]["layer"], "In1.Cu"); assert_eq!(a["pours"][1]["net"], "PGND");
        // not copper: refused, and nothing landed
        let n = sent.borrow().iter().filter(|(v, _)| v == "fusion_apply_pours").count();
        let r = translate("kicad_apply_zone_plan", &json!({"zones": [{"name": "x", "net": "GND", "layers": ["F.SilkS"], "polygon": square}]}), "Doc-brd", &call);
        assert_eq!(r["errorCode"], "unknown_layer");
        assert_eq!(sent.borrow().iter().filter(|(v, _)| v == "fusion_apply_pours").count(), n);
    }
    /// fusion-bridge#138: the zone state and the plan board name a pour the same way, and a changed
    /// outline removes only the old one the run owned, then lands the new one, and reads both back by id.
    fn pour_row(net: &str, layer: i64, outline: Value) -> Value { json!({"net": net, "layer": layer, "outline": outline, "filledMm2": 1.0, "filled": true}) }
    #[test]
    fn a_changed_pour_replaces_the_old_one_the_run_owns() {
        let (old, new, theirs) = (json!([[0.0, 0.0], [5.0, 0.0], [5.0, 5.0], [0.0, 5.0]]), json!([[0.0, 0.0], [8.0, 0.0], [8.0, 5.0], [0.0, 5.0]]), json!([[20.0, 0.0], [25.0, 0.0], [25.0, 5.0]]));
        let rows = std::cell::RefCell::new(vec![pour_row("GND", 1, old.clone()), pour_row("GND", 1, theirs.clone())]);
        let sent = std::cell::RefCell::new(Vec::<(String, Value)>::new());
        let call = |v: &str, a: &Value| -> Value {
            sent.borrow_mut().push((v.to_string(), a.clone()));
            match v {
                "fusion_routing_state" => json!({"success": true, "boardId": "B", "revision": "r1"}),
                "fusion_pour_state" => json!({"success": true, "revision": "r1", "pours": rows.borrow().clone(), "zones": []}),
                "fusion_apply_pours" => { rows.borrow_mut().push(pour_row("GND", 1, a["pours"][0]["points"].clone())); json!({"success": true, "mutated": true}) }
                "fusion_remove_pours" => { rows.borrow_mut().remove(0); json!({"success": true, "verified": true, "revision": "r3"}) }
                _ => json!({"success": true}),
            }
        };
        let old_id = pour_zone_id(&pour_row("GND", 1, old.clone()));
        let flipped: Vec<Value> = new.as_array().unwrap().iter().map(|p| json!([p[0], -p[1].as_f64().unwrap()])).collect();
        let r = translate("kicad_apply_zone_plan", &json!({"ownedItemIds": [old_id], "zones": [{"name": "GND pour", "net": "GND", "layers": ["F.Cu"], "polygon": flipped}]}), "Doc-brd", &call);
        assert_eq!(r["success"], true, "{r}");
        assert_eq!(r["removedIds"], json!([old_id]));
        assert_eq!(r["deletionVerified"], true);
        assert_eq!(r["itemIds"], json!([pour_zone_id(&pour_row("GND", 1, new.clone()))]));
        let verbs: Vec<String> = sent.borrow().iter().map(|(v, _)| v.clone()).filter(|v| v == "fusion_apply_pours" || v == "fusion_remove_pours").collect();
        assert_eq!(verbs, ["fusion_remove_pours", "fusion_apply_pours"], "the old pour goes first: DELETE picks by a point the new one may share");
        // the pour the board started with is still there, and a run that owns nothing removes nothing
        assert_eq!(rows.borrow().len(), 2);
        let n = sent.borrow().len();
        let r = translate("kicad_apply_zone_plan", &json!({"ownedItemIds": [], "zones": [{"name": "GND pour", "net": "GND", "layers": ["F.Cu"], "polygon": [[30.0, 0.0], [35.0, 0.0], [35.0, -5.0]]}]}), "Doc-brd", &call);
        assert!(r.get("removedIds").is_none(), "{r}");
        assert!(sent.borrow()[n..].iter().all(|(v, _)| v != "fusion_remove_pours"));
    }
    #[test]
    fn a_pour_the_board_started_with_is_not_claimed_when_the_plan_matches_it() {
        let shape = json!([[0.0, 0.0], [5.0, 0.0], [5.0, 5.0], [0.0, 5.0]]);
        let row = pour_row("GND", 1, shape.clone());
        let call = |v: &str, _: &Value| -> Value {
            match v {
                "fusion_routing_state" => json!({"success": true, "boardId": "B", "revision": "r1"}),
                "fusion_pour_state" => json!({"success": true, "revision": "r1", "pours": [row.clone()], "zones": []}),
                _ => json!({"success": true, "mutated": false}),
            }
        };
        let flipped: Vec<Value> = shape.as_array().unwrap().iter().map(|p| json!([p[0], -p[1].as_f64().unwrap()])).collect();
        let r = translate("kicad_apply_zone_plan", &json!({"ownedItemIds": [], "zones": [{"name": "x", "net": "GND", "layers": ["F.Cu"], "polygon": flipped}]}), "Doc-brd", &call);
        assert_eq!(r["itemIds"], json!([]));
    }
    #[test]
    fn a_pour_that_does_not_land_puts_the_old_one_back() {
        let old = json!([[0.0, 0.0], [5.0, 0.0], [5.0, 5.0]]);
        let sent = std::cell::RefCell::new(Vec::<(String, Value)>::new());
        let call = |v: &str, a: &Value| -> Value {
            sent.borrow_mut().push((v.to_string(), a.clone()));
            let applies = sent.borrow().iter().filter(|(x, _)| x == "fusion_apply_pours").count();
            match v {
                "fusion_routing_state" => json!({"success": true, "boardId": "B", "revision": "r1"}),
                "fusion_pour_state" => json!({"success": true, "pours": [pour_row("GND", 1, old.clone())], "zones": []}),
                "fusion_apply_pours" if applies == 1 => json!({"success": false, "errorCode": "readback_mismatch"}),
                _ => json!({"success": true, "verified": true}),
            }
        };
        let r = translate("kicad_apply_zone_plan", &json!({"ownedItemIds": [pour_zone_id(&pour_row("GND", 1, old.clone()))], "zones": [{"name": "x", "net": "GND", "layers": ["F.Cu"], "polygon": [[0.0, 0.0], [8.0, 0.0], [8.0, -5.0]]}]}), "Doc-brd", &call);
        assert_eq!(r["errorCode"], "readback_mismatch");
        assert_eq!(r["oldPoursRestored"], true);
        assert!(r.get("removedIds").is_none(), "{r}");
        let last = sent.borrow().iter().rfind(|(v, _)| v == "fusion_apply_pours").cloned().unwrap().1;
        assert_eq!(last["pours"][0]["points"], old);
    }
    /// What the live run found on bridge 1.13.73: the bridge's remove answered verified, and the wrong pour had gone.
    #[test]
    fn the_result_is_read_back_by_id_not_taken_on_the_bridges_word() {
        let (old, new) = (json!([[1.0, 1.0], [19.0, 1.0], [19.0, 9.0], [1.0, 9.0]]), [[1.0, -1.0], [19.0, -1.0], [19.0, -14.0], [1.0, -14.0]]);
        let call = |v: &str, _: &Value| -> Value {
            match v {
                "fusion_routing_state" => json!({"success": true, "boardId": "B", "revision": "r1"}),
                // the old pour is still here, the new one is not, whatever the verbs say
                "fusion_pour_state" => json!({"success": true, "pours": [pour_row("GND", 16, old.clone())], "zones": []}),
                _ => json!({"success": true, "verified": true}),
            }
        };
        let r = translate("kicad_apply_zone_plan", &json!({"ownedItemIds": [pour_zone_id(&pour_row("GND", 16, old.clone()))], "zones": [{"name": "GND", "net": "GND", "layers": ["B.Cu"], "polygon": new}]}), "Doc-brd", &call);
        assert_eq!(r["success"], false, "{r}");
        assert_eq!(r["errorCode"], "readback_mismatch");
        assert_eq!(r["missing"], json!(["GND"]));
        assert_eq!(r["stillThere"].as_array().unwrap().len(), 1);
    }
    #[test]
    fn a_pour_keeps_its_id_whichever_vertex_it_starts_at() {
        let a = pour_row("GND", 1, json!([[0.0, 0.0], [5.0, 0.0], [5.0, 5.0]]));
        let b = pour_row("GND", 1, json!([[5.0, 5.0], [0.0, 0.0], [5.0, 0.0]]));
        assert_eq!(pour_zone_id(&a), pour_zone_id(&b));
        assert_ne!(pour_zone_id(&a), pour_zone_id(&pour_row("GND", 16, json!([[0.0, 0.0], [5.0, 0.0], [5.0, 5.0]]))));
        assert_ne!(pour_zone_id(&a), pour_zone_id(&pour_row("VIN", 1, json!([[0.0, 0.0], [5.0, 0.0], [5.0, 5.0]]))));
    }
    #[test]
    fn a_pour_the_bridge_refuses_comes_back_as_the_bridges_answer() {
        let call = |v: &str, a: &Value| -> Value {
            if v == "fusion_apply_pours" { json!({"success": false, "errorCode": "readback_mismatch", "missing": ["PGND"]}) } else { four_layer(&std::cell::RefCell::new(Vec::new()), v, a) }
        };
        let r = translate("kicad_apply_zone_plan", &json!({"zones": [{"name": "plane", "net": "PGND", "layers": ["In1.Cu"], "polygon": [[0.0, 0.0], [5.0, 0.0], [5.0, 5.0]]}]}), "Doc-brd", &call);
        assert_eq!(r["success"], false); assert_eq!(r["errorCode"], "readback_mismatch"); assert_eq!(r["missing"][0], "PGND");
    }
    #[test]
    fn standard_board_model_pads_keep_their_ref_pad_name() {
        // fusion-bridge 1.12+: name is the pad's own, key is REF.PAD, unconnected comes from the copper
        let f = json!({"success": true, "boardId": "B", "revision": "r1", "segments": [], "vias": [], "airwireCount": 0, "unconnected": 7,
                       "pads": [{"ref": "R1", "name": "1", "key": "R1.1", "net": "A", "at": [5.0, 4.5], "x": 5.0, "y": 4.5}]});
        let st = to_kicad_state(&f);
        assert_eq!(st["pads"][0]["name"], "R1.1");
        assert_eq!(st["unconnected"], 7);
        // an older bridge: REF.PAD in name, unconnected from Fusion's airwires
        let st = to_kicad_state(&json!({"success": true, "segments": [], "vias": [], "airwireCount": 3, "pads": [{"name": "R1.1", "net": "A", "at": [5.0, 4.5]}]}));
        assert_eq!(st["pads"][0]["name"], "R1.1");
        assert_eq!(st["unconnected"], 3);
    }
    /// Issue #203: on a board whose bottom is 304, B.Cu goes to Fusion as 304 and 304 reads back as B.Cu.
    #[test]
    fn the_bottom_layer_follows_the_board() {
        set_bottom(304);
        assert_eq!(layer_arg(&json!("B.Cu")), Some(json!(304)));
        assert_eq!(layer_label(304, &HashMap::new()), "B.Cu");
        assert_eq!(layer_label(16, &HashMap::new()), "B.Cu");
        assert_eq!(layer_label(2, &HashMap::new()), "In1.Cu");
        set_bottom(16);
        assert_eq!(layer_arg(&json!("B.Cu")), Some(json!(16)));
        // fusion-bridge 1.13.87 reports the bottom as 16 even on a 304 board; a reply keyed 304 still counts as B.Cu
        let r = translate("kicad_zone_state", &json!({}), "d", &|v, _| if v == "fusion_pour_state" { json!({"success": true, "filledAreaByLayerMm2": {"1": 10.0, "304": 25.0}, "pours": []}) } else { json!({"source": "<eagle/>"}) });
        assert_eq!(r["filledAreaByLayerMm2"]["B.Cu"], 25.0, "{r}");
        assert!(r["filledAreaByLayerMm2"].get("In303.Cu").is_none(), "{r}");
    }

}