use serde_json::json;

use crate::schema::{
    ActionAvailability, ActionIntent, ActionShortcut, ActionTarget, BlackboardDescriptor,
    InspectorDescriptor, InspectorTarget, MenuDescriptor, MenuSurface, NodeActionDescriptor,
    NodeChromeDescriptor, NodeChromeKind, NodeChromePlacement, NodeChromeVisibility,
    NodeControlBinding, NodeControlDescriptor, NodeControlEditability, NodeControlKind,
    NodeControlOption, NodeControlOptionSource, NodeControlPresentation, NodeControlValidation,
    NodeControlValidationRule, NodeKitContentDensity, NodeRegistry, NodeRepeatableAnchorRule,
    NodeRepeatableCollectionDescriptor, NodeSchema, NodeSurfaceLayoutBudget,
    NodeSurfaceOverflowIndicator, NodeSurfaceProjection, NodeSurfaceSlotDescriptor,
    NodeSurfaceSlotKind, NodeSurfaceSlotVisibility, PortDecl, PortHandleVisibility,
    PortViewDescriptor, PortViewSide,
};
use jellyflow_core::core::{
    CanvasSize, NodeKindKey, PortCapacity, PortDirection, PortKey, PortKind,
};
use jellyflow_core::types::TypeDesc;

#[test]
fn node_registry_view_descriptors_are_adapter_facing_and_deterministic() {
    let mut registry = NodeRegistry::new();
    registry.register(NodeSchema {
        kind: NodeKindKey::new("demo.note"),
        latest_kind_version: 1,
        kind_aliases: vec![NodeKindKey::new("demo.sticky")],
        title: "Note".into(),
        category: vec!["Knowledge".into()],
        keywords: vec!["memo".into(), "markdown".into()],
        renderer_key: Some("note-card".into()),
        default_size: Some(CanvasSize {
            width: 180.0,
            height: 120.0,
        }),
        layout_budget: NodeSurfaceLayoutBudget::default()
            .with_min_readable_size(CanvasSize {
                width: 220.0,
                height: 150.0,
            })
            .with_preferred_size(CanvasSize {
                width: 240.0,
                height: 180.0,
            })
            .with_slot_line_budget(3)
            .with_control_line_budget(2)
            .with_repeatable_visible_items(4)
            .with_overflow_indicator(NodeSurfaceOverflowIndicator::Count)
            .with_density_priority([
                NodeKitContentDensity::Full,
                NodeKitContentDensity::Regular,
                NodeKitContentDensity::Compact,
            ]),
        ports: vec![PortDecl {
            key: PortKey::new("source"),
            dir: PortDirection::In,
            kind: PortKind::Data,
            capacity: PortCapacity::Single,
            ty: Some(TypeDesc::Opaque {
                key: "markdown".into(),
                params: Vec::new(),
            }),
            label: Some("Source".into()),
            view: PortViewDescriptor::left()
                .with_order(10)
                .with_group("input")
                .with_anchor("field.source")
                .with_icon_key("file-text"),
        }],
        surface_slots: vec![
            NodeSurfaceSlotDescriptor::header("header.main")
                .with_label("Title")
                .with_order(0),
            NodeSurfaceSlotDescriptor::field_row("field.source")
                .with_label("Source")
                .with_anchor("field.source")
                .with_lane("fields")
                .with_slot("source")
                .with_icon_key("file-text"),
        ],
        repeatable_collections: Vec::new(),
        actions: Vec::new(),
        menus: Vec::new(),
        inspectors: Vec::new(),
        blackboards: Vec::new(),
        chrome: Vec::new(),
        default_data: json!({ "body": "" }),
    });
    registry.register(NodeSchema {
        kind: NodeKindKey::new("demo.add"),
        latest_kind_version: 1,
        kind_aliases: Vec::new(),
        title: "Add".into(),
        category: Vec::new(),
        keywords: Vec::new(),
        renderer_key: None,
        default_size: None,
        layout_budget: NodeSurfaceLayoutBudget::default(),
        ports: Vec::new(),
        surface_slots: Vec::new(),
        repeatable_collections: Vec::new(),
        actions: Vec::new(),
        menus: Vec::new(),
        inspectors: Vec::new(),
        blackboards: Vec::new(),
        chrome: Vec::new(),
        default_data: serde_json::Value::Null,
    });

    let descriptors = registry.view_descriptors();

    assert_eq!(descriptors.len(), 2);
    assert_eq!(descriptors[0].kind, NodeKindKey::new("demo.add"));
    assert_eq!(
        descriptors[0].renderer_key, "demo.add",
        "schemas without an explicit renderer key fall back to the canonical kind"
    );
    assert_eq!(descriptors[1].kind, NodeKindKey::new("demo.note"));
    assert_eq!(descriptors[1].renderer_key, "note-card");
    assert_eq!(descriptors[1].title, "Note");
    assert_eq!(descriptors[1].category, vec!["Knowledge"]);
    assert_eq!(descriptors[1].keywords, vec!["memo", "markdown"]);
    assert_eq!(
        descriptors[1].default_size,
        Some(CanvasSize {
            width: 180.0,
            height: 120.0,
        })
    );
    assert_eq!(
        descriptors[1].layout_budget.min_readable_size,
        Some(CanvasSize {
            width: 220.0,
            height: 150.0,
        })
    );
    assert_eq!(descriptors[1].layout_budget.slot_line_budget, Some(3));
    assert_eq!(
        descriptors[1].layout_budget.repeatable_visible_items,
        Some(4)
    );
    assert_eq!(
        descriptors[1].layout_budget.overflow_indicator,
        Some(NodeSurfaceOverflowIndicator::Count)
    );
    assert_eq!(descriptors[1].ports.len(), 1);
    assert_eq!(descriptors[1].ports[0].view.side, Some(PortViewSide::Left));
    assert_eq!(descriptors[1].ports[0].view.order, Some(10));
    assert_eq!(descriptors[1].ports[0].view.group.as_deref(), Some("input"));
    assert_eq!(
        descriptors[1].ports[0].view.anchor.as_deref(),
        Some("field.source")
    );
    assert_eq!(
        descriptors[1].ports[0].view.icon_key.as_deref(),
        Some("file-text")
    );
    assert_eq!(descriptors[1].surface_slots.len(), 2);
    assert_eq!(
        descriptors[1].surface_slots[0].kind,
        NodeSurfaceSlotKind::Header
    );
    assert_eq!(descriptors[1].surface_slots[1].key.as_str(), "field.source");
    assert_eq!(
        descriptors[1].surface_slots[1].lane.as_deref(),
        Some("fields")
    );
    assert_eq!(descriptors[1].default_data, json!({ "body": "" }));

    let alias_descriptor = registry
        .view_descriptor(&NodeKindKey::new("demo.sticky"))
        .expect("descriptor by alias");
    assert_eq!(alias_descriptor.kind, NodeKindKey::new("demo.note"));
    assert_eq!(alias_descriptor.renderer_key, "note-card");
    assert_eq!(alias_descriptor.surface_slots.len(), 2);
}

#[test]
fn node_schema_deserializes_without_adapter_view_fields() {
    let schema: NodeSchema = serde_json::from_value(json!({
        "kind": "demo.legacy",
        "latest_kind_version": 1,
        "title": "Legacy",
        "default_data": { "value": 1 }
    }))
    .expect("legacy schema");

    let mut registry = NodeRegistry::new();
    registry.register(schema);
    let descriptors = registry.view_descriptors();

    assert_eq!(descriptors[0].renderer_key, "demo.legacy");
    assert_eq!(descriptors[0].default_size, None);
}

#[test]
fn node_surface_layout_budget_round_trips_without_framework_widgets() {
    let budget = NodeSurfaceLayoutBudget::default()
        .with_min_readable_size(CanvasSize {
            width: 260.0,
            height: 180.0,
        })
        .with_preferred_size(CanvasSize {
            width: 320.0,
            height: 220.0,
        })
        .with_slot_line_budget(4)
        .with_control_line_budget(2)
        .with_repeatable_visible_items(3)
        .with_overflow_indicator(NodeSurfaceOverflowIndicator::Summary)
        .with_density_priority([
            NodeKitContentDensity::Full,
            NodeKitContentDensity::Regular,
            NodeKitContentDensity::Compact,
        ]);
    let schema = NodeSchema::builder("demo.layout", "Layout")
        .default_size(CanvasSize {
            width: 220.0,
            height: 140.0,
        })
        .layout_budget(budget.clone())
        .port(
            PortDecl::data_input("source")
                .with_view_anchor("field.source")
                .with_view_order(1),
        )
        .surface_slot(
            NodeSurfaceSlotDescriptor::field_row("field.source")
                .with_anchor("field.source")
                .with_slot("source"),
        )
        .build();

    let encoded = serde_json::to_value(&schema).expect("serialize schema");
    assert_eq!(encoded["layout_budget"]["slot_line_budget"], json!(4));
    assert_eq!(
        encoded["layout_budget"]["overflow_indicator"],
        json!("summary")
    );

    let decoded: NodeSchema = serde_json::from_value(encoded).expect("deserialize schema");
    assert_eq!(decoded.layout_budget, budget);
    assert_eq!(
        decoded.ports[0].view.anchor.as_deref(),
        Some("field.source")
    );
    assert_eq!(
        decoded.surface_slots[0].anchor.as_deref(),
        Some("field.source")
    );

    let mut registry = NodeRegistry::new();
    registry.register(decoded);
    let descriptor = registry
        .view_descriptor(&NodeKindKey::new("demo.layout"))
        .expect("descriptor");
    assert_eq!(descriptor.layout_budget, budget);
    assert_eq!(
        descriptor.ports[0].view.anchor.as_deref(),
        Some("field.source")
    );
    assert_eq!(
        descriptor.surface_slots[0].anchor.as_deref(),
        Some("field.source")
    );
}

#[test]
fn port_view_descriptors_cover_sides_anchors_and_visibility() {
    let schema = NodeSchema::builder("demo.table", "Table")
        .port(
            PortDecl::data_input("filter")
                .on_top()
                .with_view_order(0)
                .with_label("Filter"),
        )
        .port(
            PortDecl::data_output("rows")
                .on_right()
                .with_view_group("result")
                .with_view_order(1)
                .with_label("Rows"),
        )
        .port(
            PortDecl::data_input("field.id")
                .with_view(
                    PortViewDescriptor::left()
                        .with_anchor("field.id")
                        .with_lane("fields")
                        .with_slot("id")
                        .with_label("id")
                        .with_icon_key("key"),
                )
                .hidden_handle(),
        )
        .port(
            PortDecl::data_output("summary")
                .on_bottom()
                .with_view_anchor("footer.summary")
                .with_view_order(2),
        )
        .build();

    let mut registry = NodeRegistry::new();
    registry.register(schema);
    let descriptor = registry
        .view_descriptor(&NodeKindKey::new("demo.table"))
        .expect("descriptor");

    assert_eq!(descriptor.ports.len(), 4);
    assert_eq!(descriptor.ports[0].view.side, Some(PortViewSide::Top));
    assert_eq!(descriptor.ports[1].view.side, Some(PortViewSide::Right));
    assert_eq!(descriptor.ports[1].view.group.as_deref(), Some("result"));
    assert_eq!(descriptor.ports[2].view.side, Some(PortViewSide::Left));
    assert_eq!(descriptor.ports[2].view.anchor.as_deref(), Some("field.id"));
    assert_eq!(descriptor.ports[2].view.lane.as_deref(), Some("fields"));
    assert_eq!(descriptor.ports[2].view.slot.as_deref(), Some("id"));
    assert_eq!(
        descriptor.ports[2].view.visibility,
        Some(PortHandleVisibility::Hidden)
    );
    assert_eq!(descriptor.ports[3].view.side, Some(PortViewSide::Bottom));
    assert_eq!(
        descriptor.ports[3].view.anchor.as_deref(),
        Some("footer.summary")
    );
}

#[test]
fn builder_helpers_match_explicit_port_view_descriptor_construction() {
    let explicit = PortDecl::data_output("result").with_view(PortViewDescriptor {
        side: Some(PortViewSide::Right),
        order: Some(3),
        group: Some("output".to_owned()),
        anchor: Some("row.result".to_owned()),
        lane: None,
        slot: None,
        label: None,
        icon_key: None,
        visibility: None,
    });
    let helper = PortDecl::data_output("result")
        .on_right()
        .with_view_order(3)
        .with_view_group("output")
        .with_view_anchor("row.result");

    assert_eq!(helper, explicit);
}

#[test]
fn node_surface_slot_descriptors_cover_semantic_slots_without_framework_widgets() {
    let schema = NodeSchema::builder("demo.workflow_card", "Workflow Card")
        .surface_slot(NodeSurfaceSlotDescriptor::header("header.main").with_order(0))
        .surface_slot(
            NodeSurfaceSlotDescriptor::badge("badge.status")
                .with_label("Status")
                .with_renderer_key("status-badge")
                .with_visibility(NodeSurfaceSlotVisibility::Visible),
        )
        .surface_slot(
            NodeSurfaceSlotDescriptor::action_row("actions.primary")
                .with_label("Actions")
                .collapsed(),
        )
        .surface_slot(
            NodeSurfaceSlotDescriptor::status_banner("status.validation")
                .with_label("Validation")
                .with_slot("status.validation")
                .with_renderer_key("status-banner"),
        )
        .surface_slot(
            NodeSurfaceSlotDescriptor::config_group("config.model")
                .with_label("Model config")
                .with_slot("config.model"),
        )
        .surface_slot(
            NodeSurfaceSlotDescriptor::port_rail("rail.typed_inputs")
                .with_label("Inputs")
                .with_anchor("rail.typed_inputs"),
        )
        .surface_slot(
            NodeSurfaceSlotDescriptor::metric_badge("metric.cost")
                .with_label("Cost")
                .with_slot("metrics.cost"),
        )
        .build();

    let mut registry = NodeRegistry::new();
    registry.register(schema);
    let descriptor = registry
        .view_descriptor(&NodeKindKey::new("demo.workflow_card"))
        .expect("descriptor");

    assert_eq!(descriptor.surface_slots.len(), 7);
    assert_eq!(
        descriptor.surface_slots[0],
        NodeSurfaceSlotDescriptor::header("header.main").with_order(0)
    );
    assert_eq!(descriptor.surface_slots[1].kind, NodeSurfaceSlotKind::Badge);
    assert_eq!(
        descriptor.surface_slots[1].renderer_key.as_deref(),
        Some("status-badge")
    );
    assert_eq!(
        descriptor.surface_slots[2].visibility,
        Some(NodeSurfaceSlotVisibility::Collapsed)
    );
    assert_eq!(
        descriptor.surface_slots[3].kind,
        NodeSurfaceSlotKind::StatusBanner
    );
    assert_eq!(
        descriptor.surface_slots[4].kind,
        NodeSurfaceSlotKind::ConfigGroup
    );
    assert_eq!(
        descriptor.surface_slots[5].kind,
        NodeSurfaceSlotKind::PortRail
    );
    assert_eq!(
        descriptor.surface_slots[6].kind,
        NodeSurfaceSlotKind::MetricBadge
    );
}

#[test]
fn node_control_descriptors_cover_authoring_controls_without_framework_widgets() {
    let controls = vec![
        NodeControlDescriptor::text_input("control.title")
            .with_label("Title")
            .with_binding(NodeControlBinding::data_path("title"))
            .required(),
        NodeControlDescriptor::select("control.model")
            .with_label("Model")
            .with_binding(NodeControlBinding::slot("config.model"))
            .with_options([
                NodeControlOption::new("gpt-4.1", "GPT 4.1"),
                NodeControlOption::new("local", "Local").disabled(),
            ]),
        NodeControlDescriptor::toggle("control.stream")
            .with_label("Stream")
            .with_binding(NodeControlBinding::data_path("config.stream")),
        NodeControlDescriptor::code("control.system_prompt")
            .with_label("System")
            .with_binding(NodeControlBinding::json_pointer("/prompts/system"))
            .with_language("jinja"),
        NodeControlDescriptor::color("control.tint")
            .with_label("Tint")
            .with_binding(NodeControlBinding::data_path("style.tint")),
        NodeControlDescriptor::asset("control.texture")
            .with_label("Texture")
            .with_binding(NodeControlBinding::data_path("asset.texture"))
            .with_option_source(NodeControlOptionSource::Assets),
        NodeControlDescriptor::variable_picker("control.variable")
            .with_label("Variable")
            .with_binding(NodeControlBinding::graph_symbol("inputs.topic"))
            .with_option_source(NodeControlOptionSource::Variables),
        NodeControlDescriptor::text_area("control.prompt")
            .with_label("Prompt")
            .with_binding(NodeControlBinding::slot("prompt"))
            .with_placeholder("Write the prompt"),
    ];

    let encoded = serde_json::to_value(&controls).expect("serialize controls");
    let decoded: Vec<NodeControlDescriptor> =
        serde_json::from_value(encoded).expect("deserialize controls");

    assert_eq!(decoded, controls);
    assert_eq!(decoded[0].kind, NodeControlKind::TextInput);
    assert_eq!(decoded[1].options[1].label, "Local");
    assert!(decoded[1].options[1].disabled);
    assert_eq!(decoded[3].presentation.language.as_deref(), Some("jinja"));
    assert!(decoded[7].presentation.multiline);
}

#[test]
fn node_control_projection_preserves_binding_validation_and_editability() {
    let schema = NodeSchema::builder("demo.authoring", "Authoring")
        .surface_slot(
            NodeSurfaceSlotDescriptor::field_row("field.prompt")
                .with_label("Prompt")
                .with_slot("prompt")
                .with_order(0)
                .with_control(
                    NodeControlDescriptor::text_area("control.prompt")
                        .with_label("Prompt")
                        .with_binding(NodeControlBinding::slot("prompt"))
                        .with_validation(
                            NodeControlValidation::default()
                                .required()
                                .with_rule(NodeControlValidationRule::Regex {
                                    pattern: ".+".to_owned(),
                                })
                                .with_rule(NodeControlValidationRule::EnumValues {
                                    values: vec![json!("short"), json!("long")],
                                })
                                .with_rule(NodeControlValidationRule::Range {
                                    min: Some(1.0),
                                    max: Some(4096.0),
                                })
                                .with_rule(NodeControlValidationRule::ExpressionShape {
                                    language: Some("jinja".to_owned()),
                                }),
                        )
                        .with_editability(
                            NodeControlEditability::default()
                                .read_only()
                                .disabled("Prompt is inherited"),
                        ),
                ),
        )
        .surface_slot(
            NodeSurfaceSlotDescriptor::config_group("config.model")
                .with_label("Model")
                .with_slot("config.model")
                .with_order(1)
                .with_controls([
                    NodeControlDescriptor::select("control.model")
                        .with_label("Model")
                        .with_binding(NodeControlBinding::data_path("config.model"))
                        .with_options([
                            NodeControlOption::new("small", "Small"),
                            NodeControlOption::new("large", "Large"),
                        ]),
                    NodeControlDescriptor::slider("control.temperature")
                        .with_label("Temperature")
                        .with_binding(NodeControlBinding::data_path("config.temperature"))
                        .with_validation_rule(NodeControlValidationRule::Range {
                            min: Some(0.0),
                            max: Some(2.0),
                        })
                        .with_presentation(
                            NodeControlPresentation::default()
                                .with_unit("temp")
                                .compact_label(),
                        ),
                    NodeControlDescriptor::port_binding("control.prompt_port")
                        .with_label("Prompt port")
                        .with_binding(NodeControlBinding::port_anchor("field.prompt")),
                ]),
        )
        .build();
    let mut registry = NodeRegistry::new();
    registry.register(schema);
    let descriptor = registry
        .view_descriptor(&NodeKindKey::new("demo.authoring"))
        .expect("descriptor");
    let node_data = json!({
        "prompt": "Summarize {{topic}}",
        "config": {
            "model": "large",
            "temperature": 0.7
        }
    });

    let compact = descriptor.surface_slots_projection(
        &node_data,
        Some(&crate::schema::kit::NodeKitLayoutHints::default().with_zoom_range(0.3, 0.9)),
        0.1,
    );

    assert_eq!(compact.len(), 2);
    assert_eq!(compact[0].value, "Summarize {{topic}}");
    assert_eq!(compact[0].controls[0].key, "control.prompt");
    assert_eq!(compact[0].controls[0].data_key(), Some("prompt"));
    assert_eq!(compact[0].controls[0].validation.rules.len(), 5);
    assert!(compact[0].controls[0].editability.read_only);
    assert_eq!(
        compact[0].controls[0]
            .editability
            .disabled_reason
            .as_deref(),
        Some("Prompt is inherited")
    );
    assert_eq!(compact[1].controls.len(), 3);
    assert_eq!(compact[1].controls[0].data_key(), Some("config.model"));
    assert_eq!(
        compact[1].controls[1].presentation.unit.as_deref(),
        Some("temp")
    );
    assert_eq!(compact[1].controls[2].data_key(), None);
}

#[test]
fn repeatable_collections_project_stable_item_slots_and_anchors() {
    let schema = NodeSchema::builder("demo.shader.dynamic", "Dynamic shader")
        .repeatable_collection(
            NodeRepeatableCollectionDescriptor::new("shader.inputs", "inputs", "id")
                .with_label("Inputs")
                .with_empty_label("No inputs")
                .with_item_template_slot(
                    NodeSurfaceSlotDescriptor::field_row("input")
                        .with_label("Input")
                        .with_slot("inputs")
                        .with_control(
                            NodeControlDescriptor::select("control.input.type")
                                .with_label("Type")
                                .with_binding(NodeControlBinding::data_path("ty"))
                                .with_options([
                                    NodeControlOption::new("float", "Float"),
                                    NodeControlOption::new("vec4", "Vec4"),
                                ]),
                        ),
                )
                .with_anchor_rule(
                    NodeRepeatableAnchorRule::new("rail.input", "rail.input")
                        .with_port_key_path("port"),
                )
                .with_min_items(1)
                .with_max_items(3)
                .reorderable()
                .with_add_action("action.input.add")
                .with_remove_action("action.input.remove")
                .with_reorder_action("action.input.reorder"),
        )
        .build();
    let mut registry = NodeRegistry::new();
    registry.register(schema);
    let descriptor = registry
        .view_descriptor(&NodeKindKey::new("demo.shader.dynamic"))
        .expect("descriptor");
    let before = json!({
        "inputs": [
            { "id": "albedo", "name": "Albedo", "ty": "vec4", "port": "in_albedo" },
            { "id": "roughness", "name": "Roughness", "ty": "float", "port": "in_roughness" }
        ]
    });
    let after_add = json!({
        "inputs": [
            { "id": "albedo", "name": "Albedo", "ty": "vec4", "port": "in_albedo" },
            { "id": "roughness", "name": "Roughness", "ty": "float", "port": "in_roughness" },
            { "id": "normal", "name": "Normal", "ty": "vec4", "port": "in_normal" }
        ]
    });
    let after_reorder = json!({
        "inputs": [
            { "id": "roughness", "name": "Roughness", "ty": "float", "port": "in_roughness" },
            { "id": "albedo", "name": "Albedo", "ty": "vec4", "port": "in_albedo" },
            { "id": "normal", "name": "Normal", "ty": "vec4", "port": "in_normal" }
        ]
    });

    let before_items = descriptor.repeatable_items_projection(&before, "shader.inputs");
    let added_items = descriptor.repeatable_items_projection(&after_add, "shader.inputs");
    let reordered_items = descriptor.repeatable_items_projection(&after_reorder, "shader.inputs");

    assert_eq!(before_items.len(), 2);
    assert_eq!(before_items[0].slot_key, "rail.input.albedo");
    assert_eq!(before_items[0].anchor, "rail.input.albedo");
    assert_eq!(before_items[0].port_key.as_deref(), Some("in_albedo"));
    assert_eq!(before_items[0].slots[0].key, "rail.input.albedo.input");
    assert_eq!(
        before_items[0].slots[0].anchor.as_deref(),
        Some("rail.input.albedo.input")
    );
    assert_eq!(before_items[0].slots[0].controls[0].data_key(), Some("ty"));
    assert_eq!(added_items[0].anchor, "rail.input.albedo");
    assert_eq!(added_items[1].anchor, "rail.input.roughness");
    assert_eq!(added_items[2].anchor, "rail.input.normal");
    assert_eq!(reordered_items[0].anchor, "rail.input.roughness");
    assert_eq!(reordered_items[1].anchor, "rail.input.albedo");

    let numeric_id = json!({
        "inputs": [
            { "id": 42, "name": "Numeric", "ty": "float", "port": 99 }
        ]
    });
    let numeric_items = descriptor.repeatable_items_projection(&numeric_id, "shader.inputs");
    assert_eq!(numeric_items.len(), 1);
    assert_eq!(numeric_items[0].item_id, "42");
    assert_eq!(numeric_items[0].anchor, "rail.input.42");
    assert_eq!(numeric_items[0].port_key.as_deref(), Some("99"));

    let collection = descriptor
        .repeatable_collection("shader.inputs")
        .expect("collection");
    assert_eq!(
        collection.add_disabled_reason(&after_add).as_deref(),
        Some("Maximum of 3 items reached")
    );
    assert_eq!(
        collection
            .remove_disabled_reason(&json!({ "inputs": [{ "id": "albedo" }] }))
            .as_deref(),
        Some("Minimum of 1 items required")
    );
    assert!(collection.is_empty_for(&json!({ "inputs": [] })));
    assert!(
        descriptor
            .repeatable_items_projection(&json!({ "inputs": [] }), "shader.inputs")
            .is_empty(),
        "empty collections must not create fake item slots or handles"
    );
}

#[test]
fn action_menu_inspector_and_blackboard_descriptors_are_headless() {
    let insert_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)
    .with_icon_key("sparkles");
    let disabled_remove = NodeActionDescriptor::new(
        "action.param.remove",
        "Remove parameter",
        ActionTarget::RepeatableItem {
            collection_key: "params".to_owned(),
            item_id: "temperature".to_owned(),
        },
        ActionIntent::RemoveRepeatableItem {
            collection_key: "params".to_owned(),
            item_id: "temperature".to_owned(),
        },
    )
    .disabled("Minimum of 1 items required")
    .danger()
    .with_shortcut(ActionShortcut::new("Backspace").shift());
    let schema = NodeSchema::builder("demo.authoring_actions", "Actions")
        .action(insert_action.clone())
        .action(disabled_remove.clone())
        .menu(
            MenuDescriptor::new("menu.dropped_wire", MenuSurface::DroppedWire)
                .with_label("Insert compatible node")
                .with_action_key("action.insert.llm"),
        )
        .menu(
            MenuDescriptor::new("menu.node", MenuSurface::Node)
                .with_action_keys(["action.param.remove"]),
        )
        .inspector(
            InspectorDescriptor::new(
                "inspector.param.temperature",
                InspectorTarget::RepeatableItem {
                    collection_key: "params".to_owned(),
                    item_id: "temperature".to_owned(),
                },
            )
            .with_label("Temperature")
            .with_control(
                NodeControlDescriptor::slider("control.temperature")
                    .with_label("Temperature")
                    .with_binding(NodeControlBinding::data_path("value")),
            )
            .with_action_key("action.param.remove"),
        )
        .blackboard(BlackboardDescriptor::new(
            "blackboard.shader_properties",
            "Shader properties",
            NodeRepeatableCollectionDescriptor::new("shader.properties", "properties", "id")
                .with_item_template_slot(NodeSurfaceSlotDescriptor::field_row("property")),
        ))
        .build();
    let mut registry = NodeRegistry::new();
    registry.register(schema);
    let descriptor = registry
        .view_descriptor(&NodeKindKey::new("demo.authoring_actions"))
        .expect("descriptor");

    assert_eq!(descriptor.actions.len(), 2);
    assert!(
        descriptor
            .action("action.insert.llm")
            .expect("insert")
            .is_enabled()
    );
    assert_eq!(
        descriptor
            .action("action.param.remove")
            .expect("remove")
            .availability,
        ActionAvailability::disabled("Minimum of 1 items required")
    );
    assert_eq!(
        descriptor
            .menu("menu.dropped_wire")
            .expect("dropped menu")
            .action_keys,
        vec!["action.insert.llm"]
    );
    assert_eq!(
        descriptor
            .inspector("inspector.param.temperature")
            .expect("inspector")
            .controls[0]
            .data_key(),
        Some("value")
    );
    assert_eq!(
        descriptor
            .blackboard("blackboard.shader_properties")
            .expect("blackboard")
            .collection
            .key,
        "shader.properties"
    );

    let encoded = serde_json::to_value(&descriptor).expect("serialize descriptor");
    let decoded: crate::schema::NodeKindViewDescriptor =
        serde_json::from_value(encoded).expect("deserialize descriptor");
    assert_eq!(decoded.actions, descriptor.actions);
    assert_eq!(decoded.menus, descriptor.menus);
    assert_eq!(decoded.inspectors, descriptor.inspectors);
    assert_eq!(decoded.blackboards, descriptor.blackboards);
}

#[test]
fn node_chrome_descriptors_cover_adapter_owned_chrome_without_framework_widgets() {
    let schema = NodeSchema::builder("demo.llm", "LLM")
        .renderer_key("decision-card")
        .default_size(CanvasSize {
            width: 228.0,
            height: 196.0,
        })
        .chrome(NodeChromeDescriptor::resizer("resize.corner"))
        .chrome(
            NodeChromeDescriptor::toolbar("toolbar.primary", NodeChromePlacement::TopRight)
                .with_label("Tools")
                .with_renderer_key("node-toolbar")
                .with_icon_key("wrench")
                .with_order(10),
        )
        .chrome(
            NodeChromeDescriptor::status_strip("status.run", NodeChromePlacement::InsideFooter)
                .with_label("Run status")
                .with_renderer_key("run-status")
                .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),
        )
        .chrome(
            NodeChromeDescriptor::validation_banner(
                "validation.warning",
                NodeChromePlacement::InsideHeader,
            )
            .with_label("Warning")
            .hidden(),
        )
        .build();

    let mut registry = NodeRegistry::new();
    registry.register(schema);
    let descriptor = registry
        .view_descriptor(&NodeKindKey::new("demo.llm"))
        .expect("descriptor");

    assert_eq!(descriptor.chrome.len(), 5);
    assert_eq!(descriptor.chrome[0].kind, NodeChromeKind::Resizer);
    assert_eq!(
        descriptor.chrome[0].effective_visibility(),
        NodeChromeVisibility::Selected
    );
    assert!(descriptor.chrome[0].interactive);
    assert!(!descriptor.chrome[0].is_visible_for_state(false, false, false));
    assert!(descriptor.chrome[0].is_visible_for_state(true, false, false));

    let toolbar = &descriptor.chrome[1];
    assert_eq!(toolbar.kind, NodeChromeKind::Toolbar);
    assert_eq!(toolbar.placement, NodeChromePlacement::TopRight);
    assert_eq!(toolbar.label.as_deref(), Some("Tools"));
    assert_eq!(toolbar.renderer_key.as_deref(), Some("node-toolbar"));
    assert_eq!(toolbar.icon_key.as_deref(), Some("wrench"));
    assert!(toolbar.interactive);

    let status = &descriptor.chrome[2];
    assert_eq!(status.kind, NodeChromeKind::StatusStrip);
    assert_eq!(status.placement, NodeChromePlacement::InsideFooter);
    assert_eq!(status.effective_visibility(), NodeChromeVisibility::Always);
    assert!(status.is_visible_for_state(false, false, false));
    assert!(!status.interactive);

    let run = &descriptor.chrome[3];
    assert_eq!(run.kind, NodeChromeKind::RunActionStrip);
    assert_eq!(run.effective_visibility(), NodeChromeVisibility::Selected);
    assert!(run.interactive);

    let validation = &descriptor.chrome[4];
    assert_eq!(validation.kind, NodeChromeKind::ValidationBanner);
    assert_eq!(
        validation.effective_visibility(),
        NodeChromeVisibility::Hidden
    );
    assert!(!validation.is_visible_for_state(true, true, true));
}

#[test]
fn node_surface_projection_uses_layout_hints_for_density_and_slot_limits() {
    let schema = NodeSchema::builder("demo.surface", "Surface")
        .surface_slot(NodeSurfaceSlotDescriptor::header("header.main").with_order(0))
        .surface_slot(
            NodeSurfaceSlotDescriptor::field_row("field.tags")
                .with_label("Tags")
                .with_order(1),
        )
        .surface_slot(NodeSurfaceSlotDescriptor::action_row("actions.primary").with_order(2))
        .build();
    let mut registry = NodeRegistry::new();
    registry.register(schema);
    let descriptor = registry
        .view_descriptor(&NodeKindKey::new("demo.surface"))
        .expect("descriptor");
    let node_data = json!({
        "tags": ["root", "fallback"],
        "fields": {
            "tags": ["alpha", "beta", "gamma"]
        }
    });

    let compact = descriptor.surface_slots_projection(
        &node_data,
        Some(&crate::schema::kit::NodeKitLayoutHints::default().with_zoom_range(0.3, 0.9)),
        0.1,
    );
    let regular = descriptor.surface_slots_projection(
        &node_data,
        Some(&crate::schema::kit::NodeKitLayoutHints::default().with_zoom_range(0.3, 0.9)),
        0.5,
    );
    let full = descriptor.surface_slots_projection(
        &node_data,
        Some(&crate::schema::kit::NodeKitLayoutHints::default().with_zoom_range(0.3, 0.9)),
        1.0,
    );

    assert_eq!(compact.len(), 2);
    assert_eq!(regular.len(), 3);
    assert_eq!(full.len(), 3);
    assert_eq!(full[0].kind, NodeSurfaceSlotKind::Header);
    assert_eq!(compact[0].label, "main");
    assert_eq!(compact[1].label, "Tags");
    assert_eq!(compact[1].value, "alpha");
    assert_eq!(regular[1].value, "alpha");
    assert_eq!(full[1].value, "alpha · beta …");

    let projection = NodeSurfaceProjection::from_layout_hints(
        &crate::schema::kit::NodeKitLayoutHints::default().with_zoom_range(0.3, 0.9),
        1.0,
    );
    assert_eq!(projection.density, NodeKitContentDensity::Full);
}

#[test]
fn node_kind_view_descriptor_resolves_ports_and_slots_by_anchor() {
    let schema = NodeSchema::builder("demo.review_card", "Review Card")
        .port(
            PortDecl::data_input("assignee")
                .with_label("Assignee")
                .on_left()
                .with_view_anchor("field.assignee")
                .with_view_order(1),
        )
        .port(
            PortDecl::data_output("result")
                .with_label("Result")
                .on_right()
                .with_view_anchor("actions.primary")
                .with_view_order(0),
        )
        .surface_slot(
            NodeSurfaceSlotDescriptor::field_row("field.status")
                .with_label("Status")
                .with_anchor("field.status")
                .with_order(1),
        )
        .surface_slot(
            NodeSurfaceSlotDescriptor::field_row("field.assignee")
                .with_label("Assignee")
                .with_anchor("field.assignee")
                .with_order(0),
        )
        .surface_slot(
            NodeSurfaceSlotDescriptor::action_row("actions.primary")
                .with_label("Actions")
                .with_anchor("actions.primary")
                .with_order(2),
        )
        .build();

    let mut registry = NodeRegistry::new();
    registry.register(schema);
    let descriptor = registry
        .view_descriptor(&NodeKindKey::new("demo.review_card"))
        .expect("descriptor");

    assert_eq!(
        descriptor
            .port_decl("assignee")
            .map(|decl| decl.key.0.as_str()),
        Some("assignee")
    );
    assert_eq!(
        descriptor
            .port_decl_by_anchor("field.assignee")
            .map(|decl| decl.key.0.as_str()),
        Some("assignee")
    );
    assert_eq!(
        descriptor
            .surface_slot("field.assignee")
            .map(|slot| slot.display_label()),
        Some(Some("Assignee"))
    );

    let slots = descriptor.surface_slots_of_kind(NodeSurfaceSlotKind::FieldRow);
    assert_eq!(slots[0].key, "field.assignee");
    assert_eq!(slots[1].key, "field.status");
    assert_eq!(
        descriptor
            .surface_slot_by_anchor("actions.primary")
            .map(|slot| slot.key.as_str()),
        Some("actions.primary")
    );
}

#[test]
fn slot_and_port_visibility_helpers_follow_adapter_contract() {
    let hidden_port = PortViewDescriptor::left().hidden();
    let collapsed_port = PortViewDescriptor::right().collapsed();
    let hidden_slot = NodeSurfaceSlotDescriptor::badge("badge.priority").hidden();
    let collapsed_slot = NodeSurfaceSlotDescriptor::action_row("actions.primary").collapsed();

    assert!(hidden_port.is_hidden());
    assert!(collapsed_port.is_collapsed());
    assert!(hidden_port.is_hidden_or_collapsed());
    assert_eq!(
        hidden_port.resolved_side(PortDirection::Out),
        PortViewSide::Left
    );
    assert!(hidden_slot.is_hidden());
    assert!(collapsed_slot.is_collapsed());
    assert!(hidden_slot.is_hidden_or_collapsed());
    assert_eq!(hidden_slot.order_key(), i32::MAX);
    assert_eq!(hidden_slot.key_tail(), Some("priority"));
    assert_eq!(collapsed_slot.display_label(), Some("primary"));
    assert!(hidden_slot.is_hidden());
    assert!(collapsed_slot.is_collapsed());
}
