pub mod ai;
pub mod command;
pub mod common;
pub mod editor;
pub mod governance;
pub mod kernel;
pub mod mindmap;
pub mod page_aggregate;
pub mod query;
pub mod search;
pub mod tool;

pub use ai::{
    AiAccessPermissionLevel, AiAccessScope, AiEvent, AiEventKind, AiRuntimeOwner, AiSession,
    AiShareContext, AiStructuredWriteKind, AiStructuredWriteResult, AiToolCall,
};
pub use command::{
    check_when, evaluate_when, is_command_enabled, parse_when_expr, CommandContext,
    CommandContextValue, CommandEnvelope, CopyTreeDocumentPages, CreateDocumentPage, CreatePage,
    CreateWorkspace, DeleteBlock, DeleteDocumentPage, DuplicateDocumentPage, EmbedBlock,
    InsertBlock, MoveBlock, MoveDocumentPage, PatchBlock, PatchPageBlock, PutMindmap,
    ReplaceMediaAssetStorage, RestoreDocumentPage, SavePageContent, UpdateBlock, UpdatePageOptions,
    UpdatePageStats, UpdatePageTitle, WhenExprNode, WhenExprToken,
};
pub use common::{
    ActorPayload, AffectedObject, ErrorDetail, ErrorPayload, OkPayload, RequestMeta, ResponseMeta,
    SourcePayload, TargetRef,
};
pub use editor::{
    BlockProps, ContentNode, ContentNodePayload, EditorAttachReferenceToken, EditorBlock,
    EditorBlockDocument, EditorBlockType, EditorCommand, EditorCommandCatalog,
    EditorCommandDescriptor, EditorCommandKind, EditorDeleteBlock, EditorDetachReferenceToken,
    EditorIndentBlock, EditorInsertBlockAfter, EditorKernelCommandMapping, EditorMergeWithPrevious,
    EditorMoveBlock, EditorOutdentBlock, EditorReplaceBlock, EditorSplitBlock,
    EditorToggleHeadingCollapse, MarkdownExportBoundary, MarkdownExportMode, MarkdownExportOptions,
    MarkdownExportRequest, MarkdownExportResult, MarkdownFlavor, MarkdownImportBoundary,
    MarkdownImportMode, MarkdownImportOptions, MarkdownImportRequest, MarkdownImportResult,
    ReferenceToken, ReferenceTokenKind, ReferenceTokenStrategy, TextMark,
};
pub use kernel::{
    DocBufferDirtyState, DocumentBuffer, DocumentContentResult, DocumentReadEvidenceItem,
    DocumentReadEvidenceKind, DocumentReadNode, DocumentReadNodeMeta, DocumentReadNodeType,
    DocumentReadOutlineEntry, DocumentReadPageSubtree, DocumentReadStats, DocumentReadSubtree,
    KernelAttachEdge, KernelAuditStamp, KernelBlockAssetRelation, KernelContentPayload,
    KernelCreateNode, KernelDetachEdge, KernelEdge, KernelEdgeListResult, KernelEdgeType,
    KernelGetNode, KernelGetSubtree, KernelGraphDirection, KernelGraphTraversalResult,
    KernelGraphVisit, KernelListChildren, KernelListEdges, KernelMoveSubtree, KernelNode,
    KernelNodeMetadata, KernelNodeType, KernelObjectIdentity, KernelObjectKind,
    KernelProjectionAssetKind, KernelProjectionCapability, KernelProjectionFilter,
    KernelProjectionItem, KernelProjectionKind, KernelProjectionRequest,
    KernelProjectionResourceKind, KernelProjectionResourceMeta, KernelProjectionResult,
    KernelProjectionRowKind, KernelRefsPayload, KernelSubtreeRef, KernelSubtreeResult,
    KernelTraverseGraph, KernelUpdateNode, ObjectWorkspacePath, PageBodyWriteRequest,
    WorkspaceSource, WorkspaceSourceCapability, WorkspaceSourceKind,
};
pub use mindmap::{
    MindmapAdapterProjection, MindmapAssociativeLine, MindmapCommand, MindmapKernelCapabilities,
    MindmapKernelCommand, MindmapKernelEdge, MindmapKernelNode, MindmapKernelProjection,
    MindmapNodeData, MindmapNodeInput, MindmapNodeRef, MindmapOp, MindmapProjection,
    MindmapProjectionEdge, MindmapProjectionNode, MindmapProjectionOwner, MindmapProjectionSource,
    MindmapSummary, MindmapTreeNode,
};
pub use page_aggregate::{
    PageAggregateProjection, PageAggregateSource, PageBody, PageHead, PageIdentity, PageLayout,
    PageOptions, PagePermissions, PageStats, PageTree,
};
pub use query::{
    GetBlock, GetBridgeCommand, GetBridgeRequest, GetBridgeTrace, GetMindmap, GetPage,
    GetPageContent, GetPageMeta, ListBridgeWorkspaceOverview, ListPageBlocks, ListSidebarDataset,
    QueryEnvelope, SearchBlocks, SearchDocuments, SearchPages, SearchRecent,
};
pub use search::{
    SearchHighlight, SearchProjectionOwner, SearchQuery, SearchResultProjection, SearchScope,
};
pub use tool::{
    default_tool_registry, invocation_kind_label, tool_effect_label, tool_mode_label,
    InvocationKind, ToolEffect, ToolExecutionMode, ToolInvocation, ToolRegistry, ToolSetSpec,
    ToolSpec, BRIDGE_TOOL_COMMAND_GET, BRIDGE_TOOL_REQUEST_GET, BRIDGE_TOOL_TRACE_GET,
    DOCS_TOOLSET_READ, DOCS_TOOL_READ, DOCS_TOOL_SEARCH, DOC_TOOLSET_READ, DOC_TOOLSET_WRITE,
    DOC_TOOL_FIND, DOC_TOOL_GET, DOC_TOOL_INSERT_BLOCKS, DOC_TOOL_REPLACE_RANGE,
    INDEX_TOOL_REBUILD, MINDMAP_TOOLSET_READ, MINDMAP_TOOLSET_WRITE, MINDMAP_TOOL_APPLY_OPS,
    MINDMAP_TOOL_EMPTY_TRASH, MINDMAP_TOOL_EXPAND_NODE, MINDMAP_TOOL_GET, MINDMAP_TOOL_GET_SUBTREE,
    MINDMAP_TOOL_OUTLINE_TO_MINDMAP, MINDMAP_TOOL_PUT, OBSERVE_TOOLSET_READ,
    ONLYOFFICE_TOOLSET_SERVICE, ONLYOFFICE_TOOL_PREPARE_CALLBACK,
    ONLYOFFICE_TOOL_PREPARE_FORCESAVE, ONLYOFFICE_TOOL_PREPARE_PROXY,
    ONLYOFFICE_TOOL_SESSION_RESOLVE, ONLYOFFICE_TOOL_SIGN, RECOVERY_TOOLSET_JOB,
    REPLAY_TOOL_EVENTS,
};

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn command_envelope_keeps_validate_only_flag() {
        let envelope = CommandEnvelope::<CreateWorkspace> {
            name: "create_workspace".into(),
            command_id: "cmd_1".into(),
            idempotency_key: Some("idem_1".into()),
            actor: ActorPayload {
                actor_type: "human".into(),
                actor_id: "user_1".into(),
                session_id: Some("session_1".into()),
            },
            source: SourcePayload {
                channel: "cli".into(),
                client: "mnote-cli".into(),
            },
            target: None,
            payload: CreateWorkspace {
                name: "demo".into(),
                slug: Some("demo".into()),
            },
            reason: Some("初始化工作区".into()),
            refs: vec![],
            dry_run: false,
            validate_only: true,
        };

        assert!(envelope.validate_only);
    }

    #[test]
    fn default_tool_registry_exposes_doc_tools() {
        let registry = default_tool_registry();
        assert_eq!(
            registry.tool_names(),
            vec![
                "search_web",
                "doc_get",
                "doc_find",
                "docs_search",
                "docs_read",
                "image_read",
                "doc_insert_blocks",
                "doc_replace_range",
                "slash_run",
                "mindmap_get",
                "mindmap_get_subtree",
                "mindmap_put",
                "mindmap_apply_ops",
                "mindmap_expand_node",
                "mindmap_empty_trash",
                "mindmap_outline_to_mindmap",
                "bridge_request_get",
                "bridge_trace_get",
                "bridge_command_get",
                "event_replay",
                "index_rebuild",
                "onlyoffice_session_resolve",
                "onlyoffice_sign",
                "onlyoffice_prepare_proxy",
                "onlyoffice_prepare_callback",
                "onlyoffice_prepare_forcesave",
            ]
        );
        assert_eq!(
            registry
                .tool("search_web")
                .expect("search_web should exist")
                .toolset_id,
            "toolset.readonly"
        );
        assert_eq!(
            registry
                .tool("image_read")
                .expect("image_read should exist")
                .toolset_id,
            "toolset.media_read"
        );
        assert_eq!(
            registry
                .tool("slash_run")
                .expect("slash_run should exist")
                .toolset_id,
            "toolset.slash_write"
        );
        let readonly = registry
            .toolset("toolset.readonly")
            .expect("readonly toolset should exist");
        assert!(!readonly.write_toolset);
        assert_eq!(readonly.tool_names, &["search_web"]);
        let media = registry
            .toolset("toolset.media_read")
            .expect("media toolset should exist");
        assert!(!media.write_toolset);
        assert_eq!(media.tool_names, &["image_read"]);
        let slash = registry
            .toolset("toolset.slash_write")
            .expect("slash toolset should exist");
        assert!(slash.write_toolset);
        assert_eq!(slash.tool_names, &["slash_run"]);
        let docs_read = registry
            .toolset("toolset.docs_read")
            .expect("docs_read toolset should exist");
        assert!(!docs_read.write_toolset);
        assert_eq!(docs_read.tool_names, &["docs_search", "docs_read"]);
        let doc_write = registry
            .toolset("toolset.doc_write")
            .expect("doc_write toolset should exist");
        assert!(doc_write.write_toolset);
        assert_eq!(
            doc_write.tool_names,
            &["doc_insert_blocks", "doc_replace_range"]
        );
        assert_eq!(
            registry
                .tool("doc_get")
                .expect("doc_get should exist")
                .toolset_id,
            "toolset.doc_read"
        );
        let mindmap_write = registry
            .toolset("toolset.mindmap_write")
            .expect("mindmap_write toolset should exist");
        assert!(mindmap_write.write_toolset);
        assert_eq!(
            mindmap_write.tool_names,
            &[
                "mindmap_put",
                "mindmap_apply_ops",
                "mindmap_expand_node",
                "mindmap_empty_trash",
                "mindmap_outline_to_mindmap",
            ]
        );
        let onlyoffice_service = registry
            .toolset("toolset.onlyoffice_service")
            .expect("onlyoffice service toolset should exist");
        assert!(onlyoffice_service.write_toolset);
        assert_eq!(
            onlyoffice_service.tool_names,
            &[
                "onlyoffice_session_resolve",
                "onlyoffice_sign",
                "onlyoffice_prepare_proxy",
                "onlyoffice_prepare_callback",
                "onlyoffice_prepare_forcesave",
            ]
        );
        let observe = registry
            .toolset("toolset.observe_read")
            .expect("observe toolset should exist");
        assert!(!observe.write_toolset);
        assert_eq!(
            observe.tool_names,
            &[
                "bridge_request_get",
                "bridge_trace_get",
                "bridge_command_get"
            ]
        );
        let recovery = registry
            .toolset("toolset.recovery_job")
            .expect("recovery toolset should exist");
        assert!(recovery.write_toolset);
        assert_eq!(recovery.tool_names, &["event_replay", "index_rebuild"]);
    }

    #[test]
    fn page_aggregate_projection_contract_covers_kernel_owned_fields() {
        let projection = PageAggregateProjection {
            schema: "mnote.page_aggregate.v1".into(),
            projection_version: 1,
            source: PageAggregateSource::KernelProjection,
            page_id: "page_1".into(),
            parent_id: Some("root".into()),
            title: "Rust 页面".into(),
            path: vec!["root".into(), "page_1".into()],
            sidebar_tree_membership: vec!["workspace-sidebar".into()],
            body_ref: Some("page_1:body".into()),
            layout_options: serde_json::json!({"wideLayout": true}),
            updated_at: Some("2026-04-29T00:00:00Z".into()),
            identity: page_aggregate::PageIdentity {
                document_id: "page_1".into(),
                workspace_id: "ws_demo".into(),
            },
            head: page_aggregate::PageHead {
                title: "Rust 页面".into(),
                updated_at: serde_json::json!("2026-04-29T00:00:00Z"),
                permissions: page_aggregate::PagePermissions {
                    read_only: false,
                    disable_download: false,
                    disable_copy: false,
                },
            },
            layout: page_aggregate::PageLayout {
                page_options: page_aggregate::PageOptions::default(),
            },
            body: page_aggregate::PageBody {
                content: serde_json::json!([]),
                revision: serde_json::json!(7),
                conflict_detection_key: serde_json::json!("page_1:7"),
                file_version: serde_json::json!(null),
                block_document: serde_json::json!({
                    "documentId": "page_1",
                    "rootBlockIds": [],
                    "blocks": []
                }),
                block_projection_version: 1,
                projection_source: "fixture".into(),
            },
            tree: page_aggregate::PageTree {
                page_subtree: serde_json::json!({"rootNodeId": "page_1"}),
            },
            stats: page_aggregate::PageStats::default(),
        };

        assert_eq!(projection.source, PageAggregateSource::KernelProjection);
        assert_eq!(projection.projection_version, 1);
        assert_eq!(projection.page_id, "page_1");
        assert_eq!(projection.identity.document_id, "page_1");
    }

    #[test]
    fn search_projection_contract_marks_owner_and_scope() {
        let result = SearchResultProjection {
            id: "page_1".into(),
            title: "Rust 搜索".into(),
            snippet: "Rust Web".into(),
            updated_at: Some("2026-04-29T00:00:00Z".into()),
            created_at: None,
            match_field: "title".into(),
            has_ocr: false,
            public_path: "/documents/page_1".into(),
            node_id: Some("page_1".into()),
            subtree_root_id: Some("page_1".into()),
            evidence: vec![],
            score: 3.0,
            projection_owner: SearchProjectionOwner::RustKernel,
        };
        let query = SearchQuery {
            query: "Rust".into(),
            workspace_id: "ws_demo".into(),
            scope: SearchScope::Workspace,
            page_id: None,
            limit: 20,
        };

        assert_eq!(query.scope, SearchScope::Workspace);
        assert_eq!(result.projection_owner, SearchProjectionOwner::RustKernel);
    }

    #[test]
    fn ai_protocol_contract_exposes_structured_write_result() {
        let result = AiStructuredWriteResult {
            ok: true,
            write_kind: AiStructuredWriteKind::SummaryNode,
            command_name: "tree.node.create".into(),
            target_id: Some("summary_1".into()),
            revision: Some(2),
        };

        assert_eq!(result.write_kind, AiStructuredWriteKind::SummaryNode);
        assert_eq!(result.command_name, "tree.node.create");
    }

    #[test]
    fn mindmap_projection_and_command_contract_cover_kernel_truth() {
        let projection = MindmapProjection {
            schema: "mnote.mindmap_projection.v1".into(),
            map_id: "mind_1".into(),
            root_node: "root".into(),
            nodes: vec![],
            edges: vec![],
            layout_hints: serde_json::json!({"layout": "right"}),
            revision: 1,
            owner: MindmapProjectionOwner::RustKernel,
        };
        let command = MindmapCommand::RenameNode {
            map_id: "mind_1".into(),
            node_id: "root".into(),
            title: "新标题".into(),
        };

        assert_eq!(projection.owner, MindmapProjectionOwner::RustKernel);
        assert!(matches!(command, MindmapCommand::RenameNode { .. }));
    }

    #[test]
    fn mindmap_kernel_and_adapter_projection_serialize_as_camel_case() {
        let kernel_projection = MindmapKernelProjection {
            schema: "mnote.mindmap.kernel_projection.v1".into(),
            document_id: "doc_1".into(),
            mindmap_id: "mind_1".into(),
            root_node_id: "root".into(),
            revision: 7,
            nodes: vec![
                MindmapKernelNode {
                    node_id: "root".into(),
                    parent_id: None,
                    text: "KMIND".into(),
                    order: 0,
                    collapsed: false,
                    style: Default::default(),
                    refs: Default::default(),
                },
                MindmapKernelNode {
                    node_id: "child_1".into(),
                    parent_id: Some("root".into()),
                    text: "二级节点".into(),
                    order: 1,
                    collapsed: false,
                    style: Default::default(),
                    refs: Default::default(),
                },
            ],
            edges: vec![MindmapKernelEdge {
                edge_id: "root->child_1".into(),
                from_node_id: "root".into(),
                to_node_id: "child_1".into(),
                edge_kind: "tree".into(),
            }],
            summaries: vec![MindmapSummary {
                summary_id: "summary_1".into(),
                parent_node_id: "child_1".into(),
                node_ids: vec!["child_1".into()],
                text: "概要".into(),
                style: Default::default(),
            }],
            associative_lines: vec![MindmapAssociativeLine {
                line_id: "line_1".into(),
                from_node_id: "root".into(),
                to_node_id: "child_1".into(),
                text: Some("关联".into()),
                style: Default::default(),
            }],
            layout: serde_json::json!({ "type": "logicalStructure" }),
            theme: serde_json::json!({ "template": "classic" }),
            view: serde_json::json!({ "zoom": 1.0 }),
            capabilities: MindmapKernelCapabilities {
                can_edit: true,
                can_edit_text: true,
                can_insert_child: true,
                can_insert_sibling_after: true,
                can_delete_node: true,
                can_move_node: true,
                can_patch_view: true,
                can_set_layout: true,
                can_set_theme: true,
            },
            source: MindmapProjectionSource::Kernel,
        };

        let adapter_projection = MindmapAdapterProjection {
            schema: "mnote.mindmap.simple_mind_map_scene.v1".into(),
            runtime: "simple-mind-map".into(),
            root: serde_json::json!({
                "data": { "uid": "root", "text": "KMIND" },
                "children": []
            }),
            layout: serde_json::json!("logicalStructure"),
            theme: serde_json::json!("classic"),
            theme_config: serde_json::json!({}),
            view: serde_json::json!({ "scale": 1.0 }),
            config: serde_json::json!({}),
            compat_payload: serde_json::json!({}),
            kernel_revision: 7,
        };

        let kernel_value =
            serde_json::to_value(kernel_projection).expect("kernel projection should serialize");
        let adapter_value =
            serde_json::to_value(adapter_projection).expect("adapter projection should serialize");

        assert_eq!(kernel_value["documentId"], serde_json::json!("doc_1"));
        assert_eq!(kernel_value["mindmapId"], serde_json::json!("mind_1"));
        assert_eq!(kernel_value["rootNodeId"], serde_json::json!("root"));
        assert_eq!(
            kernel_value["nodes"][1]["parentId"],
            serde_json::json!("root")
        );
        assert_eq!(
            kernel_value["associativeLines"][0]["fromNodeId"],
            serde_json::json!("root")
        );
        assert_eq!(
            kernel_value["capabilities"]["canEditText"],
            serde_json::json!(true)
        );
        assert_eq!(kernel_value["source"], serde_json::json!("kernel"));

        assert_eq!(
            adapter_value["runtime"],
            serde_json::json!("simple-mind-map")
        );
        assert_eq!(adapter_value["themeConfig"], serde_json::json!({}));
        assert_eq!(adapter_value["compatPayload"], serde_json::json!({}));
        assert_eq!(adapter_value["kernelRevision"], serde_json::json!(7));
    }

    #[test]
    fn mindmap_kernel_command_contract_covers_minimum_write_surface() {
        let commands = vec![
            MindmapKernelCommand::UpdateText {
                mindmap_id: "mind_1".into(),
                node_id: "root".into(),
                text: "新标题".into(),
            },
            MindmapKernelCommand::InsertChild {
                mindmap_id: "mind_1".into(),
                parent_node_id: "root".into(),
                node: MindmapNodeInput {
                    uid: Some("child_1".into()),
                    text: "子节点".into(),
                    hyperlink: None,
                    note: None,
                    refs: None,
                },
            },
            MindmapKernelCommand::InsertSiblingAfter {
                mindmap_id: "mind_1".into(),
                target_node_id: "child_1".into(),
                node: MindmapNodeInput {
                    uid: Some("child_2".into()),
                    text: "同级节点".into(),
                    hyperlink: None,
                    note: None,
                    refs: None,
                },
            },
            MindmapKernelCommand::DeleteNode {
                mindmap_id: "mind_1".into(),
                node_id: "child_2".into(),
            },
            MindmapKernelCommand::MoveNode {
                mindmap_id: "mind_1".into(),
                node_id: "child_1".into(),
                new_parent_node_id: "root".into(),
                order: Some(1),
            },
            MindmapKernelCommand::PatchView {
                mindmap_id: "mind_1".into(),
                patch: serde_json::json!({ "scale": 1.2 }),
            },
            MindmapKernelCommand::SetLayout {
                mindmap_id: "mind_1".into(),
                layout: serde_json::json!({ "type": "mindMap" }),
            },
            MindmapKernelCommand::SetTheme {
                mindmap_id: "mind_1".into(),
                theme: serde_json::json!({ "template": "classic" }),
                theme_config: serde_json::json!({ "lineColor": "#f59e0b" }),
            },
        ];

        let encoded = serde_json::to_value(&commands).expect("commands should serialize");

        assert_eq!(encoded[0]["type"], serde_json::json!("updateText"));
        assert_eq!(encoded[1]["type"], serde_json::json!("insertChild"));
        assert_eq!(encoded[2]["type"], serde_json::json!("insertSiblingAfter"));
        assert_eq!(encoded[3]["type"], serde_json::json!("deleteNode"));
        assert_eq!(encoded[4]["type"], serde_json::json!("moveNode"));
        assert_eq!(encoded[5]["type"], serde_json::json!("patchView"));
        assert_eq!(encoded[6]["type"], serde_json::json!("setLayout"));
        assert_eq!(encoded[7]["type"], serde_json::json!("setTheme"));
        assert_eq!(encoded[0]["mindmapId"], serde_json::json!("mind_1"));
        assert_eq!(encoded[1]["parentNodeId"], serde_json::json!("root"));
        assert_eq!(encoded[4]["newParentNodeId"], serde_json::json!("root"));
        assert_eq!(
            encoded[7]["themeConfig"]["lineColor"],
            serde_json::json!("#f59e0b")
        );
    }
}
