use serde_json::json;

use super::{
    NodeKitContentDensity, NodeKitFixture, NodeKitFixtureEdge, NodeKitFixtureNode,
    NodeKitLayoutHints, NodeKitManifest, NodeKitRegistry,
};
use crate::schema::{
    ActionIntent, ActionTarget, BlackboardDescriptor, InspectorDescriptor, InspectorTarget,
    MenuDescriptor, MenuSurface, NodeActionDescriptor, NodeChromeDescriptor, NodeChromePlacement,
    NodeControlBinding, NodeControlDescriptor, NodeControlEditability, NodeControlOption,
    NodeControlOptionSource, NodeControlPresentation, NodeControlValidation,
    NodeControlValidationRule, NodeRepeatableAnchorRule, NodeRepeatableCollectionDescriptor,
    NodeSchema, NodeSurfaceLayoutBudget, NodeSurfaceOverflowIndicator, NodeSurfaceSlotDescriptor,
    PortDecl, PortViewDescriptor,
};
use jellyflow_core::core::{
    CanvasPoint, CanvasSize, EdgeKind, EdgeLabelAnchor, EdgeViewDescriptor, PortCapacity,
    PortDirection, PortKind,
};
use jellyflow_core::types::TypeDesc;

pub fn builtin_node_kits() -> NodeKitRegistry {
    let mut registry = NodeKitRegistry::new();
    registry.register(workflow_automation_manifest());
    registry.register(shader_blueprint_manifest());
    registry.register(erd_table_manifest());
    registry.register(mind_map_knowledge_canvas_manifest());
    registry
}

pub fn workflow_automation_manifest() -> NodeKitManifest {
    NodeKitManifest::new("workflow.automation", "Workflow automation")
        .with_supported_adapter("egui")
        .with_supported_adapter("proof")
        .with_supported_adapter("gpui")
        .with_capability("workflow")
        .with_capability("automation")
        .with_layout_hints(
            NodeKitLayoutHints::default()
                .with_zoom_range(0.72, 0.9)
                .with_field_spacing(10.0)
                .with_action_spacing(8.0)
                .with_measurement_note(
                    "Compact action rows and badge blocks should collapse at low zoom.",
                ),
        )
        .recipe(workflow_start_schema())
        .recipe(workflow_task_schema())
        .recipe(workflow_trigger_schema())
        .recipe(workflow_tool_schema())
        .recipe(workflow_llm_schema())
        .recipe(workflow_decision_schema())
        .recipe(workflow_switch_schema())
        .recipe(workflow_output_schema())
        .recipe(workflow_return_schema())
        .recipe(workflow_error_schema())
        .fixture(workflow_fixture())
}

pub fn erd_table_manifest() -> NodeKitManifest {
    NodeKitManifest::new("erd.table", "ERD table")
        .with_supported_adapter("egui")
        .with_supported_adapter("proof")
        .with_supported_adapter("gpui")
        .with_capability("erd")
        .with_capability("table")
        .with_layout_hints(
            NodeKitLayoutHints::default()
                .with_zoom_range(0.72, 0.88)
                .with_field_spacing(6.0)
                .with_measurement_note("Field rows should stay aligned to anchored ports."),
        )
        .recipe(erd_table_schema())
        .fixture(erd_fixture())
}

pub fn shader_blueprint_manifest() -> NodeKitManifest {
    NodeKitManifest::new("shader.blueprint", "Shader and blueprint")
        .with_supported_adapter("egui")
        .with_supported_adapter("proof")
        .with_supported_adapter("gpui")
        .with_capability("shader")
        .with_capability("blueprint")
        .with_layout_hints(
            NodeKitLayoutHints::default()
                .with_zoom_range(0.74, 0.92)
                .with_field_spacing(6.0)
                .with_measurement_note(
                    "Typed port rails and preview regions should preserve handle alignment.",
                ),
        )
        .recipe(shader_texture_sample_schema())
        .recipe(shader_mix_schema())
        .fixture(shader_fixture())
}

pub fn mind_map_knowledge_canvas_manifest() -> NodeKitManifest {
    NodeKitManifest::new("mind-map.knowledge-canvas", "Mind map and knowledge canvas")
        .with_supported_adapter("egui")
        .with_supported_adapter("proof")
        .with_supported_adapter("gpui")
        .with_capability("mind-map")
        .with_capability("knowledge-canvas")
        .with_layout_hints(
            NodeKitLayoutHints::default()
                .with_zoom_range(0.68, 0.88)
                .with_measurement_note(
                    "Topic, summary, and source nodes should degrade to shells with stable anchors.",
                ),
        )
        .recipe(mind_topic_schema())
        .recipe(mind_idea_schema())
        .recipe(source_card_schema())
        .fixture(mind_map_fixture())
}

fn product_layout_budget(
    min_readable_size: CanvasSize,
    preferred_size: CanvasSize,
    slot_line_budget: usize,
    control_line_budget: usize,
    repeatable_visible_items: Option<usize>,
) -> NodeSurfaceLayoutBudget {
    let budget = NodeSurfaceLayoutBudget::default()
        .with_min_readable_size(min_readable_size)
        .with_preferred_size(preferred_size)
        .with_slot_line_budget(slot_line_budget)
        .with_control_line_budget(control_line_budget)
        .with_overflow_indicator(NodeSurfaceOverflowIndicator::Count)
        .with_density_priority([
            NodeKitContentDensity::Full,
            NodeKitContentDensity::Regular,
            NodeKitContentDensity::Compact,
        ]);

    if let Some(items) = repeatable_visible_items {
        budget.with_repeatable_visible_items(items)
    } else {
        budget
    }
}

fn workflow_trigger_schema() -> NodeSchema {
    NodeSchema::builder("demo.trigger", "Trigger")
        .category(["Automation", "Workflow"])
        .keywords(["webhook", "schedule", "event"])
        .renderer_key("data-card")
        .default_size(CanvasSize {
            width: 208.0,
            height: 96.0,
        })
        .port(exec_output("event").on_right().with_view_group("exec"))
        .port(
            data_output("payload")
                .on_bottom()
                .with_view_group("data")
                .with_view_order(1),
        )
        .default_data(json!({ "title": "Trigger", "summary": "Starts an automation" }))
        .build()
}

fn workflow_start_schema() -> NodeSchema {
    NodeSchema::builder("demo.start", "Start")
        .category(["Workflow"])
        .renderer_key("data-card")
        .default_size(CanvasSize {
            width: 176.0,
            height: 80.0,
        })
        .port(output_port("out").on_right())
        .default_data(json!({ "title": "Start", "summary": "Entry point" }))
        .build()
}

fn workflow_task_schema() -> NodeSchema {
    NodeSchema::builder("demo.task", "Task")
        .category(["Workflow"])
        .renderer_key("task-card")
        .default_size(CanvasSize {
            width: 208.0,
            height: 96.0,
        })
        .port(PortDecl::data_input("in").with_label("in").on_left())
        .port(PortDecl::data_output("out").with_label("out").on_right())
        .default_data(json!({ "title": "Task", "summary": "Run a unit of work" }))
        .build()
}

fn workflow_tool_schema() -> NodeSchema {
    NodeSchema::builder("demo.tool", "Tool")
        .category(["Automation", "Workflow"])
        .keywords(["api", "function", "action"])
        .renderer_key("task-card")
        .default_size(CanvasSize {
            width: 208.0,
            height: 104.0,
        })
        .port(exec_input("in").on_left().with_view_group("exec"))
        .port(exec_output("out").on_right().with_view_group("exec"))
        .port(
            data_input("args")
                .on_top()
                .with_view_group("data")
                .with_view_order(0),
        )
        .port(
            data_output("result")
                .on_bottom()
                .with_view_group("data")
                .with_view_order(0),
        )
        .default_data(json!({ "title": "Tool", "summary": "Runs an external action" }))
        .build()
}

fn workflow_llm_schema() -> NodeSchema {
    NodeSchema::builder("demo.llm", "LLM")
        .category(["Automation", "AI"])
        .keywords(["prompt", "model", "dify"])
        .renderer_key("decision-card")
        .default_size(CanvasSize {
            width: 228.0,
            height: 196.0,
        })
        .layout_budget(product_layout_budget(
            CanvasSize {
                width: 340.0,
                height: 288.0,
            },
            CanvasSize {
                width: 364.0,
                height: 312.0,
            },
            4,
            3,
            Some(3),
        ))
        .port(exec_input("in").on_left().with_view_group("exec"))
        .port(exec_output("out").on_right().with_view_group("exec"))
        .port(
            data_input("prompt")
                .on_top()
                .with_view_anchor("field.prompt")
                .with_view_group("parameters")
                .with_view_order(0),
        )
        .port(
            data_output("completion")
                .on_bottom()
                .with_view_anchor("field.completion")
                .with_view_group("outputs")
                .with_view_order(0),
        )
        .surface_slot(
            NodeSurfaceSlotDescriptor::field_row("field.prompt")
                .with_label("Prompt")
                .with_slot("prompt")
                .with_anchor("field.prompt")
                .with_lane("parameters")
                .with_order(0)
                .with_control(
                    NodeControlDescriptor::text_area("control.prompt")
                        .with_label("Prompt")
                        .with_binding(NodeControlBinding::slot("prompt"))
                        .required()
                        .with_placeholder("Use {{ variable }} references"),
                )
                .with_control(
                    NodeControlDescriptor::variable_picker("control.prompt_variable")
                        .with_label("Variable")
                        .with_binding(NodeControlBinding::graph_symbol("workflow.inputs"))
                        .with_option_source(NodeControlOptionSource::Variables),
                ),
        )
        .surface_slot(
            NodeSurfaceSlotDescriptor::field_row("field.completion")
                .with_label("Completion")
                .with_slot("completion")
                .with_anchor("field.completion")
                .with_lane("outputs")
                .with_order(1)
                .with_control(
                    NodeControlDescriptor::text_area("control.completion")
                        .with_label("Completion")
                        .with_binding(NodeControlBinding::slot("completion"))
                        .read_only(),
                ),
        )
        .surface_slot(
            NodeSurfaceSlotDescriptor::badge("badge.model")
                .with_label("Model")
                .with_slot("meta.model")
                .with_anchor("meta.model")
                .with_order(0)
                .with_control(llm_model_select_control()),
        )
        .surface_slot(
            NodeSurfaceSlotDescriptor::metric_badge("metric.latency")
                .with_label("Latency")
                .with_slot("metrics.latency")
                .with_anchor("metric.latency")
                .with_order(1),
        )
        .surface_slot(
            NodeSurfaceSlotDescriptor::config_group("config.model")
                .with_label("Config")
                .with_slot("config.model")
                .with_anchor("config.model")
                .with_order(1)
                .with_controls([
                    llm_model_select_control(),
                    NodeControlDescriptor::slider("control.temperature")
                        .with_label("Temperature")
                        .with_binding(NodeControlBinding::data_path("config.model.temperature"))
                        .with_validation_rule(NodeControlValidationRule::Range {
                            min: Some(0.0),
                            max: Some(2.0),
                        })
                        .with_presentation(
                            NodeControlPresentation::default()
                                .with_unit("temp")
                                .compact_label(),
                        ),
                    NodeControlDescriptor::toggle("control.stream")
                        .with_label("Stream")
                        .with_binding(NodeControlBinding::data_path("config.model.stream")),
                    NodeControlDescriptor::asset("control.knowledge_base")
                        .with_label("Knowledge base")
                        .with_binding(NodeControlBinding::data_path("config.model.knowledge_base"))
                        .with_option_source(NodeControlOptionSource::Assets),
                ]),
        )
        .surface_slot(
            NodeSurfaceSlotDescriptor::nested_region("nested.policy")
                .with_label("Policy")
                .with_slot("nested.policy")
                .with_anchor("nested.policy")
                .with_order(2),
        )
        .surface_slot(
            NodeSurfaceSlotDescriptor::status_banner("status.validation")
                .with_label("Status")
                .with_slot("status.validation")
                .with_anchor("status.validation")
                .with_order(3),
        )
        .surface_slot(
            NodeSurfaceSlotDescriptor::action_row("actions.primary")
                .with_label("Actions")
                .with_slot("actions.primary")
                .with_anchor("actions.primary")
                .with_order(4)
                .with_control(
                    NodeControlDescriptor::toggle("control.enable_run")
                        .with_label("Run enabled")
                        .with_binding(NodeControlBinding::data_path("actions.primary.enabled"))
                        .with_editability(
                            NodeControlEditability::default()
                                .disabled("Requires a valid model and prompt"),
                        ),
                ),
        )
        .action(
            NodeActionDescriptor::new(
                "action.llm.run",
                "Run",
                ActionTarget::Node {
                    node_kind: "demo.llm".to_owned(),
                },
                ActionIntent::RunNode,
            )
            .with_group("primary")
            .with_order(0)
            .with_icon_key("play"),
        )
        .action(
            NodeActionDescriptor::new(
                "action.insert.llm",
                "Insert LLM",
                ActionTarget::DroppedWire {
                    source_port_key: Some("completion".to_owned()),
                },
                ActionIntent::InsertNode {
                    node_kind: "demo.llm".to_owned(),
                },
            )
            .with_group("create")
            .with_order(0),
        )
        .menu(
            MenuDescriptor::new("menu.dropped_wire.llm", MenuSurface::DroppedWire)
                .with_label("Insert compatible node")
                .with_action_key("action.insert.llm"),
        )
        .menu(
            MenuDescriptor::new("menu.node.llm", MenuSurface::Node)
                .with_label("LLM")
                .with_action_key("action.llm.run"),
        )
        .inspector(
            InspectorDescriptor::new(
                "inspector.llm",
                InspectorTarget::Node {
                    node_kind: "demo.llm".to_owned(),
                },
            )
            .with_label("LLM")
            .with_control(
                NodeControlDescriptor::select("inspector.model")
                    .with_label("Model")
                    .with_binding(NodeControlBinding::data_path("meta.model"))
                    .with_options([
                        NodeControlOption::new("gpt-4.1-mini", "GPT 4.1 Mini"),
                        NodeControlOption::new("gpt-4.1", "GPT 4.1"),
                    ]),
            )
            .with_action_key("action.llm.run"),
        )
        .repeatable_collection(
            NodeRepeatableCollectionDescriptor::new("llm.params", "params", "id")
                .with_label("Parameters")
                .with_empty_label("No parameters")
                .with_item_template_slot(
                    NodeSurfaceSlotDescriptor::field_row("param")
                        .with_label("Parameter")
                        .with_slot("params")
                        .with_control(
                            NodeControlDescriptor::text_input("control.param.name")
                                .with_label("Name")
                                .with_binding(NodeControlBinding::data_path("name")),
                        )
                        .with_control(
                            NodeControlDescriptor::expression("control.param.value")
                                .with_label("Value")
                                .with_binding(NodeControlBinding::data_path("value"))
                                .with_validation_rule(NodeControlValidationRule::ExpressionShape {
                                    language: Some("jinja".to_owned()),
                                }),
                        ),
                )
                .with_anchor_rule(NodeRepeatableAnchorRule::new("param", "param"))
                .with_min_items(0)
                .with_max_items(12)
                .reorderable()
                .with_add_action("action.param.add")
                .with_remove_action("action.param.remove")
                .with_reorder_action("action.param.reorder"),
        )
        .action(
            NodeActionDescriptor::new(
                "action.param.add",
                "Add parameter",
                ActionTarget::Node {
                    node_kind: "demo.llm".to_owned(),
                },
                ActionIntent::AddRepeatableItem {
                    collection_key: "llm.params".to_owned(),
                },
            )
            .with_group("parameters"),
        )
        .action(
            NodeActionDescriptor::new(
                "action.param.remove",
                "Remove parameter",
                ActionTarget::Node {
                    node_kind: "demo.llm".to_owned(),
                },
                ActionIntent::RemoveRepeatableItem {
                    collection_key: "llm.params".to_owned(),
                    item_id: String::new(),
                },
            )
            .with_group("parameters")
            .danger(),
        )
        .action(
            NodeActionDescriptor::new(
                "action.param.reorder",
                "Reorder parameter",
                ActionTarget::Node {
                    node_kind: "demo.llm".to_owned(),
                },
                ActionIntent::ReorderRepeatableItem {
                    collection_key: "llm.params".to_owned(),
                    item_id: String::new(),
                },
            )
            .with_group("parameters"),
        )
        .inspector(
            InspectorDescriptor::new(
                "inspector.param.topic",
                InspectorTarget::RepeatableItem {
                    collection_key: "llm.params".to_owned(),
                    item_id: "topic".to_owned(),
                },
            )
            .with_label("Parameter")
            .with_control(
                NodeControlDescriptor::expression("inspector.param.value")
                    .with_label("Value")
                    .with_binding(NodeControlBinding::data_path("value")),
            ),
        )
        .chrome(NodeChromeDescriptor::resizer("resize.corner").with_order(0))
        .chrome(
            NodeChromeDescriptor::toolbar("toolbar.primary", NodeChromePlacement::TopRight)
                .with_label("Node tools")
                .with_renderer_key("node-toolbar")
                .with_icon_key("settings")
                .with_order(10),
        )
        .chrome(
            NodeChromeDescriptor::status_strip("status.run", NodeChromePlacement::InsideFooter)
                .with_label("Ready")
                .with_renderer_key("run-status")
                .with_icon_key("activity")
                .with_order(20),
        )
        .chrome(
            NodeChromeDescriptor::run_action_strip("actions.run", NodeChromePlacement::Bottom)
                .with_label("Run")
                .with_renderer_key("run-actions")
                .with_icon_key("play")
                .with_order(30),
        )
        .default_data(json!({
            "title": "LLM",
            "summary": "Prompt, model, tools, and variables",
            "meta": {
                "model": "gpt-4.1-mini"
            },
            "metrics": {
                "latency": "420ms"
            },
            "config": {
                "model": {
                    "temperature": 0.2,
                    "tools": "retrieval",
                    "stream": true,
                    "knowledge_base": "product-docs"
                }
            },
            "status": {
                "validation": "Ready"
            },
            "nested": {
                "policy": {
                    "guardrails": "Block PII",
                    "response": "Return structured route"
                }
            },
            "actions": {
                "primary": {
                    "enabled": false,
                    "items": ["Test prompt", "Open trace", "Copy config"]
                }
            },
            "fields": {
                "prompt": "Customer intake + policy",
                "completion": "Priority and route"
            },
            "params": [
                { "id": "topic", "name": "topic", "value": "{{ customer.topic }}" },
                { "id": "priority", "name": "priority", "value": "{{ intake.priority }}" }
            ]
        }))
        .build()
}

fn llm_model_select_control() -> NodeControlDescriptor {
    let model_values = vec![
        json!("gpt-4.1-mini"),
        json!("gpt-4.1"),
        json!("local-llama"),
    ];

    NodeControlDescriptor::select("control.model")
        .with_label("Model")
        .with_binding(NodeControlBinding::data_path("meta.model"))
        .with_options([
            NodeControlOption::new("gpt-4.1-mini", "GPT 4.1 Mini"),
            NodeControlOption::new("gpt-4.1", "GPT 4.1"),
            NodeControlOption::new("local-llama", "Local Llama"),
        ])
        .with_validation(NodeControlValidation::default().required().with_rule(
            NodeControlValidationRule::EnumValues {
                values: model_values,
            },
        ))
}

fn workflow_decision_schema() -> NodeSchema {
    NodeSchema::builder("demo.decision", "Decision")
        .category(["Workflow"])
        .renderer_key("decision-card")
        .default_size(CanvasSize {
            width: 208.0,
            height: 104.0,
        })
        .port(input_port("in"))
        .port(output_port("yes").on_top().with_view_order(0))
        .port(output_port("no").on_bottom().with_view_order(1))
        .surface_slot(
            NodeSurfaceSlotDescriptor::badge("badge.branch")
                .with_label("Branch")
                .with_slot("meta.branch")
                .with_anchor("meta.branch")
                .with_order(0),
        )
        .default_data(json!({ "title": "Decision", "summary": "Branch the flow" }))
        .build()
}

fn workflow_switch_schema() -> NodeSchema {
    NodeSchema::builder("demo.switch", "Switch")
        .category(["Automation", "Workflow"])
        .keywords(["branch", "condition", "router"])
        .renderer_key("decision-card")
        .default_size(CanvasSize {
            width: 208.0,
            height: 104.0,
        })
        .port(exec_input("in").on_left().with_view_group("exec"))
        .port(exec_output("yes").on_top().with_view_order(0))
        .port(exec_output("no").on_bottom().with_view_order(1))
        .default_data(json!({ "title": "Switch", "summary": "Branch execution" }))
        .build()
}

fn workflow_output_schema() -> NodeSchema {
    NodeSchema::builder("demo.output", "Output")
        .alias("demo.workflow_output")
        .category(["Workflow"])
        .renderer_key("output-card")
        .default_size(CanvasSize {
            width: 208.0,
            height: 96.0,
        })
        .port(PortDecl::data_input("in").with_label("in").on_left())
        .default_data(json!({ "title": "Output", "summary": "Publish the result" }))
        .build()
}

fn workflow_return_schema() -> NodeSchema {
    NodeSchema::builder("demo.workflow_output", "Workflow output")
        .category(["Automation", "Workflow"])
        .keywords(["return", "response", "result"])
        .renderer_key("output-card")
        .default_size(CanvasSize {
            width: 208.0,
            height: 96.0,
        })
        .port(exec_input("in").on_left().with_view_group("exec"))
        .port(
            data_input("result")
                .on_top()
                .with_view_group("data")
                .with_view_order(0),
        )
        .default_data(json!({ "title": "Workflow output", "summary": "Returns data to caller" }))
        .build()
}

fn workflow_error_schema() -> NodeSchema {
    NodeSchema::builder("demo.error", "Error")
        .category(["Automation", "Workflow"])
        .keywords(["failure", "retry", "fallback"])
        .renderer_key("output-card")
        .default_size(CanvasSize {
            width: 208.0,
            height: 96.0,
        })
        .port(exec_input("error").on_left().with_view_group("exec"))
        .port(exec_output("out").on_right().with_view_group("exec"))
        .default_data(json!({ "title": "Error path", "summary": "Retry or recover" }))
        .build()
}

fn erd_table_schema() -> NodeSchema {
    NodeSchema::builder("demo.table", "Table")
        .category(["ERD"])
        .keywords(["database", "schema", "relation"])
        .renderer_key("table-card")
        .default_size(CanvasSize {
            width: 226.0,
            height: 186.0,
        })
        .layout_budget(product_layout_budget(
            CanvasSize {
                width: 420.0,
                height: 330.0,
            },
            CanvasSize {
                width: 448.0,
                height: 356.0,
            },
            4,
            2,
            Some(4),
        ))
        .port(
            data_input("fk")
                .with_label("foreign key")
                .on_left()
                .with_view_anchor("field.foreign_key")
                .with_view_group("fields")
                .with_view_order(0),
        )
        .port(
            data_output("pk")
                .with_label("primary key")
                .on_right()
                .with_view_anchor("field.primary_key")
                .with_view_group("fields")
                .with_view_order(0),
        )
        .surface_slot(
            NodeSurfaceSlotDescriptor::field_row("field.primary_key")
                .with_label("Primary key")
                .with_slot("primary_key")
                .with_anchor("field.primary_key")
                .with_lane("fields")
                .with_order(0)
                .with_control(
                    NodeControlDescriptor::text_input("control.primary_key.name")
                        .with_label("Name")
                        .with_binding(NodeControlBinding::slot("primary_key"))
                        .required(),
                ),
        )
        .surface_slot(
            NodeSurfaceSlotDescriptor::field_row("field.field")
                .with_label("Field")
                .with_slot("field")
                .with_anchor("field.field")
                .with_lane("fields")
                .with_order(1)
                .with_controls([
                    NodeControlDescriptor::text_input("control.field.name")
                        .with_label("Name")
                        .with_binding(NodeControlBinding::slot("field")),
                    NodeControlDescriptor::select("control.field.type")
                        .with_label("Type")
                        .with_binding(NodeControlBinding::data_path("schema.field.type"))
                        .with_options([
                            NodeControlOption::new("text", "Text"),
                            NodeControlOption::new("integer", "Integer"),
                            NodeControlOption::new("uuid", "UUID"),
                        ]),
                ]),
        )
        .surface_slot(
            NodeSurfaceSlotDescriptor::field_row("field.foreign_key")
                .with_label("Foreign key")
                .with_slot("foreign_key")
                .with_anchor("field.foreign_key")
                .with_lane("fields")
                .with_order(2)
                .with_control(
                    NodeControlDescriptor::port_binding("control.foreign_key.binding")
                        .with_label("Relation")
                        .with_binding(NodeControlBinding::port_anchor("field.foreign_key")),
                ),
        )
        .surface_slot(
            NodeSurfaceSlotDescriptor::badge("badge.cardinality")
                .with_label("1:N")
                .with_slot("meta.cardinality")
                .with_anchor("meta.cardinality")
                .with_order(0),
        )
        .surface_slot(
            NodeSurfaceSlotDescriptor::metric_badge("metric.rows")
                .with_label("Rows")
                .with_slot("metrics.rows")
                .with_anchor("metric.rows")
                .with_order(1),
        )
        .surface_slot(
            NodeSurfaceSlotDescriptor::action_row("actions.table")
                .with_label("Actions")
                .with_slot("actions.table")
                .with_anchor("actions.table")
                .with_order(2),
        )
        .repeatable_collection(
            NodeRepeatableCollectionDescriptor::new("table.columns", "columns", "id")
                .with_label("Columns")
                .with_empty_label("No columns")
                .with_item_template_slot(
                    NodeSurfaceSlotDescriptor::field_row("column")
                        .with_label("Column")
                        .with_slot("columns")
                        .with_control(
                            NodeControlDescriptor::text_input("control.column.name")
                                .with_label("Name")
                                .with_binding(NodeControlBinding::data_path("name")),
                        )
                        .with_control(
                            NodeControlDescriptor::select("control.column.type")
                                .with_label("Type")
                                .with_binding(NodeControlBinding::data_path("ty"))
                                .with_options([
                                    NodeControlOption::new("uuid", "UUID"),
                                    NodeControlOption::new("text", "Text"),
                                    NodeControlOption::new("integer", "Integer"),
                                ]),
                        ),
                )
                .with_anchor_rule(
                    NodeRepeatableAnchorRule::new("field.column", "field.column")
                        .with_port_key_path("port"),
                )
                .with_min_items(1)
                .with_max_items(16)
                .reorderable()
                .with_add_action("action.column.add")
                .with_remove_action("action.column.remove")
                .with_reorder_action("action.column.reorder"),
        )
        .action(
            NodeActionDescriptor::new(
                "action.column.add",
                "Add column",
                ActionTarget::Node {
                    node_kind: "demo.table".to_owned(),
                },
                ActionIntent::AddRepeatableItem {
                    collection_key: "table.columns".to_owned(),
                },
            )
            .with_group("columns")
            .with_order(0),
        )
        .action(
            NodeActionDescriptor::new(
                "action.column.remove",
                "Remove column",
                ActionTarget::Node {
                    node_kind: "demo.table".to_owned(),
                },
                ActionIntent::RemoveRepeatableItem {
                    collection_key: "table.columns".to_owned(),
                    item_id: String::new(),
                },
            )
            .with_group("columns")
            .danger(),
        )
        .action(
            NodeActionDescriptor::new(
                "action.column.reorder",
                "Reorder column",
                ActionTarget::Node {
                    node_kind: "demo.table".to_owned(),
                },
                ActionIntent::ReorderRepeatableItem {
                    collection_key: "table.columns".to_owned(),
                    item_id: String::new(),
                },
            )
            .with_group("columns"),
        )
        .menu(
            MenuDescriptor::new("menu.table", MenuSurface::Node).with_action_keys([
                "action.column.add",
                "action.column.remove",
                "action.column.reorder",
            ]),
        )
        .inspector(
            InspectorDescriptor::new(
                "inspector.column.email",
                InspectorTarget::RepeatableItem {
                    collection_key: "table.columns".to_owned(),
                    item_id: "email".to_owned(),
                },
            )
            .with_label("Column")
            .with_control(
                NodeControlDescriptor::text_input("inspector.column.name")
                    .with_label("Name")
                    .with_binding(NodeControlBinding::data_path("name")),
            ),
        )
        .default_data(json!({
            "title": "Table",
            "summary": "id · field · field",
            "meta": { "cardinality": "1:N" },
            "metrics": { "rows": "12k" },
            "actions": { "table": ["Add column", "Inspect relation"] },
            "field_order": ["primary_key", "field", "foreign_key"],
            "fields": {
                "primary_key": "id",
                "field": "field",
                "foreign_key": "field_id"
            },
            "schema": {
                "field": {
                    "type": "text"
                }
            },
            "columns": [
                { "id": "id", "name": "id", "ty": "uuid", "port": "pk" },
                { "id": "email", "name": "email", "ty": "text", "port": "field_email" },
                { "id": "user_id", "name": "user_id", "ty": "uuid", "port": "fk" }
            ]
        }))
        .build()
}

fn shader_texture_sample_schema() -> NodeSchema {
    NodeSchema::builder("demo.shader.texture_sample", "Texture Sample")
        .category(["Shader", "Blueprint"])
        .keywords(["shader", "texture", "unreal", "graph"])
        .renderer_key("shader-card")
        .default_size(CanvasSize {
            width: 224.0,
            height: 156.0,
        })
        .layout_budget(product_layout_budget(
            CanvasSize {
                width: 360.0,
                height: 272.0,
            },
            CanvasSize {
                width: 384.0,
                height: 296.0,
            },
            3,
            2,
            Some(3),
        ))
        .port(
            data_input("uv")
                .with_label("UV")
                .with_type(shader_vec(2))
                .on_left()
                .with_view_anchor("rail.inputs")
                .with_view_group("typed_ports")
                .with_view_order(0),
        )
        .port(
            data_output("color")
                .with_label("Color")
                .with_type(shader_vec(4))
                .on_right()
                .with_view_anchor("rail.outputs")
                .with_view_group("typed_ports")
                .with_view_order(0),
        )
        .surface_slot(
            NodeSurfaceSlotDescriptor::port_rail("rail.inputs")
                .with_label("Inputs")
                .with_slot("ports.inputs")
                .with_anchor("rail.inputs")
                .with_lane("ports")
                .with_order(0),
        )
        .surface_slot(
            NodeSurfaceSlotDescriptor::preview("preview.texture")
                .with_label("Preview")
                .with_slot("preview.texture")
                .with_anchor("preview.texture")
                .with_order(1)
                .with_control(
                    NodeControlDescriptor::asset("control.texture")
                        .with_label("Texture")
                        .with_binding(NodeControlBinding::data_path("preview.texture"))
                        .with_option_source(NodeControlOptionSource::Assets),
                ),
        )
        .surface_slot(
            NodeSurfaceSlotDescriptor::port_rail("rail.outputs")
                .with_label("Outputs")
                .with_slot("ports.outputs")
                .with_anchor("rail.outputs")
                .with_lane("ports")
                .with_order(2),
        )
        .default_data(json!({
            "title": "Texture Sample",
            "summary": "Samples albedo from UV",
            "ports": {
                "inputs": ["vec2 uv"],
                "outputs": ["vec4 color"]
            },
            "preview": {
                "texture": "checker"
            }
        }))
        .build()
}

fn shader_mix_schema() -> NodeSchema {
    let shader_properties =
        NodeRepeatableCollectionDescriptor::new("shader.properties", "properties", "id")
            .with_item_template_slot(
                NodeSurfaceSlotDescriptor::field_row("property")
                    .with_label("Property")
                    .with_slot("properties")
                    .with_control(
                        NodeControlDescriptor::text_input("control.property.name")
                            .with_label("Name")
                            .with_binding(NodeControlBinding::data_path("name")),
                    ),
            )
            .with_anchor_rule(NodeRepeatableAnchorRule::new("property", "property"))
            .with_add_action("action.shader_property.add");

    NodeSchema::builder("demo.shader.mix", "Mix")
        .category(["Shader", "Blueprint"])
        .keywords(["shader", "mix", "lerp", "blueprint"])
        .renderer_key("shader-card")
        .default_size(CanvasSize {
            width: 224.0,
            height: 168.0,
        })
        .layout_budget(product_layout_budget(
            CanvasSize {
                width: 360.0,
                height: 272.0,
            },
            CanvasSize {
                width: 392.0,
                height: 340.0,
            },
            3,
            2,
            Some(3),
        ))
        .port(
            data_input("a")
                .with_label("A")
                .with_type(shader_vec(4))
                .on_left()
                .with_view_anchor("rail.inputs")
                .with_view_group("typed_ports")
                .with_view_order(0),
        )
        .port(
            data_input("b")
                .with_label("B")
                .with_type(shader_vec(4))
                .on_left()
                .with_view_anchor("rail.inputs")
                .with_view_group("typed_ports")
                .with_view_order(1),
        )
        .port(
            data_input("factor")
                .with_label("Factor")
                .with_type(TypeDesc::Float)
                .on_bottom()
                .with_view_anchor("config.factor")
                .with_view_group("config")
                .with_view_order(2),
        )
        .port(
            data_output("result")
                .with_label("Result")
                .with_type(shader_vec(4))
                .on_right()
                .with_view_anchor("rail.outputs")
                .with_view_group("typed_ports")
                .with_view_order(0),
        )
        .surface_slot(
            NodeSurfaceSlotDescriptor::port_rail("rail.inputs")
                .with_label("Inputs")
                .with_slot("ports.inputs")
                .with_anchor("rail.inputs")
                .with_lane("ports")
                .with_order(0),
        )
        .surface_slot(
            NodeSurfaceSlotDescriptor::config_group("config.factor")
                .with_label("Factor")
                .with_slot("config.factor")
                .with_anchor("config.factor")
                .with_order(1)
                .with_control(
                    NodeControlDescriptor::slider("control.factor")
                        .with_label("Factor")
                        .with_binding(NodeControlBinding::data_path("config.factor.default"))
                        .with_validation_rule(NodeControlValidationRule::Range {
                            min: Some(0.0),
                            max: Some(1.0),
                        }),
                ),
        )
        .surface_slot(
            NodeSurfaceSlotDescriptor::preview("preview.result")
                .with_label("Preview")
                .with_slot("preview.result")
                .with_anchor("preview.result")
                .with_order(2),
        )
        .surface_slot(
            NodeSurfaceSlotDescriptor::port_rail("rail.outputs")
                .with_label("Outputs")
                .with_slot("ports.outputs")
                .with_anchor("rail.outputs")
                .with_lane("ports")
                .with_order(3),
        )
        .repeatable_collection(
            NodeRepeatableCollectionDescriptor::new("shader.inputs", "dynamic_inputs", "id")
                .with_label("Dynamic inputs")
                .with_empty_label("No dynamic inputs")
                .with_item_template_slot(
                    NodeSurfaceSlotDescriptor::port_rail("input")
                        .with_label("Input")
                        .with_slot("dynamic_inputs")
                        .with_control(
                            NodeControlDescriptor::select("control.shader_input.type")
                                .with_label("Type")
                                .with_binding(NodeControlBinding::data_path("ty"))
                                .with_options([
                                    NodeControlOption::new("float", "Float"),
                                    NodeControlOption::new("vec2", "Vec2"),
                                    NodeControlOption::new("vec4", "Vec4"),
                                ]),
                        ),
                )
                .with_anchor_rule(
                    NodeRepeatableAnchorRule::new("rail.inputs", "rail.inputs")
                        .with_port_key_path("port"),
                )
                .with_min_items(2)
                .with_max_items(8)
                .reorderable()
                .with_add_action("action.shader_input.add")
                .with_remove_action("action.shader_input.remove")
                .with_reorder_action("action.shader_input.reorder"),
        )
        .repeatable_collection(shader_properties.clone())
        .action(
            NodeActionDescriptor::new(
                "action.shader_input.add",
                "Add input",
                ActionTarget::Node {
                    node_kind: "demo.shader.mix".to_owned(),
                },
                ActionIntent::AddRepeatableItem {
                    collection_key: "shader.inputs".to_owned(),
                },
            )
            .with_group("inputs"),
        )
        .action(
            NodeActionDescriptor::new(
                "action.shader_input.remove",
                "Remove input",
                ActionTarget::Node {
                    node_kind: "demo.shader.mix".to_owned(),
                },
                ActionIntent::RemoveRepeatableItem {
                    collection_key: "shader.inputs".to_owned(),
                    item_id: String::new(),
                },
            )
            .with_group("inputs")
            .danger(),
        )
        .action(
            NodeActionDescriptor::new(
                "action.shader_input.reorder",
                "Reorder input",
                ActionTarget::Node {
                    node_kind: "demo.shader.mix".to_owned(),
                },
                ActionIntent::ReorderRepeatableItem {
                    collection_key: "shader.inputs".to_owned(),
                    item_id: String::new(),
                },
            )
            .with_group("inputs"),
        )
        .action(
            NodeActionDescriptor::new(
                "action.shader_property.add",
                "Add property",
                ActionTarget::Blackboard {
                    blackboard_key: "blackboard.shader.properties".to_owned(),
                },
                ActionIntent::AddRepeatableItem {
                    collection_key: "shader.properties".to_owned(),
                },
            )
            .with_group("blackboard"),
        )
        .blackboard(
            BlackboardDescriptor::new(
                "blackboard.shader.properties",
                "Shader properties",
                shader_properties,
            )
            .with_action_key("action.shader_property.add"),
        )
        .default_data(json!({
            "title": "Mix",
            "summary": "Blend two color streams",
            "ports": {
                "inputs": ["vec4 a", "vec4 b"],
                "outputs": ["vec4 result"]
            },
            "config": {
                "factor": {
                    "type": "float",
                    "default": 0.5
                }
            },
            "preview": {
                "result": "gradient"
            },
            "dynamic_inputs": [
                { "id": "a", "name": "A", "ty": "vec4", "port": "a" },
                { "id": "b", "name": "B", "ty": "vec4", "port": "b" },
                { "id": "factor", "name": "Factor", "ty": "float", "port": "factor" }
            ],
            "properties": [
                { "id": "base_color", "name": "Base Color", "ty": "vec4" },
                { "id": "roughness", "name": "Roughness", "ty": "float" }
            ]
        }))
        .build()
}

fn mind_topic_schema() -> NodeSchema {
    NodeSchema::builder("demo.topic", "Topic")
        .category(["Mind map"])
        .keywords(["mindnode", "margin-note", "knowledge"])
        .renderer_key("topic-card")
        .default_size(CanvasSize {
            width: 210.0,
            height: 96.0,
        })
        .layout_budget(product_layout_budget(
            CanvasSize {
                width: 320.0,
                height: 220.0,
            },
            CanvasSize {
                width: 344.0,
                height: 240.0,
            },
            3,
            2,
            None,
        ))
        .port(input_port("in"))
        .port(output_port("out"))
        .surface_slot(
            NodeSurfaceSlotDescriptor::header("header.main")
                .with_label("Topic")
                .with_slot("title")
                .with_order(0)
                .with_control(
                    NodeControlDescriptor::text_input("control.topic.title")
                        .with_label("Title")
                        .with_binding(NodeControlBinding::slot("title")),
                ),
        )
        .surface_slot(
            NodeSurfaceSlotDescriptor::body("body.summary")
                .with_label("Summary")
                .with_slot("summary")
                .with_anchor("body.summary")
                .with_order(1)
                .with_control(
                    NodeControlDescriptor::text_area("control.topic.summary")
                        .with_label("Summary")
                        .with_binding(NodeControlBinding::slot("summary")),
                ),
        )
        .default_data(json!({ "title": "Topic", "summary": "Central idea" }))
        .build()
}

fn mind_idea_schema() -> NodeSchema {
    NodeSchema::builder("demo.idea", "Idea")
        .category(["Mind map"])
        .keywords(["branch", "note", "thought"])
        .renderer_key("idea-card")
        .default_size(CanvasSize {
            width: 176.0,
            height: 76.0,
        })
        .layout_budget(product_layout_budget(
            CanvasSize {
                width: 248.0,
                height: 132.0,
            },
            CanvasSize {
                width: 280.0,
                height: 156.0,
            },
            2,
            1,
            None,
        ))
        .port(input_port("in"))
        .port(output_port("out"))
        .surface_slot(
            NodeSurfaceSlotDescriptor::header("header.main")
                .with_label("Idea")
                .with_slot("title")
                .with_order(0)
                .with_control(
                    NodeControlDescriptor::text_input("control.idea.title")
                        .with_label("Title")
                        .with_binding(NodeControlBinding::slot("title")),
                ),
        )
        .default_data(json!({ "title": "Idea", "summary": "Branch note" }))
        .build()
}

fn source_card_schema() -> NodeSchema {
    NodeSchema::builder("demo.source", "Source")
        .category(["Knowledge"])
        .keywords(["paper", "quote", "annotation", "marginnote"])
        .renderer_key("source-card")
        .default_size(CanvasSize {
            width: 210.0,
            height: 92.0,
        })
        .layout_budget(product_layout_budget(
            CanvasSize {
                width: 328.0,
                height: 220.0,
            },
            CanvasSize {
                width: 352.0,
                height: 240.0,
            },
            3,
            2,
            None,
        ))
        .port(output_port("out"))
        .surface_slot(
            NodeSurfaceSlotDescriptor::header("header.main")
                .with_label("Source")
                .with_slot("title")
                .with_order(0)
                .with_control(
                    NodeControlDescriptor::text_input("control.source.title")
                        .with_label("Title")
                        .with_binding(NodeControlBinding::slot("title")),
                ),
        )
        .surface_slot(
            NodeSurfaceSlotDescriptor::preview("preview.main")
                .with_label("Excerpt")
                .with_slot("preview")
                .with_anchor("preview.main")
                .with_order(1)
                .with_control(
                    NodeControlDescriptor::asset("control.source.asset")
                        .with_label("Source")
                        .with_binding(NodeControlBinding::slot("preview"))
                        .with_option_source(NodeControlOptionSource::Assets),
                ),
        )
        .default_data(json!({
            "title": "Source",
            "summary": "Evidence card",
            "preview": "Annotated excerpt"
        }))
        .build()
}

fn workflow_fixture() -> NodeKitFixture {
    NodeKitFixture::new("workflow.review", "Workflow review")
        .with_description("A compact review loop with branch and output paths.")
        .node(
            NodeKitFixtureNode::new(
                "trigger",
                "demo.trigger",
                CanvasPoint {
                    x: -320.0,
                    y: -40.0,
                },
            )
            .with_data(json!({
                "title": "Trigger",
                "summary": "Starts an automation"
            })),
        )
        .node(
            NodeKitFixtureNode::new("tool", "demo.tool", CanvasPoint { x: -20.0, y: -80.0 })
                .with_data(json!({
                    "title": "Tool",
                    "summary": "Runs an external action"
                })),
        )
        .node(
            NodeKitFixtureNode::new(
                "decision",
                "demo.decision",
                CanvasPoint { x: 300.0, y: -80.0 },
            )
            .with_data(json!({
                "title": "Decision",
                "summary": "Branch the flow"
            })),
        )
        .node(
            NodeKitFixtureNode::new(
                "output",
                "demo.workflow_output",
                CanvasPoint { x: 620.0, y: -40.0 },
            )
            .with_data(json!({
                "title": "Workflow output",
                "summary": "Returns data to caller"
            })),
        )
        .edge(
            NodeKitFixtureEdge::new("trigger", "tool", EdgeKind::Exec)
                .with_from_port("event")
                .with_to_port("in")
                .with_data(json!({ "label": "event" }))
                .with_view(
                    EdgeViewDescriptor::new()
                        .with_label("event")
                        .with_label_anchor(EdgeLabelAnchor::Center)
                        .with_target_marker_key("arrow")
                        .with_style_token("workflow"),
                ),
        )
        .edge(
            NodeKitFixtureEdge::new("tool", "decision", EdgeKind::Exec)
                .with_from_port("out")
                .with_to_port("in")
                .with_data(json!({ "label": "flow" })),
        )
        .edge(
            NodeKitFixtureEdge::new("decision", "output", EdgeKind::Exec)
                .with_from_port("yes")
                .with_to_port("in")
                .with_data(json!({ "label": "yes" })),
        )
        .expect_counts(4, 3)
}

fn erd_fixture() -> NodeKitFixture {
    NodeKitFixture::new("erd.customer_orders", "Customer orders")
        .with_description("Three linked tables showing a classic 1:N relationship.")
        .node(
            NodeKitFixtureNode::new(
                "customers",
                "demo.table",
                CanvasPoint {
                    x: -520.0,
                    y: -80.0,
                },
            )
            .with_data(json!({
                "title": "customers",
                "summary": "id · email · plan_id",
                "field_order": ["primary_key", "field", "foreign_key"],
                "fields": {
                    "primary_key": "id",
                    "field": "email",
                    "foreign_key": "plan_id"
                },
                "meta": { "cardinality": "1:N" },
                "metrics": { "rows": "42k" },
                "actions": { "table": ["Add column", "Inspect relation"] },
                "columns": [
                    { "id": "id", "name": "id", "ty": "uuid", "port": "pk" },
                    { "id": "email", "name": "email", "ty": "text", "port": "field_email" },
                    { "id": "plan_id", "name": "plan_id", "ty": "uuid", "port": "fk" }
                ]
            })),
        )
        .node(
            NodeKitFixtureNode::new("orders", "demo.table", CanvasPoint { x: 0.0, y: -100.0 })
                .with_data(json!({
                    "title": "orders",
                    "summary": "id · customer_id · total",
                    "field_order": ["primary_key", "field", "foreign_key"],
                    "fields": {
                        "primary_key": "id",
                        "field": "total",
                        "foreign_key": "customer_id"
                    },
                    "meta": { "cardinality": "1:N" },
                    "metrics": { "rows": "94k" },
                    "actions": { "table": ["Add column", "Inspect relation"] },
                    "columns": [
                        { "id": "id", "name": "id", "ty": "uuid", "port": "pk" },
                        { "id": "customer_id", "name": "customer_id", "ty": "uuid", "port": "fk" },
                        { "id": "total", "name": "total", "ty": "integer", "port": "field_total" }
                    ]
                })),
        )
        .node(
            NodeKitFixtureNode::new(
                "order_items",
                "demo.table",
                CanvasPoint { x: 520.0, y: -80.0 },
            )
            .with_data(json!({
                "title": "order_items",
                "summary": "id · order_id · sku_id · qty",
                "field_order": ["primary_key", "field", "foreign_key", "field"],
                "fields": {
                    "primary_key": "id",
                    "field": "qty",
                    "foreign_key": "order_id"
                },
                "meta": { "cardinality": "1:N" },
                "metrics": { "rows": "320k" },
                "actions": { "table": ["Add column", "Inspect relation"] },
                "columns": [
                    { "id": "id", "name": "id", "ty": "uuid", "port": "pk" },
                    { "id": "order_id", "name": "order_id", "ty": "uuid", "port": "fk" },
                    { "id": "qty", "name": "qty", "ty": "integer", "port": "field_qty" }
                ]
            })),
        )
        .edge(
            NodeKitFixtureEdge::new("customers", "orders", EdgeKind::Data)
                .with_from_port("pk")
                .with_to_port("fk")
                .with_data(json!({ "label": "1:N" })),
        )
        .edge(
            NodeKitFixtureEdge::new("orders", "order_items", EdgeKind::Data)
                .with_from_port("pk")
                .with_to_port("fk")
                .with_data(json!({ "label": "1:N" })),
        )
        .expect_counts(3, 2)
}

fn shader_fixture() -> NodeKitFixture {
    NodeKitFixture::new("shader.material_mix", "Material mix")
        .with_description("A compact shader graph with typed rails and preview slots.")
        .node(
            NodeKitFixtureNode::new(
                "texture",
                "demo.shader.texture_sample",
                CanvasPoint {
                    x: -320.0,
                    y: -60.0,
                },
            )
            .with_data(json!({
                "title": "Albedo",
                "summary": "Texture sample",
                "ports": {
                    "inputs": ["vec2 uv"],
                    "outputs": ["vec4 color"]
                },
                "preview": {
                    "texture": "checker"
                }
            })),
        )
        .node(
            NodeKitFixtureNode::new("mix", "demo.shader.mix", CanvasPoint { x: 80.0, y: -60.0 })
                .with_data(json!({
                    "title": "Mix",
                    "summary": "Blend albedo with tint",
                    "ports": {
                        "inputs": ["vec4 albedo", "vec4 tint"],
                        "outputs": ["vec4 result"]
                    },
                    "config": {
                        "factor": {
                            "type": "float",
                            "default": 0.5
                        }
                    },
                    "preview": {
                        "result": "gradient"
                    }
                })),
        )
        .edge(
            NodeKitFixtureEdge::new("texture", "mix", EdgeKind::Data)
                .with_from_port("color")
                .with_to_port("a")
                .with_data(json!({ "label": "vec4" })),
        )
        .expect_counts(2, 1)
}

fn mind_map_fixture() -> NodeKitFixture {
    NodeKitFixture::new("mind-map.strategy", "Strategy map")
        .with_description("A radial map showing topic, ideas, and source cards.")
        .node(
            NodeKitFixtureNode::new("center", "demo.topic", CanvasPoint { x: 0.0, y: 0.0 })
                .with_data(json!({
                    "title": "Product strategy",
                    "summary": "MindNode-style radial map"
                })),
        )
        .node(
            NodeKitFixtureNode::new(
                "users",
                "demo.idea",
                CanvasPoint {
                    x: 432.0,
                    y: -128.0,
                },
            )
            .with_data(json!({
                "title": "Users",
                "summary": "Researchers, builders, editors"
            })),
        )
        .node(
            NodeKitFixtureNode::new("risks", "demo.idea", CanvasPoint { x: 432.0, y: 136.0 })
                .with_data(json!({
                    "title": "Risks",
                    "summary": "Trust, scale, migration"
                })),
        )
        .node(
            NodeKitFixtureNode::new("sources", "demo.source", CanvasPoint { x: -420.0, y: 24.0 })
                .with_data(json!({
                    "title": "Source",
                    "summary": "Evidence card"
                })),
        )
        .edge(
            NodeKitFixtureEdge::new("center", "users", EdgeKind::Data)
                .with_data(json!({ "label": "branch" })),
        )
        .edge(
            NodeKitFixtureEdge::new("center", "risks", EdgeKind::Data)
                .with_data(json!({ "label": "branch" })),
        )
        .edge(
            NodeKitFixtureEdge::new("sources", "center", EdgeKind::Data)
                .with_from_port("out")
                .with_to_port("in")
                .with_data(json!({ "label": "evidence" })),
        )
        .expect_counts(4, 3)
}

fn input_port(key: &str) -> PortDecl {
    PortDecl::new(key, PortDirection::In, PortKind::Data, PortCapacity::Multi)
        .with_label(key)
        .on_left()
}

fn output_port(key: &str) -> PortDecl {
    PortDecl::new(key, PortDirection::Out, PortKind::Data, PortCapacity::Multi)
        .with_label(key)
        .with_view(PortViewDescriptor::right())
}

fn data_input(key: &str) -> PortDecl {
    PortDecl::new(key, PortDirection::In, PortKind::Data, PortCapacity::Multi).with_label(key)
}

fn data_output(key: &str) -> PortDecl {
    PortDecl::new(key, PortDirection::Out, PortKind::Data, PortCapacity::Multi).with_label(key)
}

fn shader_vec(width: u8) -> TypeDesc {
    TypeDesc::Opaque {
        key: format!("shader.vec{width}"),
        params: Vec::new(),
    }
}

fn exec_input(key: &str) -> PortDecl {
    PortDecl::new(key, PortDirection::In, PortKind::Exec, PortCapacity::Single).with_label(key)
}

fn exec_output(key: &str) -> PortDecl {
    PortDecl::new(key, PortDirection::Out, PortKind::Exec, PortCapacity::Multi).with_label(key)
}
