use crate::io::{NodeGraphEditorConfig, NodeGraphViewState};
use crate::runtime::connection::{ConnectionHandleRef, ConnectionHandleValidity};
use crate::runtime::geometry::{HandleBounds, HandlePosition};
use crate::runtime::measurement::{
    LayoutEdgePosition, MeasuredHandle, MeasuredSurfaceAnchor, MeasuredSurfaceSlot,
    NodeHandleMeasurementSource, NodeInternalsInvalidation, NodeInternalsInvalidationReason,
    NodeMeasurement, NodeMeasurementError, NodeMeasurementOutcome, NodeMeasurementStatus,
};
use crate::runtime::store::NodeGraphStore;
use crate::schema::{
    NodeRepeatableAnchorRule, NodeRepeatableCollectionDescriptor, NodeSurfaceSlotDescriptor,
    NodeSurfaceSlotVisibility,
};
use jellyflow_core::core::{
    CanvasPoint, CanvasRect, CanvasSize, Edge, EdgeId, EdgeKind, EdgeLabelAnchor, EdgeRouteKind,
    EdgeViewDescriptor, Graph, GraphBuilder, GraphId, Node, NodeId, NodeKindKey, Port,
    PortCapacity, PortDirection, PortId, PortKey, PortKind,
};
use jellyflow_core::ops::{GraphOp, GraphTransaction};

#[test]
fn measured_size_feeds_rendering_query_without_persisting_graph_size() {
    let node = NodeId::from_u128(1);
    let graph = graph_with_unsized_node(node);
    let mut store = NodeGraphStore::new(
        graph,
        NodeGraphViewState::default(),
        NodeGraphEditorConfig::default(),
    );
    let viewport = CanvasSize {
        width: 10.0,
        height: 10.0,
    };
    let persisted_before = serde_json::to_value(store.graph()).expect("serialize graph");

    assert!(store.rendering_query(viewport).visible_node_ids.is_empty());

    let outcome = store
        .report_node_measurement(NodeMeasurement::new(node).with_size(Some(CanvasSize {
            width: 10.0,
            height: 10.0,
        })))
        .expect("node measurement");
    assert_eq!(outcome, NodeMeasurementOutcome::Changed);
    assert_eq!(store.rendering_query(viewport).visible_node_ids, vec![node]);
    assert_eq!(
        store.graph().nodes().get(&node).expect("node exists").size,
        None,
        "runtime measurements must not persist into Graph"
    );
    assert_eq!(
        serde_json::to_value(store.graph()).expect("serialize measured graph"),
        persisted_before,
        "runtime measurements must not change the persisted Graph payload"
    );

    assert_eq!(
        store.clear_node_measurement(node),
        NodeMeasurementOutcome::Changed
    );
    assert!(store.rendering_query(viewport).visible_node_ids.is_empty());
    assert_eq!(
        serde_json::to_value(store.graph()).expect("serialize cleared graph"),
        persisted_before,
        "clearing runtime measurements must not change the persisted Graph payload"
    );
}

#[test]
fn measured_handles_feed_edge_endpoints_and_connection_targets() {
    let source = NodeId::from_u128(10);
    let target = NodeId::from_u128(11);
    let out = PortId::from_u128(20);
    let input = PortId::from_u128(21);
    let edge = EdgeId::from_u128(30);
    let mut store = NodeGraphStore::new(
        graph_with_unsized_connected_nodes(source, target, out, input, edge),
        NodeGraphViewState::default(),
        NodeGraphEditorConfig::default(),
    );
    let source_handle = ConnectionHandleRef::new(source, out, PortDirection::Out);
    let target_handle = ConnectionHandleRef::new(target, input, PortDirection::In);

    store
        .report_node_measurement(
            NodeMeasurement::new(source)
                .with_size(Some(CanvasSize {
                    width: 100.0,
                    height: 100.0,
                }))
                .with_handles([MeasuredHandle::new(
                    source_handle,
                    HandleBounds {
                        rect: CanvasRect {
                            origin: CanvasPoint { x: 90.0, y: 40.0 },
                            size: CanvasSize {
                                width: 10.0,
                                height: 20.0,
                            },
                        },
                        position: HandlePosition::Right,
                    },
                )]),
        )
        .expect("source measurement");
    store
        .report_node_measurement(
            NodeMeasurement::new(target)
                .with_revision(0)
                .with_size(Some(CanvasSize {
                    width: 100.0,
                    height: 100.0,
                }))
                .with_handles([MeasuredHandle::new(
                    target_handle,
                    HandleBounds {
                        rect: CanvasRect {
                            origin: CanvasPoint { x: 0.0, y: 40.0 },
                            size: CanvasSize {
                                width: 10.0,
                                height: 20.0,
                            },
                        },
                        position: HandlePosition::Left,
                    },
                )]),
        )
        .expect("target measurement");

    let endpoints = store
        .edge_position_from_layout_facts(edge)
        .expect("edge endpoints");
    assert_eq!(endpoints.source.point, CanvasPoint { x: 100.0, y: 50.0 });
    assert_eq!(endpoints.target.point, CanvasPoint { x: 200.0, y: 50.0 });

    let facts = store.layout_facts_query(CanvasSize {
        width: 320.0,
        height: 160.0,
    });
    assert_eq!(facts.revision, store.layout_facts_revision());
    assert_eq!(facts.rendering.visible_node_ids, vec![source, target]);
    assert_eq!(
        facts.visible_edge_positions,
        vec![LayoutEdgePosition::new(edge, endpoints)]
    );
    assert_eq!(facts.visible_edge_position(edge), Some(endpoints));
    assert!(
        facts.visible_edge_route_facts(edge).is_some(),
        "layout facts should include a renderer-neutral route fact for visible edges"
    );
    assert!(
        facts
            .connection_target_candidates
            .iter()
            .any(|candidate| candidate.target.handle == target_handle)
    );

    let target = store.resolve_connection_target_from_layout_facts(
        CanvasPoint { x: 205.0, y: 50.0 },
        source_handle,
    );
    assert_eq!(target.target.expect("target handle").handle, target_handle);
    assert_eq!(target.feedback, ConnectionHandleValidity::Valid);
    assert!(target.is_handle_valid);
}

#[test]
fn measured_semantic_anchor_feeds_edge_endpoints_and_connection_targets() {
    let source = NodeId::from_u128(60);
    let target = NodeId::from_u128(61);
    let out = PortId::from_u128(62);
    let input = PortId::from_u128(63);
    let edge = EdgeId::from_u128(64);
    let mut store = NodeGraphStore::new(
        graph_with_unsized_connected_nodes(source, target, out, input, edge),
        NodeGraphViewState::default(),
        NodeGraphEditorConfig::default(),
    );
    let source_handle = ConnectionHandleRef::new(source, out, PortDirection::Out);
    let target_handle = ConnectionHandleRef::new(target, input, PortDirection::In);

    store
        .report_node_measurement(
            NodeMeasurement::new(source)
                .with_revision(1)
                .with_size(Some(CanvasSize {
                    width: 100.0,
                    height: 100.0,
                }))
                .with_handles([MeasuredHandle::new(
                    source_handle,
                    HandleBounds {
                        rect: CanvasRect {
                            origin: CanvasPoint { x: 90.0, y: 40.0 },
                            size: CanvasSize {
                                width: 10.0,
                                height: 20.0,
                            },
                        },
                        position: HandlePosition::Right,
                    },
                )]),
        )
        .expect("source measurement");
    store
        .report_node_measurement(
            NodeMeasurement::new(target)
                .with_revision(7)
                .with_size(Some(CanvasSize {
                    width: 120.0,
                    height: 120.0,
                }))
                .with_slots([MeasuredSurfaceSlot::new(
                    "field.prompt",
                    CanvasRect {
                        origin: CanvasPoint { x: 8.0, y: 44.0 },
                        size: CanvasSize {
                            width: 104.0,
                            height: 24.0,
                        },
                    },
                )])
                .with_anchors([MeasuredSurfaceAnchor::new(
                    "field.prompt.input",
                    CanvasRect {
                        origin: CanvasPoint { x: 0.0, y: 48.0 },
                        size: CanvasSize {
                            width: 10.0,
                            height: 20.0,
                        },
                    },
                    HandlePosition::Left,
                )
                .with_port_key("p")]),
        )
        .expect("target measurement");

    let resolution = store.resolve_node_handle_measurement(target_handle);
    assert!(matches!(
        resolution.source,
        NodeHandleMeasurementSource::MeasuredAnchor { ref anchor }
            if anchor == "field.prompt.input"
    ));
    assert_eq!(
        resolution.bounds.expect("anchor bounds").rect.origin,
        CanvasPoint { x: 0.0, y: 48.0 }
    );

    let endpoints = store
        .edge_position_from_layout_facts(edge)
        .expect("edge endpoints");
    assert_eq!(endpoints.source.point, CanvasPoint { x: 100.0, y: 50.0 });
    assert_eq!(endpoints.target.point, CanvasPoint { x: 200.0, y: 58.0 });

    let facts = store.layout_facts_query(CanvasSize {
        width: 360.0,
        height: 180.0,
    });
    assert_eq!(
        facts.node_measurement_status(target),
        NodeMeasurementStatus::Fresh { revision: 7 }
    );
    assert!(
        facts
            .connection_target_candidates
            .iter()
            .any(|candidate| candidate.target.handle == target_handle
                && candidate.bounds.rect.origin == CanvasPoint { x: 0.0, y: 48.0 })
    );

    let target = store.resolve_connection_target_from_layout_facts(
        CanvasPoint { x: 205.0, y: 58.0 },
        source_handle,
    );
    assert_eq!(target.target.expect("target handle").handle, target_handle);
    assert_eq!(target.feedback, ConnectionHandleValidity::Valid);
}

#[test]
fn edge_route_facts_project_route_style_and_interaction_without_mutating_edge_data() {
    let source = NodeId::from_u128(120);
    let target = NodeId::from_u128(121);
    let out = PortId::from_u128(122);
    let input = PortId::from_u128(123);
    let edge = EdgeId::from_u128(124);
    let graph = graph_with_unsized_connected_nodes(source, target, out, input, edge);
    let mut view_state = NodeGraphViewState::default();
    view_state.selected_edges = vec![edge];
    let mut store = NodeGraphStore::new(graph, view_state, NodeGraphEditorConfig::default());
    let view = EdgeViewDescriptor::new()
        .with_label("approved")
        .with_label_anchor(EdgeLabelAnchor::Target)
        .with_route_kind(EdgeRouteKind::Straight)
        .with_hit_target_width(30.0);
    store
        .dispatch_transaction(&jellyflow_core::ops::GraphTransaction::from_ops([
            jellyflow_core::ops::GraphOp::SetEdgeView {
                id: edge,
                from: EdgeViewDescriptor::default(),
                to: view,
            },
        ]))
        .expect("set edge view");
    let graph_with_view = serde_json::to_value(store.graph()).expect("serialize graph with view");
    report_basic_connected_measurements(&mut store, source, target, out, input);

    let facts = store.layout_facts_query(CanvasSize {
        width: 360.0,
        height: 180.0,
    });
    let route = facts
        .visible_edge_route_facts(edge)
        .expect("visible edge route facts");

    assert_eq!(
        route.kind,
        crate::runtime::geometry::ResolvedEdgeRouteKind::Straight
    );
    assert_eq!(route.label_anchor, EdgeLabelAnchor::Target);
    assert_eq!(route.hit_test.interaction_width, 30.0);
    assert!(route.interaction.selected);
    assert!(route.interaction.selectable);
    assert!(route.interaction.can_reconnect());
    assert!(route.contains_point(CanvasPoint { x: 150.0, y: 55.0 }));
    assert!(
        !route.contains_point(CanvasPoint { x: 150.0, y: 66.0 }),
        "straight route should honor the configured interaction width"
    );
    assert_eq!(
        serde_json::to_value(store.graph()).expect("serialize after route facts"),
        graph_with_view,
        "route projection must not write ephemeral selection or route facts into the graph"
    );
}

#[test]
fn edge_route_facts_use_smoothstep_path_for_orthogonal_route_hint() {
    let source = NodeId::from_u128(130);
    let target = NodeId::from_u128(131);
    let out = PortId::from_u128(132);
    let input = PortId::from_u128(133);
    let edge = EdgeId::from_u128(134);
    let mut store = NodeGraphStore::new(
        graph_with_unsized_connected_nodes(source, target, out, input, edge),
        NodeGraphViewState::default(),
        NodeGraphEditorConfig::default(),
    );
    store
        .dispatch_transaction(&jellyflow_core::ops::GraphTransaction::from_ops([
            jellyflow_core::ops::GraphOp::SetEdgeView {
                id: edge,
                from: EdgeViewDescriptor::default(),
                to: EdgeViewDescriptor::new().with_route_kind(EdgeRouteKind::Orthogonal),
            },
        ]))
        .expect("set edge view");
    report_basic_connected_measurements(&mut store, source, target, out, input);

    let facts = store.layout_facts_query(CanvasSize {
        width: 360.0,
        height: 180.0,
    });
    let route = facts
        .visible_edge_route_facts(edge)
        .expect("visible edge route facts");

    assert_eq!(
        route.kind,
        crate::runtime::geometry::ResolvedEdgeRouteKind::Orthogonal
    );
    assert!(
        route.path.commands.len() > 2,
        "orthogonal route hint should survive projection as a multi-leg path"
    );
}

#[test]
fn invalidating_node_internals_marks_measurement_dirty_without_mutating_graph() {
    let source = NodeId::from_u128(70);
    let target = NodeId::from_u128(71);
    let out = PortId::from_u128(72);
    let input = PortId::from_u128(73);
    let edge = EdgeId::from_u128(74);
    let mut store = NodeGraphStore::new(
        graph_with_unsized_connected_nodes(source, target, out, input, edge),
        NodeGraphViewState::default(),
        NodeGraphEditorConfig::default(),
    );
    let source_handle = ConnectionHandleRef::new(source, out, PortDirection::Out);
    let target_handle = ConnectionHandleRef::new(target, input, PortDirection::In);
    let persisted_before = serde_json::to_value(store.graph()).expect("serialize graph");

    report_basic_connected_measurements(&mut store, source, target, out, input);
    assert!(store.edge_position_from_layout_facts(edge).is_some());

    assert_eq!(
        store.invalidate_node_internals(NodeInternalsInvalidation::one(
            target,
            NodeInternalsInvalidationReason::DataChanged,
        )),
        NodeMeasurementOutcome::Changed
    );
    assert_eq!(
        store.node_measurement_status(target),
        NodeMeasurementStatus::Dirty {
            revision: 0,
            reason: NodeInternalsInvalidationReason::DataChanged,
        }
    );
    assert_eq!(
        serde_json::to_value(store.graph()).expect("serialize invalidated graph"),
        persisted_before,
        "invalidating internals must not mutate semantic graph data"
    );
    let dirty_endpoints = store
        .edge_position_from_layout_facts(edge)
        .expect("dirty measurements keep node-size fallback available");
    assert_eq!(
        dirty_endpoints.target.point,
        CanvasPoint { x: 200.0, y: 50.0 },
        "dirty target handle facts must not be reused for edge routing"
    );
    assert!(
        store
            .resolve_connection_target_from_layout_facts(
                CanvasPoint { x: 205.0, y: 50.0 },
                source_handle,
            )
            .target
            .is_none(),
        "dirty target handle facts must not be reused for connection hit tests"
    );

    store
        .report_node_measurement(
            NodeMeasurement::new(target)
                .with_revision(0)
                .with_size(Some(CanvasSize {
                    width: 100.0,
                    height: 100.0,
                }))
                .with_handles([MeasuredHandle::new(
                    target_handle,
                    HandleBounds {
                        rect: CanvasRect {
                            origin: CanvasPoint { x: 0.0, y: 58.0 },
                            size: CanvasSize {
                                width: 10.0,
                                height: 20.0,
                            },
                        },
                        position: HandlePosition::Left,
                    },
                )]),
        )
        .expect("stale target measurement is structurally valid");
    assert_eq!(
        store.node_measurement_status(target),
        NodeMeasurementStatus::Dirty {
            revision: 0,
            reason: NodeInternalsInvalidationReason::DataChanged,
        },
        "measurements with the old revision must not clear dirty state"
    );

    store
        .report_node_measurement(
            NodeMeasurement::new(target)
                .with_revision(8)
                .with_size(Some(CanvasSize {
                    width: 100.0,
                    height: 100.0,
                }))
                .with_handles([MeasuredHandle::new(
                    target_handle,
                    HandleBounds {
                        rect: CanvasRect {
                            origin: CanvasPoint { x: 0.0, y: 58.0 },
                            size: CanvasSize {
                                width: 10.0,
                                height: 20.0,
                            },
                        },
                        position: HandlePosition::Left,
                    },
                )]),
        )
        .expect("remeasured target");
    assert_eq!(
        store.node_measurement_status(target),
        NodeMeasurementStatus::Fresh { revision: 8 }
    );
    let endpoints = store
        .edge_position_from_layout_facts(edge)
        .expect("fresh endpoints");
    assert_eq!(endpoints.target.point, CanvasPoint { x: 200.0, y: 68.0 });
}

#[test]
fn node_data_transactions_invalidate_reported_internals_before_rerouting_edges() {
    let source = NodeId::from_u128(75);
    let target = NodeId::from_u128(76);
    let out = PortId::from_u128(77);
    let input = PortId::from_u128(78);
    let edge = EdgeId::from_u128(79);
    let mut store = NodeGraphStore::new(
        graph_with_unsized_connected_nodes(source, target, out, input, edge),
        NodeGraphViewState::default(),
        NodeGraphEditorConfig::default(),
    );
    report_basic_connected_measurements(&mut store, source, target, out, input);
    store
        .report_node_measurement(
            NodeMeasurement::new(target)
                .with_revision(7)
                .with_size(Some(CanvasSize {
                    width: 100.0,
                    height: 100.0,
                }))
                .with_handles([MeasuredHandle::new(
                    ConnectionHandleRef::new(target, input, PortDirection::In),
                    HandleBounds {
                        rect: CanvasRect {
                            origin: CanvasPoint { x: 0.0, y: 16.0 },
                            size: CanvasSize {
                                width: 10.0,
                                height: 20.0,
                            },
                        },
                        position: HandlePosition::Left,
                    },
                )]),
        )
        .expect("custom target measurement");
    let before = store
        .edge_position_from_layout_facts(edge)
        .expect("fresh measured route");
    assert_eq!(before.target.point, CanvasPoint { x: 200.0, y: 26.0 });

    let from = store
        .graph()
        .nodes()
        .get(&target)
        .expect("target node")
        .data
        .clone();
    let to = serde_json::json!({ "fields": { "dynamic": "changed" } });
    store
        .dispatch_transaction(
            &GraphTransaction::from_ops([GraphOp::SetNodeData {
                id: target,
                from,
                to,
            }])
            .with_label("adapter data change"),
        )
        .expect("node data transaction");

    assert_eq!(
        store.node_measurement_status(target),
        NodeMeasurementStatus::Dirty {
            revision: 7,
            reason: NodeInternalsInvalidationReason::DataChanged,
        },
        "node data edits must mark adapter-reported internals stale"
    );
    let dirty = store
        .edge_position_from_layout_facts(edge)
        .expect("dirty route keeps node-size fallback");
    assert_eq!(
        dirty.target.point,
        CanvasPoint { x: 200.0, y: 50.0 },
        "stale target anchor must not keep routing after data changes"
    );

    store
        .report_node_measurement(
            NodeMeasurement::new(target)
                .with_revision(1)
                .with_size(Some(CanvasSize {
                    width: 100.0,
                    height: 100.0,
                }))
                .with_handles([MeasuredHandle::new(
                    ConnectionHandleRef::new(target, input, PortDirection::In),
                    HandleBounds {
                        rect: CanvasRect {
                            origin: CanvasPoint { x: 0.0, y: 70.0 },
                            size: CanvasSize {
                                width: 10.0,
                                height: 20.0,
                            },
                        },
                        position: HandlePosition::Left,
                    },
                )]),
        )
        .expect("stale remeasurement is accepted but ignored");
    assert_eq!(
        store.node_measurement_status(target),
        NodeMeasurementStatus::Dirty {
            revision: 7,
            reason: NodeInternalsInvalidationReason::DataChanged,
        },
        "adapters must advance measurement revision before dirty internals become fresh again"
    );
    let stale = store
        .edge_position_from_layout_facts(edge)
        .expect("stale remeasurement still keeps fallback route");
    assert_eq!(stale.target.point, CanvasPoint { x: 200.0, y: 50.0 });

    store
        .report_node_measurement(
            NodeMeasurement::new(target)
                .with_revision(8)
                .with_size(Some(CanvasSize {
                    width: 100.0,
                    height: 100.0,
                }))
                .with_handles([MeasuredHandle::new(
                    ConnectionHandleRef::new(target, input, PortDirection::In),
                    HandleBounds {
                        rect: CanvasRect {
                            origin: CanvasPoint { x: 0.0, y: 70.0 },
                            size: CanvasSize {
                                width: 10.0,
                                height: 20.0,
                            },
                        },
                        position: HandlePosition::Left,
                    },
                )]),
        )
        .expect("fresh remeasurement after data change");
    let fresh = store
        .edge_position_from_layout_facts(edge)
        .expect("fresh route after remeasurement");
    assert_eq!(fresh.target.point, CanvasPoint { x: 200.0, y: 80.0 });
}

#[test]
fn invalidating_multiple_nodes_keeps_unrelated_measurements_fresh() {
    let a = NodeId::from_u128(80);
    let b = NodeId::from_u128(81);
    let c = NodeId::from_u128(82);
    let mut graph = GraphBuilder::new(GraphId::from_u128(80));
    graph.insert_node(
        a,
        Node {
            pos: CanvasPoint::default(),
            ..node_fixture(Vec::new())
        },
    );
    graph.insert_node(
        b,
        Node {
            pos: CanvasPoint { x: 100.0, y: 0.0 },
            ..node_fixture(Vec::new())
        },
    );
    graph.insert_node(
        c,
        Node {
            pos: CanvasPoint { x: 200.0, y: 0.0 },
            ..node_fixture(Vec::new())
        },
    );
    let mut store = NodeGraphStore::new(
        graph.into(),
        NodeGraphViewState::default(),
        NodeGraphEditorConfig::default(),
    );
    for (node, revision) in [(a, 1), (b, 2), (c, 3)] {
        store
            .report_node_measurement(
                NodeMeasurement::new(node)
                    .with_revision(revision)
                    .with_size(Some(CanvasSize {
                        width: 20.0,
                        height: 20.0,
                    })),
            )
            .expect("measurement");
    }

    assert_eq!(
        store.invalidate_node_internals(NodeInternalsInvalidation::new(
            [a, b],
            NodeInternalsInvalidationReason::ZoomChanged,
        )),
        NodeMeasurementOutcome::Changed
    );

    assert_eq!(
        store.node_measurement_status(a),
        NodeMeasurementStatus::Dirty {
            revision: 1,
            reason: NodeInternalsInvalidationReason::ZoomChanged,
        }
    );
    assert_eq!(
        store.node_measurement_status(b),
        NodeMeasurementStatus::Dirty {
            revision: 2,
            reason: NodeInternalsInvalidationReason::ZoomChanged,
        }
    );
    assert_eq!(
        store.node_measurement_status(c),
        NodeMeasurementStatus::Fresh { revision: 3 }
    );
}

#[test]
fn semantic_measurement_facts_serialize_without_renderer_types() {
    let node = NodeId::from_u128(90);
    let port = PortId::from_u128(91);
    let measurement = NodeMeasurement::new(node)
        .with_revision(2)
        .with_size(Some(CanvasSize {
            width: 160.0,
            height: 120.0,
        }))
        .with_slots([MeasuredSurfaceSlot::new(
            "field.prompt",
            CanvasRect {
                origin: CanvasPoint { x: 8.0, y: 40.0 },
                size: CanvasSize {
                    width: 144.0,
                    height: 28.0,
                },
            },
        )
        .with_visibility(NodeSurfaceSlotVisibility::Visible)])
        .with_anchors([MeasuredSurfaceAnchor::new(
            "field.prompt.input",
            CanvasRect {
                origin: CanvasPoint { x: 0.0, y: 44.0 },
                size: CanvasSize {
                    width: 10.0,
                    height: 20.0,
                },
            },
            HandlePosition::Left,
        )
        .with_port(port)
        .with_port_key("p")]);

    let json = serde_json::to_string(&measurement).expect("serialize measurement");
    assert!(json.contains("field.prompt"));
    assert!(!json.contains("egui"));
    assert!(!json.contains("gpui"));
    assert!(!json.contains("dioxus"));

    let roundtrip: NodeMeasurement = serde_json::from_str(&json).expect("deserialize measurement");
    assert_eq!(roundtrip, measurement);
}

#[test]
fn repeatable_item_anchors_survive_reorder_and_disappear_after_remove() {
    let node = NodeId::from_u128(95);
    let graph = graph_with_unsized_node(node);
    let mut store = NodeGraphStore::new(
        graph,
        NodeGraphViewState::default(),
        NodeGraphEditorConfig::default(),
    );
    let collection = NodeRepeatableCollectionDescriptor::new("table.columns", "columns", "id")
        .with_item_template_slot(NodeSurfaceSlotDescriptor::field_row("column"))
        .with_anchor_rule(NodeRepeatableAnchorRule::new(
            "field.column",
            "field.column",
        ));
    let initial_data = serde_json::json!({
        "columns": [
            { "id": "id", "name": "id" },
            { "id": "email", "name": "email" },
            { "id": "user_id", "name": "user_id" }
        ]
    });
    let reordered_data = serde_json::json!({
        "columns": [
            { "id": "user_id", "name": "user_id" },
            { "id": "id", "name": "id" },
            { "id": "email", "name": "email" }
        ]
    });
    let removed_data = serde_json::json!({
        "columns": [
            { "id": "id", "name": "id" },
            { "id": "email", "name": "email" }
        ]
    });

    let initial_items = collection.item_projections(&initial_data);
    store
        .report_node_measurement(
            NodeMeasurement::new(node)
                .with_revision(1)
                .with_slots(repeatable_slots(&initial_items))
                .with_anchors(repeatable_anchors(&initial_items)),
        )
        .expect("initial repeatable measurement");
    assert!(
        store
            .node_measurement(node)
            .expect("initial measurement")
            .anchors
            .iter()
            .any(|anchor| anchor.anchor == "field.column.user_id")
    );

    store
        .node_internals()
        .invalidate_one(node, NodeInternalsInvalidationReason::DataChanged);
    assert_eq!(
        store.node_measurement_status(node),
        NodeMeasurementStatus::Dirty {
            revision: 1,
            reason: NodeInternalsInvalidationReason::DataChanged,
        }
    );

    let reordered_items = collection.item_projections(&reordered_data);
    store
        .report_node_measurement(
            NodeMeasurement::new(node)
                .with_revision(2)
                .with_slots(repeatable_slots(&reordered_items))
                .with_anchors(repeatable_anchors(&reordered_items)),
        )
        .expect("reordered repeatable measurement");
    let reordered = store.node_measurement(node).expect("reordered measurement");
    assert_eq!(
        reordered.anchors[0].anchor, "field.column.user_id",
        "y/order can change, but logical item id drives the same anchor"
    );
    assert!(
        reordered
            .anchors
            .iter()
            .any(|anchor| anchor.anchor == "field.column.email")
    );

    let removed_items = collection.item_projections(&removed_data);
    store
        .node_internals()
        .invalidate_one(node, NodeInternalsInvalidationReason::DataChanged);
    store
        .report_node_measurement(
            NodeMeasurement::new(node)
                .with_revision(3)
                .with_slots(repeatable_slots(&removed_items))
                .with_anchors(repeatable_anchors(&removed_items)),
        )
        .expect("removed repeatable measurement");
    let removed = store.node_measurement(node).expect("removed measurement");
    assert!(
        removed
            .anchors
            .iter()
            .all(|anchor| anchor.anchor != "field.column.user_id"),
        "removed repeatable items stop reporting stale anchors"
    );
}

#[test]
fn clearing_measurement_removes_derived_handle_facts() {
    let source = NodeId::from_u128(40);
    let target = NodeId::from_u128(41);
    let out = PortId::from_u128(42);
    let input = PortId::from_u128(43);
    let edge = EdgeId::from_u128(44);
    let mut store = NodeGraphStore::new(
        graph_with_unsized_connected_nodes(source, target, out, input, edge),
        NodeGraphViewState::default(),
        NodeGraphEditorConfig::default(),
    );
    let source_handle = ConnectionHandleRef::new(source, out, PortDirection::Out);
    let target_handle = ConnectionHandleRef::new(target, input, PortDirection::In);

    store
        .report_node_measurement(
            NodeMeasurement::new(source)
                .with_size(Some(CanvasSize {
                    width: 100.0,
                    height: 100.0,
                }))
                .with_handles([MeasuredHandle::new(
                    source_handle,
                    HandleBounds {
                        rect: CanvasRect {
                            origin: CanvasPoint { x: 90.0, y: 40.0 },
                            size: CanvasSize {
                                width: 10.0,
                                height: 20.0,
                            },
                        },
                        position: HandlePosition::Right,
                    },
                )]),
        )
        .expect("source measurement");
    store
        .report_node_measurement(
            NodeMeasurement::new(target)
                .with_size(Some(CanvasSize {
                    width: 100.0,
                    height: 100.0,
                }))
                .with_handles([MeasuredHandle::new(
                    target_handle,
                    HandleBounds {
                        rect: CanvasRect {
                            origin: CanvasPoint { x: 0.0, y: 40.0 },
                            size: CanvasSize {
                                width: 10.0,
                                height: 20.0,
                            },
                        },
                        position: HandlePosition::Left,
                    },
                )]),
        )
        .expect("target measurement");

    assert!(store.edge_position_from_layout_facts(edge).is_some());
    assert!(
        store
            .resolve_connection_target_from_layout_facts(
                CanvasPoint { x: 205.0, y: 50.0 },
                source_handle,
            )
            .target
            .is_some()
    );

    assert_eq!(
        store.clear_node_measurement(target),
        NodeMeasurementOutcome::Changed
    );
    assert!(store.edge_position_from_layout_facts(edge).is_none());
    let facts = store.layout_facts_query(CanvasSize {
        width: 320.0,
        height: 160.0,
    });
    assert!(facts.visible_edge_position(edge).is_none());
    assert!(
        facts
            .connection_target_candidates
            .iter()
            .all(|candidate| candidate.target.handle != target_handle)
    );
    let resolved = store.resolve_connection_target_from_layout_facts(
        CanvasPoint { x: 205.0, y: 50.0 },
        source_handle,
    );
    assert!(resolved.target.is_none());
    assert!(!resolved.is_handle_valid);
}

fn report_basic_connected_measurements(
    store: &mut NodeGraphStore,
    source: NodeId,
    target: NodeId,
    out: PortId,
    input: PortId,
) {
    let source_handle = ConnectionHandleRef::new(source, out, PortDirection::Out);
    let target_handle = ConnectionHandleRef::new(target, input, PortDirection::In);
    store
        .report_node_measurement(
            NodeMeasurement::new(source)
                .with_size(Some(CanvasSize {
                    width: 100.0,
                    height: 100.0,
                }))
                .with_handles([MeasuredHandle::new(
                    source_handle,
                    HandleBounds {
                        rect: CanvasRect {
                            origin: CanvasPoint { x: 90.0, y: 40.0 },
                            size: CanvasSize {
                                width: 10.0,
                                height: 20.0,
                            },
                        },
                        position: HandlePosition::Right,
                    },
                )]),
        )
        .expect("source measurement");
    store
        .report_node_measurement(
            NodeMeasurement::new(target)
                .with_size(Some(CanvasSize {
                    width: 100.0,
                    height: 100.0,
                }))
                .with_handles([MeasuredHandle::new(
                    target_handle,
                    HandleBounds {
                        rect: CanvasRect {
                            origin: CanvasPoint { x: 0.0, y: 40.0 },
                            size: CanvasSize {
                                width: 10.0,
                                height: 20.0,
                            },
                        },
                        position: HandlePosition::Left,
                    },
                )]),
        )
        .expect("target measurement");
}

#[test]
fn invalid_measurement_input_is_rejected_without_replacing_existing_facts() {
    let node = NodeId::from_u128(50);
    let mut store = NodeGraphStore::new(
        graph_with_unsized_node(node),
        NodeGraphViewState::default(),
        NodeGraphEditorConfig::default(),
    );
    let measurement = NodeMeasurement::new(node).with_size(Some(CanvasSize {
        width: 10.0,
        height: 10.0,
    }));

    store
        .report_node_measurement(measurement.clone())
        .expect("valid measurement");
    let before = store.node_measurement(node);

    let err = store
        .report_node_measurement(NodeMeasurement::new(node).with_size(Some(CanvasSize {
            width: 0.0,
            height: 10.0,
        })))
        .expect_err("invalid size should be rejected");

    assert!(matches!(err, NodeMeasurementError::InvalidSize { .. }));
    assert_eq!(store.node_measurement(node), before);
}

fn graph_with_unsized_node(node: NodeId) -> Graph {
    let mut graph = GraphBuilder::new(GraphId::from_u128(1));
    graph.insert_node(
        node,
        Node {
            pos: CanvasPoint::default(),
            ..node_fixture(Vec::new())
        },
    );
    graph.into()
}

fn graph_with_unsized_connected_nodes(
    source: NodeId,
    target: NodeId,
    out: PortId,
    input: PortId,
    edge: EdgeId,
) -> Graph {
    let mut graph = GraphBuilder::new(GraphId::from_u128(2));
    graph.insert_node(
        source,
        Node {
            pos: CanvasPoint::default(),
            ports: vec![out],
            ..node_fixture(Vec::new())
        },
    );
    graph.insert_node(
        target,
        Node {
            pos: CanvasPoint { x: 200.0, y: 0.0 },
            ports: vec![input],
            ..node_fixture(Vec::new())
        },
    );
    graph.insert_port(out, port_fixture(source, PortDirection::Out));
    graph.insert_port(input, port_fixture(target, PortDirection::In));
    graph.insert_edge(edge, Edge::new(EdgeKind::Data, out, input));
    graph.into()
}

fn node_fixture(ports: Vec<PortId>) -> Node {
    Node {
        kind: NodeKindKey::new("test.node"),
        kind_version: 1,
        pos: CanvasPoint::default(),
        origin: None,
        selectable: None,
        focusable: None,
        draggable: None,
        connectable: None,
        deletable: None,
        parent: None,
        extent: None,
        expand_parent: None,
        size: None,
        hidden: false,
        collapsed: false,
        ports,
        data: serde_json::Value::Null,
    }
}

fn repeatable_slots<'a>(
    items: &'a [crate::schema::NodeRepeatableItemProjection],
) -> impl Iterator<Item = MeasuredSurfaceSlot> + 'a {
    items.iter().map(|item| {
        MeasuredSurfaceSlot::new(
            item.slot_key.clone(),
            CanvasRect {
                origin: CanvasPoint {
                    x: 8.0,
                    y: 24.0 + item.item_index as f32 * 28.0,
                },
                size: CanvasSize {
                    width: 144.0,
                    height: 24.0,
                },
            },
        )
    })
}

fn repeatable_anchors<'a>(
    items: &'a [crate::schema::NodeRepeatableItemProjection],
) -> impl Iterator<Item = MeasuredSurfaceAnchor> + 'a {
    items.iter().map(|item| {
        MeasuredSurfaceAnchor::new(
            item.anchor.clone(),
            CanvasRect {
                origin: CanvasPoint {
                    x: 0.0,
                    y: 26.0 + item.item_index as f32 * 28.0,
                },
                size: CanvasSize {
                    width: 10.0,
                    height: 20.0,
                },
            },
            HandlePosition::Left,
        )
    })
}

fn port_fixture(node: NodeId, direction: PortDirection) -> Port {
    Port {
        node,
        key: PortKey::new("p"),
        dir: direction,
        kind: PortKind::Data,
        capacity: PortCapacity::Multi,
        connectable: None,
        connectable_start: None,
        connectable_end: None,
        ty: None,
        data: serde_json::Value::Null,
    }
}
