use super::drag_drop_state::TreeShellDragEffect;
use super::filetree_runtime::{
    FileTreeIntentEvent, FileTreeKeyboardCommand, FileTreeRuntimeAction,
    FileTreeRuntimeEnvironment, FileTreeRuntimeOutput, FileTreeRuntimeState,
};
use super::page_runtime::{
    PageTreeCommandDispatchEvent, PageTreeDropFeedback, PageTreeDropPosition, PageTreeIntentEvent,
    PageTreeRuntimeAction, PageTreeRuntimeEnvironment, PageTreeRuntimeOutput, PageTreeRuntimeState,
};
use super::picker_runtime::{
    PickerRuntimeAction, PickerRuntimeEnvironment, PickerRuntimeOutput, PickerRuntimePickTarget,
    PickerRuntimeState,
};
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum TreeShellRuntimeMode {
    Page,
    FileTree,
    Picker,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(
    tag = "mode",
    rename_all = "camelCase",
    rename_all_fields = "camelCase"
)]
pub enum TreeShellRuntimeRequest {
    Page {
        request_id: String,
        environment: PageTreeRuntimeEnvironment,
        state: PageTreeRuntimeState,
        action: PageTreeRuntimeAction,
    },
    FileTree {
        request_id: String,
        environment: FileTreeRuntimeEnvironment,
        state: FileTreeRuntimeState,
        action: FileTreeRuntimeAction,
    },
    Picker {
        request_id: String,
        environment: PickerRuntimeEnvironment,
        state: PickerRuntimeState,
        action: PickerRuntimeAction,
    },
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TreeShellRuntimeResult {
    pub request_id: String,
    pub mode: TreeShellRuntimeMode,
    pub state: TreeShellRuntimeStateSnapshot,
    pub dom_patches: Vec<TreeShellDomPatch>,
    pub host_events: Vec<TreeShellHostEvent>,
    pub command_events: Vec<TreeShellCommandEvent>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "mode", content = "state", rename_all = "camelCase")]
pub enum TreeShellRuntimeStateSnapshot {
    Page(PageTreeRuntimeState),
    FileTree(FileTreeRuntimeState),
    Picker(PickerRuntimeState),
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(
    tag = "kind",
    rename_all = "camelCase",
    rename_all_fields = "camelCase"
)]
pub enum TreeShellDomPatch {
    PageState {
        focused_id: Option<String>,
        expanded_ids: BTreeSet<String>,
        drop_feedback: Option<PageTreeDropFeedback>,
    },
    FileTreeState {
        active_row_id: Option<String>,
        selected_row_ids: BTreeSet<String>,
        anchor_row_id: Option<String>,
        focused_row_id: Option<String>,
        drag_row_ids: Vec<String>,
        drag_effect: Option<TreeShellDragEffect>,
        drop_target_row_id: Option<String>,
    },
    PickerState {
        active_item_key: Option<String>,
        focus_dom: bool,
    },
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(
    tag = "kind",
    rename_all = "camelCase",
    rename_all_fields = "camelCase"
)]
pub enum TreeShellHostEvent {
    PageOpen {
        node_id: String,
    },
    PageContextMenu {
        node_id: String,
    },
    FileTreeOpen {
        target: super::filetree_runtime::FileTreeOpenTarget,
    },
    FileTreeContextMenu {
        row_id: String,
        target: super::filetree_runtime::FileTreeOpenTarget,
    },
    FileTreeInternalDrop {
        target_row_id: Option<String>,
        target: Option<super::filetree_runtime::FileTreeOpenTarget>,
        row_ids: Vec<String>,
        copy: bool,
    },
    FileTreeExternalDrop {
        target_row_id: Option<String>,
        target: Option<super::filetree_runtime::FileTreeOpenTarget>,
        file_count: u32,
    },
    FileTreeKeyboardCommand {
        command: FileTreeKeyboardCommand,
        row_ids: Vec<String>,
        target_row_id: Option<String>,
    },
    PickerPickRoot,
    PickerPickDocument {
        document_id: String,
    },
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(
    tag = "kind",
    rename_all = "camelCase",
    rename_all_fields = "camelCase"
)]
pub enum TreeShellCommandEvent {
    CreateNode {
        command_name: String,
        parent_node_id: Option<String>,
    },
    RenameNode {
        command_name: String,
        node_id: String,
        title: String,
    },
    MoveSubtree {
        command_name: String,
        source_node_id: String,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        target_node_id: Option<String>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        target_parent_id: Option<String>,
        position: PageTreeDropPosition,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        sort_order: Option<i64>,
    },
    CopyResource {
        command_name: String,
        source_asset_ids: Vec<String>,
        target_document_id: String,
    },
    MoveResource {
        command_name: String,
        source_asset_ids: Vec<String>,
        target_document_id: String,
    },
    UploadResource {
        command_name: String,
        target_document_id: String,
        file_count: u32,
    },
    ResourceLifecycle {
        command_name: String,
        resource_kind: String,
        asset_id: String,
    },
}

pub fn reduce_tree_shell_runtime(request: TreeShellRuntimeRequest) -> TreeShellRuntimeResult {
    match request {
        TreeShellRuntimeRequest::Page {
            request_id,
            environment,
            state,
            action,
        } => {
            let transition = state.reduce(&environment, action);
            let dom_patches = transition
                .outputs
                .iter()
                .filter_map(|output| match output {
                    PageTreeRuntimeOutput::DomPatch => Some(TreeShellDomPatch::PageState {
                        focused_id: transition.state.focused_id.clone(),
                        expanded_ids: transition.state.expanded_ids.clone(),
                        drop_feedback: transition.state.drop_feedback.clone(),
                    }),
                    _ => None,
                })
                .collect();
            let host_events = transition
                .outputs
                .iter()
                .filter_map(|output| match output {
                    PageTreeRuntimeOutput::Intent(PageTreeIntentEvent::Open { node_id }) => {
                        Some(TreeShellHostEvent::PageOpen {
                            node_id: node_id.clone(),
                        })
                    }
                    PageTreeRuntimeOutput::Intent(PageTreeIntentEvent::ContextMenu { node_id }) => {
                        Some(TreeShellHostEvent::PageContextMenu {
                            node_id: node_id.clone(),
                        })
                    }
                    _ => None,
                })
                .collect();
            let command_events = transition
                .outputs
                .iter()
                .filter_map(|output| match output {
                    PageTreeRuntimeOutput::CommandDispatch(
                        PageTreeCommandDispatchEvent::CreateNode {
                            command_name,
                            parent_node_id,
                        },
                    ) => Some(TreeShellCommandEvent::CreateNode {
                        command_name: command_name.to_string(),
                        parent_node_id: parent_node_id.clone(),
                    }),
                    PageTreeRuntimeOutput::CommandDispatch(
                        PageTreeCommandDispatchEvent::RenameNode {
                            command_name,
                            node_id,
                            title,
                        },
                    ) => Some(TreeShellCommandEvent::RenameNode {
                        command_name: command_name.to_string(),
                        node_id: node_id.clone(),
                        title: title.clone(),
                    }),
                    PageTreeRuntimeOutput::CommandDispatch(
                        PageTreeCommandDispatchEvent::MoveSubtree {
                            command_name,
                            source_node_id,
                            target_node_id,
                            target_parent_id,
                            position,
                            sort_order,
                        },
                    ) => Some(TreeShellCommandEvent::MoveSubtree {
                        command_name: command_name.to_string(),
                        source_node_id: source_node_id.clone(),
                        target_node_id: target_node_id.clone(),
                        target_parent_id: target_parent_id.clone(),
                        position: *position,
                        sort_order: *sort_order,
                    }),
                    _ => None,
                })
                .collect();

            TreeShellRuntimeResult {
                request_id,
                mode: TreeShellRuntimeMode::Page,
                state: TreeShellRuntimeStateSnapshot::Page(transition.state),
                dom_patches,
                host_events,
                command_events,
            }
        }
        TreeShellRuntimeRequest::FileTree {
            request_id,
            environment,
            state,
            action,
        } => {
            let transition = state.reduce(&environment, action);
            let dom_patches = transition
                .outputs
                .iter()
                .filter_map(|output| match output {
                    FileTreeRuntimeOutput::DomPatch => Some(TreeShellDomPatch::FileTreeState {
                        active_row_id: transition.state.active_row_id.clone(),
                        selected_row_ids: transition.state.selection.selected_row_ids.clone(),
                        anchor_row_id: transition.state.selection.anchor_row_id.clone(),
                        focused_row_id: transition.state.selection.focused_row_id.clone(),
                        drag_row_ids: transition.state.drag_row_ids.clone(),
                        drag_effect: transition.state.drag_effect,
                        drop_target_row_id: transition.state.drop_target_row_id.clone(),
                    }),
                    _ => None,
                })
                .collect();
            let host_events = transition
                .outputs
                .iter()
                .filter_map(|output| match output {
                    FileTreeRuntimeOutput::Intent(FileTreeIntentEvent::Open { target }) => {
                        Some(TreeShellHostEvent::FileTreeOpen {
                            target: target.clone(),
                        })
                    }
                    FileTreeRuntimeOutput::Intent(FileTreeIntentEvent::ContextMenu {
                        row_id,
                        target,
                    }) => Some(TreeShellHostEvent::FileTreeContextMenu {
                        row_id: row_id.clone(),
                        target: target.clone(),
                    }),
                    FileTreeRuntimeOutput::Intent(FileTreeIntentEvent::InternalDrop {
                        target_row_id,
                        target,
                        row_ids,
                        copy,
                    }) => Some(TreeShellHostEvent::FileTreeInternalDrop {
                        target_row_id: target_row_id.clone(),
                        target: target.clone(),
                        row_ids: row_ids.clone(),
                        copy: *copy,
                    }),
                    FileTreeRuntimeOutput::Intent(FileTreeIntentEvent::ExternalDrop {
                        target_row_id,
                        target,
                        file_count,
                    }) => Some(TreeShellHostEvent::FileTreeExternalDrop {
                        target_row_id: target_row_id.clone(),
                        target: target.clone(),
                        file_count: *file_count,
                    }),
                    FileTreeRuntimeOutput::Intent(FileTreeIntentEvent::KeyboardCommand {
                        command,
                        row_ids,
                        target_row_id,
                    }) => Some(TreeShellHostEvent::FileTreeKeyboardCommand {
                        command: *command,
                        row_ids: row_ids.clone(),
                        target_row_id: target_row_id.clone(),
                    }),
                    _ => None,
                })
                .collect();

            TreeShellRuntimeResult {
                request_id,
                mode: TreeShellRuntimeMode::FileTree,
                state: TreeShellRuntimeStateSnapshot::FileTree(transition.state),
                dom_patches,
                host_events,
                command_events: Vec::new(),
            }
        }
        TreeShellRuntimeRequest::Picker {
            request_id,
            environment,
            state,
            action,
        } => {
            let transition = state.reduce(&environment, action);
            let dom_patches = transition
                .outputs
                .iter()
                .filter_map(|output| match output {
                    PickerRuntimeOutput::DomPatch { focus_dom } => {
                        Some(TreeShellDomPatch::PickerState {
                            active_item_key: transition.state.active_item_key.clone(),
                            focus_dom: *focus_dom,
                        })
                    }
                    _ => None,
                })
                .collect();
            let host_events = transition
                .outputs
                .iter()
                .filter_map(|output| match output {
                    PickerRuntimeOutput::Pick(PickerRuntimePickTarget::Root) => {
                        Some(TreeShellHostEvent::PickerPickRoot)
                    }
                    PickerRuntimeOutput::Pick(PickerRuntimePickTarget::Document {
                        document_id,
                    }) => Some(TreeShellHostEvent::PickerPickDocument {
                        document_id: document_id.clone(),
                    }),
                    _ => None,
                })
                .collect();

            TreeShellRuntimeResult {
                request_id,
                mode: TreeShellRuntimeMode::Picker,
                state: TreeShellRuntimeStateSnapshot::Picker(transition.state),
                dom_patches,
                host_events,
                command_events: Vec::new(),
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{
        reduce_tree_shell_runtime, TreeShellCommandEvent, TreeShellDomPatch, TreeShellHostEvent,
        TreeShellRuntimeMode, TreeShellRuntimeRequest, TreeShellRuntimeStateSnapshot,
    };
    use crate::tree_shell::drag_drop_state::TreeShellDragEffect;
    use crate::tree_shell::filetree_runtime::{
        FileTreeOpenTarget, FileTreeRuntimeAction, FileTreeRuntimeEnvironment, FileTreeRuntimeRow,
        FileTreeRuntimeState,
    };
    use crate::tree_shell::filetree_selection::{
        FileTreeSelectionModifiers, FileTreeSelectionState,
    };
    use crate::tree_shell::page_runtime::{
        PageTreeDropPosition, PageTreeRuntimeAction, PageTreeRuntimeEnvironment,
        PageTreeRuntimeState,
    };
    use crate::tree_shell::picker_runtime::{
        PickerRuntimeAction, PickerRuntimeEnvironment, PickerRuntimeItem, PickerRuntimeState,
    };
    use serde_json::json;
    use std::collections::BTreeSet;

    fn ids(values: &[&str]) -> Vec<String> {
        values.iter().map(|value| (*value).to_string()).collect()
    }

    fn set(values: &[&str]) -> BTreeSet<String> {
        values.iter().map(|value| (*value).to_string()).collect()
    }

    #[test]
    fn runtime_request_result_are_wire_safe_and_camel_case() {
        let request = TreeShellRuntimeRequest::Page {
            request_id: "req-page".into(),
            environment: PageTreeRuntimeEnvironment {
                visible_node_ids: ids(&["doc:a", "doc:b"]),
                expandable_node_ids: set(&["doc:a"]),
                rows: Vec::new(),
            },
            state: PageTreeRuntimeState {
                focused_id: Some("doc:a".into()),
                expanded_ids: BTreeSet::new(),
                drop_feedback: None,
            },
            action: PageTreeRuntimeAction::MoveNext,
        };

        let encoded = serde_json::to_value(&request).expect("runtime request should serialize");
        assert_eq!(
            encoded,
            json!({
                "mode": "page",
                "requestId": "req-page",
                "environment": {
                    "visibleNodeIds": ["doc:a", "doc:b"],
                    "expandableNodeIds": ["doc:a"]
                },
                "state": {
                    "focusedId": "doc:a",
                    "expandedIds": [],
                    "dropFeedback": null
                },
                "action": {
                    "kind": "moveNext"
                }
            })
        );

        let decoded: TreeShellRuntimeRequest =
            serde_json::from_value(encoded).expect("runtime request should deserialize");
        let result = reduce_tree_shell_runtime(decoded);

        assert_eq!(result.request_id, "req-page");
        assert_eq!(result.mode, TreeShellRuntimeMode::Page);
        assert_eq!(
            result.state,
            TreeShellRuntimeStateSnapshot::Page(PageTreeRuntimeState {
                focused_id: Some("doc:b".into()),
                expanded_ids: BTreeSet::new(),
                drop_feedback: None,
            })
        );
        assert_eq!(
            result.dom_patches,
            vec![TreeShellDomPatch::PageState {
                focused_id: Some("doc:b".into()),
                expanded_ids: BTreeSet::new(),
                drop_feedback: None,
            }]
        );
        assert!(result.host_events.is_empty());
        assert!(result.command_events.is_empty());
    }

    #[test]
    fn runtime_api_maps_page_intents_and_commands_to_structured_events() {
        let environment = PageTreeRuntimeEnvironment {
            visible_node_ids: ids(&["doc:a", "doc:b"]),
            expandable_node_ids: set(&["doc:a"]),
            rows: Vec::new(),
        };
        let state = PageTreeRuntimeState {
            focused_id: Some("doc:b".into()),
            ..PageTreeRuntimeState::default()
        };

        let open = reduce_tree_shell_runtime(TreeShellRuntimeRequest::Page {
            request_id: "req-open".into(),
            environment: environment.clone(),
            state: state.clone(),
            action: PageTreeRuntimeAction::OpenFocused,
        });
        assert_eq!(
            open.host_events,
            vec![TreeShellHostEvent::PageOpen {
                node_id: "doc:b".into(),
            }]
        );

        let create_result = reduce_tree_shell_runtime(TreeShellRuntimeRequest::Page {
            request_id: "req-create".into(),
            environment: environment.clone(),
            state: state.clone(),
            action: PageTreeRuntimeAction::DispatchCreate {
                parent_node_id: Some("doc:a".into()),
            },
        });
        assert_eq!(
            create_result.command_events,
            vec![TreeShellCommandEvent::CreateNode {
                command_name: "tree.node.create".into(),
                parent_node_id: Some("doc:a".into()),
            }]
        );

        let rename_result = reduce_tree_shell_runtime(TreeShellRuntimeRequest::Page {
            request_id: "req-rename".into(),
            environment: environment.clone(),
            state: state.clone(),
            action: PageTreeRuntimeAction::DispatchRename {
                node_id: "doc:b".into(),
                title: "新标题".into(),
            },
        });
        assert_eq!(
            rename_result.command_events,
            vec![TreeShellCommandEvent::RenameNode {
                command_name: "tree.node.rename".into(),
                node_id: "doc:b".into(),
                title: "新标题".into(),
            }]
        );

        let move_result = reduce_tree_shell_runtime(TreeShellRuntimeRequest::Page {
            request_id: "req-move".into(),
            environment,
            state,
            action: PageTreeRuntimeAction::DispatchMove {
                source_node_id: "doc:b".into(),
                target_parent_id: Some("doc:a".into()),
                position: PageTreeDropPosition::Inside,
            },
        });
        assert_eq!(
            move_result.command_events,
            vec![TreeShellCommandEvent::MoveSubtree {
                command_name: "tree.subtree.move".into(),
                source_node_id: "doc:b".into(),
                target_node_id: None,
                target_parent_id: Some("doc:a".into()),
                position: PageTreeDropPosition::Inside,
                sort_order: None,
            }]
        );
    }

    #[test]
    fn runtime_api_maps_filetree_and_picker_results_to_common_output_channels() {
        let filetree = reduce_tree_shell_runtime(TreeShellRuntimeRequest::FileTree {
            request_id: "req-filetree".into(),
            environment: FileTreeRuntimeEnvironment {
                visible_row_ids: ids(&["doc:root", "asset:image"]),
                rows: vec![
                    FileTreeRuntimeRow {
                        row_id: "doc:root".into(),
                        row_kind: "doc".into(),
                        document_id: Some("root".into()),
                        asset_id: None,
                    },
                    FileTreeRuntimeRow {
                        row_id: "asset:image".into(),
                        row_kind: "asset".into(),
                        document_id: Some("root".into()),
                        asset_id: Some("image".into()),
                    },
                ],
                root_uri: None,
            },
            state: FileTreeRuntimeState::default(),
            action: FileTreeRuntimeAction::SelectRow {
                row_id: "asset:image".into(),
                modifiers: FileTreeSelectionModifiers::default(),
            },
        });

        assert_eq!(filetree.mode, TreeShellRuntimeMode::FileTree);
        assert_eq!(
            filetree.dom_patches,
            vec![TreeShellDomPatch::FileTreeState {
                active_row_id: None,
                selected_row_ids: set(&["asset:image"]),
                anchor_row_id: Some("asset:image".into()),
                focused_row_id: Some("asset:image".into()),
                drag_row_ids: Vec::new(),
                drag_effect: None,
                drop_target_row_id: None,
            }]
        );

        let drag_result = reduce_tree_shell_runtime(TreeShellRuntimeRequest::FileTree {
            request_id: "req-filetree-drag".into(),
            environment: FileTreeRuntimeEnvironment {
                visible_row_ids: ids(&["doc:root", "asset:image"]),
                rows: vec![
                    FileTreeRuntimeRow {
                        row_id: "doc:root".into(),
                        row_kind: "doc".into(),
                        document_id: Some("root".into()),
                        asset_id: None,
                    },
                    FileTreeRuntimeRow {
                        row_id: "asset:image".into(),
                        row_kind: "asset".into(),
                        document_id: Some("root".into()),
                        asset_id: Some("image".into()),
                    },
                ],
                root_uri: None,
            },
            state: FileTreeRuntimeState {
                selection: FileTreeSelectionState::from_selected(&["asset:image".to_string()]),
                ..FileTreeRuntimeState::default()
            },
            action: FileTreeRuntimeAction::ResolveDragRows {
                row_id: "asset:image".into(),
                has_external_files: false,
                alt_key: false,
            },
        });

        assert_eq!(
            drag_result.dom_patches,
            vec![TreeShellDomPatch::FileTreeState {
                active_row_id: None,
                selected_row_ids: set(&["asset:image"]),
                anchor_row_id: Some("asset:image".into()),
                focused_row_id: Some("asset:image".into()),
                drag_row_ids: ids(&["asset:image"]),
                drag_effect: Some(TreeShellDragEffect::Move),
                drop_target_row_id: None,
            }]
        );

        let drop_target_result = reduce_tree_shell_runtime(TreeShellRuntimeRequest::FileTree {
            request_id: "req-filetree-drop-target".into(),
            environment: FileTreeRuntimeEnvironment {
                visible_row_ids: ids(&["doc:root", "asset:image"]),
                rows: vec![
                    FileTreeRuntimeRow {
                        row_id: "doc:root".into(),
                        row_kind: "doc".into(),
                        document_id: Some("root".into()),
                        asset_id: None,
                    },
                    FileTreeRuntimeRow {
                        row_id: "asset:image".into(),
                        row_kind: "asset".into(),
                        document_id: Some("root".into()),
                        asset_id: Some("image".into()),
                    },
                ],
                root_uri: None,
            },
            state: FileTreeRuntimeState::default(),
            action: FileTreeRuntimeAction::UpdateDropTarget {
                row_id: Some("asset:image".into()),
            },
        });

        assert_eq!(
            drop_target_result.dom_patches,
            vec![TreeShellDomPatch::FileTreeState {
                active_row_id: None,
                selected_row_ids: BTreeSet::new(),
                anchor_row_id: None,
                focused_row_id: None,
                drag_row_ids: Vec::new(),
                drag_effect: None,
                drop_target_row_id: Some("asset:image".into()),
            }]
        );

        let internal_drop = reduce_tree_shell_runtime(TreeShellRuntimeRequest::FileTree {
            request_id: "req-filetree-internal-drop".into(),
            environment: FileTreeRuntimeEnvironment {
                visible_row_ids: ids(&["doc:root", "asset:image"]),
                rows: vec![
                    FileTreeRuntimeRow {
                        row_id: "doc:root".into(),
                        row_kind: "doc".into(),
                        document_id: Some("root".into()),
                        asset_id: None,
                    },
                    FileTreeRuntimeRow {
                        row_id: "asset:image".into(),
                        row_kind: "asset".into(),
                        document_id: Some("root".into()),
                        asset_id: Some("image".into()),
                    },
                ],
                root_uri: None,
            },
            state: FileTreeRuntimeState {
                drag_row_ids: ids(&["asset:image"]),
                drag_effect: Some(TreeShellDragEffect::Move),
                drop_target_row_id: Some("doc:root".into()),
                ..FileTreeRuntimeState::default()
            },
            action: FileTreeRuntimeAction::DispatchInternalDrop {
                target_row_id: Some("doc:root".into()),
                row_ids: ids(&["asset:image"]),
                copy: false,
            },
        });

        assert_eq!(
            internal_drop.host_events,
            vec![TreeShellHostEvent::FileTreeInternalDrop {
                target_row_id: Some("doc:root".into()),
                target: Some(FileTreeOpenTarget::Document {
                    document_id: "root".into(),
                }),
                row_ids: ids(&["asset:image"]),
                copy: false,
            }]
        );
        assert_eq!(
            internal_drop.dom_patches,
            vec![TreeShellDomPatch::FileTreeState {
                active_row_id: None,
                selected_row_ids: BTreeSet::new(),
                anchor_row_id: None,
                focused_row_id: None,
                drag_row_ids: Vec::new(),
                drag_effect: None,
                drop_target_row_id: None,
            }]
        );

        let external_drop = reduce_tree_shell_runtime(TreeShellRuntimeRequest::FileTree {
            request_id: "req-filetree-external-drop".into(),
            environment: FileTreeRuntimeEnvironment {
                visible_row_ids: ids(&["doc:root"]),
                rows: vec![FileTreeRuntimeRow {
                    row_id: "doc:root".into(),
                    row_kind: "doc".into(),
                    document_id: Some("root".into()),
                    asset_id: None,
                }],
                root_uri: None,
            },
            state: FileTreeRuntimeState::default(),
            action: FileTreeRuntimeAction::DispatchExternalDrop {
                target_row_id: Some("doc:root".into()),
                file_count: 2,
            },
        });

        assert_eq!(
            external_drop.host_events,
            vec![TreeShellHostEvent::FileTreeExternalDrop {
                target_row_id: Some("doc:root".into()),
                target: Some(FileTreeOpenTarget::Document {
                    document_id: "root".into(),
                }),
                file_count: 2,
            }]
        );

        let picker = reduce_tree_shell_runtime(TreeShellRuntimeRequest::Picker {
            request_id: "req-picker".into(),
            environment: PickerRuntimeEnvironment {
                items: vec![PickerRuntimeItem {
                    item_key: "doc:target".into(),
                    document_id: Some("target".into()),
                    pickable: true,
                }],
                excluded_ids: BTreeSet::new(),
                allow_root_pick: false,
            },
            state: PickerRuntimeState {
                active_item_key: Some("doc:target".into()),
            },
            action: PickerRuntimeAction::Pick,
        });

        assert_eq!(picker.mode, TreeShellRuntimeMode::Picker);
        assert_eq!(
            picker.host_events,
            vec![TreeShellHostEvent::PickerPickDocument {
                document_id: "target".into(),
            }]
        );
    }

    #[test]
    fn runtime_command_event_schema_covers_tree_and_resource_command_channels() {
        let events = vec![
            TreeShellCommandEvent::CreateNode {
                command_name: "tree.node.create".into(),
                parent_node_id: Some("doc:parent".into()),
            },
            TreeShellCommandEvent::RenameNode {
                command_name: "tree.node.rename".into(),
                node_id: "doc:target".into(),
                title: "新标题".into(),
            },
            TreeShellCommandEvent::MoveSubtree {
                command_name: "tree.subtree.move".into(),
                source_node_id: "doc:target".into(),
                target_node_id: Some("doc:sibling".into()),
                target_parent_id: Some("doc:parent".into()),
                position: PageTreeDropPosition::After,
                sort_order: Some(3),
            },
            TreeShellCommandEvent::CopyResource {
                command_name: "tree.resource.copy".into(),
                source_asset_ids: vec!["asset:a".into()],
                target_document_id: "doc:target".into(),
            },
            TreeShellCommandEvent::MoveResource {
                command_name: "tree.resource.move".into(),
                source_asset_ids: vec!["asset:a".into()],
                target_document_id: "doc:target".into(),
            },
            TreeShellCommandEvent::UploadResource {
                command_name: "tree.resource.upload".into(),
                target_document_id: "doc:target".into(),
                file_count: 2,
            },
            TreeShellCommandEvent::ResourceLifecycle {
                command_name: "tree.resource.archive".into(),
                resource_kind: "file".into(),
                asset_id: "asset:a".into(),
            },
            TreeShellCommandEvent::ResourceLifecycle {
                command_name: "tree.resource.restore".into(),
                resource_kind: "file".into(),
                asset_id: "asset:a".into(),
            },
            TreeShellCommandEvent::ResourceLifecycle {
                command_name: "tree.resource.purge".into(),
                resource_kind: "file".into(),
                asset_id: "asset:a".into(),
            },
            TreeShellCommandEvent::ResourceLifecycle {
                command_name: "tree.resource.rename".into(),
                resource_kind: "file".into(),
                asset_id: "asset:a".into(),
            },
        ];

        let encoded = serde_json::to_value(&events).expect("command events should serialize");
        assert_eq!(
            encoded,
            json!([
                {
                    "kind": "createNode",
                    "commandName": "tree.node.create",
                    "parentNodeId": "doc:parent"
                },
                {
                    "kind": "renameNode",
                    "commandName": "tree.node.rename",
                    "nodeId": "doc:target",
                    "title": "新标题"
                },
                {
                    "kind": "moveSubtree",
                    "commandName": "tree.subtree.move",
                    "sourceNodeId": "doc:target",
                    "targetNodeId": "doc:sibling",
                    "targetParentId": "doc:parent",
                    "position": "after",
                    "sortOrder": 3
                },
                {
                    "kind": "copyResource",
                    "commandName": "tree.resource.copy",
                    "sourceAssetIds": ["asset:a"],
                    "targetDocumentId": "doc:target"
                },
                {
                    "kind": "moveResource",
                    "commandName": "tree.resource.move",
                    "sourceAssetIds": ["asset:a"],
                    "targetDocumentId": "doc:target"
                },
                {
                    "kind": "uploadResource",
                    "commandName": "tree.resource.upload",
                    "targetDocumentId": "doc:target",
                    "fileCount": 2
                },
                {
                    "kind": "resourceLifecycle",
                    "commandName": "tree.resource.archive",
                    "resourceKind": "file",
                    "assetId": "asset:a"
                },
                {
                    "kind": "resourceLifecycle",
                    "commandName": "tree.resource.restore",
                    "resourceKind": "file",
                    "assetId": "asset:a"
                },
                {
                    "kind": "resourceLifecycle",
                    "commandName": "tree.resource.purge",
                    "resourceKind": "file",
                    "assetId": "asset:a"
                },
                {
                    "kind": "resourceLifecycle",
                    "commandName": "tree.resource.rename",
                    "resourceKind": "file",
                    "assetId": "asset:a"
                }
            ])
        );
    }
}
