use crate::app::AppState;
use crate::context::RequestContext;
use crate::error::WebError;
use crate::routes::command_support::execute_runtime_command_via_legacy_cloud_with_artifacts;
use crate::routes::local_folder_source::{
    ensure_local_workspace_read_access_with_state, ensure_local_workspace_write_access_with_state,
    read_local_mindmap_data, write_local_mindmap_data,
};
use crate::routes::query_support::{
    execute_runtime_query_against_data, execute_runtime_query_via_legacy_cloud,
    fetch_documents_meta_via_legacy_cloud, fetch_query_data_via_legacy_cloud,
    resolve_effective_workspace_id,
};
use axum::extract::{Extension, Path, Query, State};
use axum::http::{HeaderMap, HeaderName, HeaderValue, StatusCode};
use axum::Json;
use bridge_runtime::{
    apply_mindmap_kernel_commands_to_value, RuntimeActorWire, RuntimeCommandEnvelopeWire,
    RuntimeQueryEnvelopeWire, RuntimeSourceWire, RuntimeTargetWire,
};
use serde::Deserialize;
use serde_json::{json, Value};

const HEADER_MNOTE_WEB_OWNER: &str = "x-mnote-web-owner";
const HEADER_MINDMAP_TRANSPORT: &str = "x-mnote-mindmap-transport";

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MindmapQueryParams {
    pub view: Option<String>,
    pub query_name: Option<String>,
    pub root_node_id: Option<String>,
    pub workspace_id: Option<String>,
    pub source_kind: Option<String>,
    pub root_uri: Option<String>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MindmapCommandRequest {
    pub command_name: Option<String>,
    #[serde(default)]
    pub commands: Vec<Value>,
    pub projection_revision: Option<u64>,
    pub workspace_id: Option<String>,
    pub data: Option<Value>,
    pub create_only: Option<bool>,
    pub source_kind: Option<String>,
    pub root_uri: Option<String>,
}

fn default_mindmap_data() -> Value {
    json!({
        "data": {"text": "中心主题"},
        "children": [],
    })
}

fn response_headers() -> HeaderMap {
    let mut headers = HeaderMap::new();
    if let Ok(name) = HeaderName::from_lowercase(HEADER_MNOTE_WEB_OWNER.as_bytes()) {
        headers.insert(name, HeaderValue::from_static("mnote-web"));
    }
    if let Ok(name) = HeaderName::from_lowercase(HEADER_MINDMAP_TRANSPORT.as_bytes()) {
        headers.insert(name, HeaderValue::from_static("mindmap-api"));
    }
    headers
}

fn resolve_query_name(params: &MindmapQueryParams) -> &'static str {
    if params.query_name.as_deref() == Some("mindmap.simple_mind_map_scene.get")
        || params.view.as_deref() == Some("simple_mind_map_scene")
    {
        return "mindmap.simple_mind_map_scene.get";
    }
    if params.query_name.as_deref() == Some("mindmap.kernel_projection.get")
        || params.view.as_deref() == Some("kernel_projection")
    {
        return "mindmap.kernel_projection.get";
    }
    "mindmap.projection.get"
}

fn read_workspace_id_from_meta(meta: &Value) -> Option<String> {
    meta.get("workspace_id")
        .or_else(|| meta.get("workspaceId"))
        .and_then(Value::as_str)
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(ToOwned::to_owned)
}

fn is_local_folder_source(source_kind: Option<&str>) -> bool {
    source_kind
        .map(str::trim)
        .is_some_and(|value| value == "local_folder")
}

fn local_root_uri<'a>(
    query_root_uri: Option<&'a str>,
    body_root_uri: Option<&'a str>,
) -> Result<&'a str, WebError> {
    query_root_uri
        .or(body_root_uri)
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .ok_or_else(|| {
            WebError::bad_request_code("local_folder_root_required", "缺少本地思维导图 rootUri")
        })
}

async fn resolve_mindmap_workspace_id(
    state: &AppState,
    context: &RequestContext,
    explicit_workspace_id: Option<&str>,
    document_id: &str,
) -> Result<Option<String>, WebError> {
    let effective_workspace_id =
        resolve_effective_workspace_id(context, explicit_workspace_id, false)?;
    if effective_workspace_id.is_some() {
        return Ok(effective_workspace_id);
    }

    // 思维导图 runtime 的历史请求体不一定带 workspaceId；
    // 这里从页面 meta 反查，确保后续 command artifacts 能进入正确 workspace 的实时流。
    let meta =
        fetch_documents_meta_via_legacy_cloud(state.config(), context, None, document_id).await?;
    Ok(read_workspace_id_from_meta(&meta))
}

fn execution_artifacts_json(
    execution: &crate::transport::legacy_cloud_guard::RetiredCloudCommandExecution,
) -> Value {
    execution
        .artifacts
        .as_ref()
        .and_then(|artifacts| serde_json::to_value(artifacts).ok())
        .unwrap_or(Value::Null)
}

pub async fn get_mindmap(
    State(state): State<AppState>,
    Extension(context): Extension<RequestContext>,
    Path((document_id, mindmap_id)): Path<(String, String)>,
    Query(params): Query<MindmapQueryParams>,
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
    let document_id = document_id.trim();
    let mindmap_id = mindmap_id.trim();
    if document_id.is_empty() || mindmap_id.is_empty() {
        return Err(WebError::bad_request_code(
            "mindmap_id_required",
            "缺少有效 documentId 或 mindmapId",
        )
        .with_context(&context));
    }
    if is_local_folder_source(params.source_kind.as_deref()) {
        let root_uri = local_root_uri(params.root_uri.as_deref(), None)?;
        ensure_local_workspace_read_access_with_state(&state, &context, root_uri)
            .map_err(|error| error.with_context(&context))?;
        let data = read_local_mindmap_data(root_uri, document_id, mindmap_id)
            .map_err(|error| error.with_context(&context))?;
        let query_name = resolve_query_name(&params);
        let result = execute_runtime_query_against_data(
            &context,
            params.workspace_id.as_deref(),
            RuntimeQueryEnvelopeWire {
                name: query_name.into(),
                payload: json!({
                    "documentId": document_id,
                    "mindmapId": mindmap_id,
                    "rootNodeId": params.root_node_id,
                    "workspaceId": params.workspace_id,
                }),
            },
            data,
        )?;
        return Ok((StatusCode::OK, response_headers(), Json(result)));
    }
    let effective_workspace_id =
        resolve_effective_workspace_id(&context, params.workspace_id.as_deref(), false)?;
    let query_name = resolve_query_name(&params);
    let result = execute_runtime_query_via_legacy_cloud(
        state.config(),
        &context,
        effective_workspace_id.as_deref(),
        RuntimeQueryEnvelopeWire {
            name: query_name.into(),
            payload: json!({
                "documentId": document_id,
                "mindmapId": mindmap_id,
                "rootNodeId": params.root_node_id,
                "workspaceId": effective_workspace_id.clone(),
            }),
        },
    )
    .await?;

    Ok((StatusCode::OK, response_headers(), Json(result)))
}

pub async fn apply_mindmap_command(
    State(state): State<AppState>,
    Extension(context): Extension<RequestContext>,
    Path((document_id, mindmap_id)): Path<(String, String)>,
    Query(params): Query<MindmapQueryParams>,
    Json(body): Json<MindmapCommandRequest>,
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
    let document_id = document_id.trim();
    let mindmap_id = mindmap_id.trim();
    if document_id.is_empty() || mindmap_id.is_empty() {
        return Err(WebError::bad_request_code(
            "mindmap_id_required",
            "缺少有效 documentId 或 mindmapId",
        )
        .with_context(&context));
    }
    let command_name = body.command_name.as_deref();
    if !matches!(
        command_name,
        None | Some("mindmaps.put") | Some("mindmap.command.apply")
    ) {
        return Err(WebError::bad_request_code(
            "mindmap_command_required",
            "仅支持 mindmaps.put 或 mindmap.command.apply",
        )
        .with_context(&context));
    }

    let local_source_kind = body
        .source_kind
        .as_deref()
        .or(params.source_kind.as_deref());
    if is_local_folder_source(local_source_kind) {
        let root_uri = local_root_uri(params.root_uri.as_deref(), body.root_uri.as_deref())?;
        ensure_local_workspace_write_access_with_state(&state, &context, root_uri)
            .map_err(|error| error.with_context(&context))?;
        if command_name == Some("mindmap.command.apply") {
            let current = read_local_mindmap_data(root_uri, document_id, mindmap_id)
                .map_err(|error| error.with_context(&context))?;
            let applied = apply_mindmap_kernel_commands_to_value(&current, &body.commands)
                .map_err(|error| WebError::bad_request(error.message).with_context(&context))?;
            if !applied.errors.is_empty() {
                return Err(WebError::bad_request_code(
                    "mindmap_command_failed",
                    applied.errors.join("; "),
                )
                .with_context(&context)
                .with_header("x-error-phase", "local_mindmap_command_apply"));
            }
            let write_result = write_local_mindmap_data(
                root_uri,
                document_id,
                mindmap_id,
                applied.data.clone(),
                false,
            )
            .map_err(|error| error.with_context(&context))?;
            let projection = execute_runtime_query_against_data(
                &context,
                body.workspace_id.as_deref(),
                RuntimeQueryEnvelopeWire {
                    name: "mindmap.simple_mind_map_scene.get".into(),
                    payload: json!({
                        "documentId": document_id,
                        "mindmapId": mindmap_id,
                        "workspaceId": body.workspace_id,
                    }),
                },
                applied.data,
            )?;
            return Ok((
                StatusCode::OK,
                response_headers(),
                Json(json!({
                    "ok": true,
                    "sourceKind": "local_folder",
                    "commandName": "mindmap.command.apply",
                    "applied": applied.applied,
                    "errors": applied.errors,
                    "projectionRevision": body.projection_revision,
                    "writeResult": write_result,
                    "kernelRevision": projection.get("kernelRevision").cloned().unwrap_or(json!(1)),
                    "root": projection.get("root").cloned().unwrap_or(Value::Null),
                })),
            ));
        }

        let data = body.data.unwrap_or_else(default_mindmap_data);
        let write_result = write_local_mindmap_data(
            root_uri,
            document_id,
            mindmap_id,
            data.clone(),
            body.create_only.unwrap_or(false),
        )
        .map_err(|error| error.with_context(&context))?;
        let projection = execute_runtime_query_against_data(
            &context,
            body.workspace_id.as_deref(),
            RuntimeQueryEnvelopeWire {
                name: "mindmap.simple_mind_map_scene.get".into(),
                payload: json!({
                    "documentId": document_id,
                    "mindmapId": mindmap_id,
                    "workspaceId": body.workspace_id,
                }),
            },
            data,
        )?;
        return Ok((
            StatusCode::OK,
            response_headers(),
            Json(json!({
                "ok": true,
                "sourceKind": "local_folder",
                "commandName": "mindmaps.put",
                "writeResult": write_result,
                "kernelRevision": projection.get("kernelRevision").cloned().unwrap_or(json!(1)),
                "root": projection.get("root").cloned().unwrap_or(Value::Null),
            })),
        ));
    }
    let effective_workspace_id =
        resolve_mindmap_workspace_id(&state, &context, body.workspace_id.as_deref(), document_id)
            .await?;

    if command_name != Some("mindmap.command.apply") {
        let command = RuntimeCommandEnvelopeWire {
            name: "mindmaps.put".into(),
            command_id: format!("mindmap_put_{}", context.trace.request_id),
            idempotency_key: context.source.idempotency_key.clone(),
            actor: RuntimeActorWire {
                actor_type: context.auth.actor_type.clone(),
                actor_id: context.auth.actor_id.clone(),
                session_id: context.auth.session_id.clone(),
            },
            source: RuntimeSourceWire {
                channel: context.source.channel.clone(),
                client: context.source.client.clone(),
                source_kind: None,
                root_uri: None,
                workspace_id: None,
                capabilities: Vec::new(),
            },
            target: Some(RuntimeTargetWire {
                workspace_id: effective_workspace_id.clone(),
                page_id: Some(document_id.to_string()),
                block_id: Some(mindmap_id.to_string()),
            }),
            payload: json!({
                "documentId": document_id,
                "mindmapId": mindmap_id,
                "workspaceId": effective_workspace_id,
                "data": body.data.unwrap_or_else(default_mindmap_data),
                "createOnly": body.create_only.unwrap_or(false),
            }),
            preflight_data: None,
            reason: Some("mnote-web mindmap put via kernel projection".into()),
            refs: vec!["task168-mindmap-put-validator-smoke".into()],
            dry_run: false,
            validate_only: false,
        };

        let execution = execute_runtime_command_via_legacy_cloud_with_artifacts(
            &state,
            &context,
            effective_workspace_id.as_deref(),
            command,
        )
        .await?;

        return Ok((
            StatusCode::OK,
            response_headers(),
            Json(json!({
                "ok": true,
                "commandName": "mindmaps.put",
                "result": execution.result,
                "artifacts": execution_artifacts_json(&execution),
                "artifactError": execution.artifact_error,
            })),
        ));
    }

    let current = fetch_query_data_via_legacy_cloud(
        state.config(),
        &context,
        effective_workspace_id.as_deref(),
        RuntimeQueryEnvelopeWire {
            name: "mindmaps.get".into(),
            payload: json!({
                "documentId": document_id,
                "mindmapId": mindmap_id,
                "workspaceId": effective_workspace_id,
            }),
        },
    )
    .await?;
    let applied = apply_mindmap_kernel_commands_to_value(&current, &body.commands)
        .map_err(|error| WebError::bad_request(error.message).with_context(&context))?;
    if !applied.errors.is_empty() {
        return Err(WebError::bad_request_code(
            "mindmap_command_failed",
            applied.errors.join("; "),
        )
        .with_context(&context)
        .with_header("x-error-phase", "mindmap_command_apply"));
    }

    let command = RuntimeCommandEnvelopeWire {
        name: "mindmaps.put".into(),
        command_id: format!("mindmap_put_{}", context.trace.request_id),
        idempotency_key: context.source.idempotency_key.clone(),
        actor: RuntimeActorWire {
            actor_type: context.auth.actor_type.clone(),
            actor_id: context.auth.actor_id.clone(),
            session_id: context.auth.session_id.clone(),
        },
        source: RuntimeSourceWire {
            channel: context.source.channel.clone(),
            client: context.source.client.clone(),
            source_kind: None,
            root_uri: None,
            workspace_id: None,
            capabilities: Vec::new(),
        },
        target: Some(RuntimeTargetWire {
            workspace_id: effective_workspace_id.clone(),
            page_id: Some(document_id.to_string()),
            block_id: Some(mindmap_id.to_string()),
        }),
        payload: json!({
            "documentId": document_id,
            "mindmapId": mindmap_id,
            "workspaceId": effective_workspace_id,
            "data": applied.data,
            "createOnly": false,
        }),
        preflight_data: None,
        reason: Some("mnote-web mindmap command apply via kernel projection".into()),
        refs: vec!["task166-mindmap-phase6-block-smoke".into()],
        dry_run: false,
        validate_only: false,
    };

    let execution = execute_runtime_command_via_legacy_cloud_with_artifacts(
        &state,
        &context,
        effective_workspace_id.as_deref(),
        command,
    )
    .await?;

    Ok((
        StatusCode::OK,
        response_headers(),
        Json(json!({
            "ok": true,
            "commandName": "mindmap.command.apply",
            "applied": applied.applied,
            "errors": applied.errors,
            "projectionRevision": body.projection_revision,
            "result": execution.result,
            "artifacts": execution_artifacts_json(&execution),
            "artifactError": execution.artifact_error,
        })),
    ))
}

#[cfg(test)]
mod tests {
    use crate::app::{build_app, AppConfig, AppState};
    use axum::body::{to_bytes, Body};
    use axum::http::{Request, StatusCode};
    use serde_json::{json, Value};
    use std::path::PathBuf;
    use tower::util::ServiceExt;

    fn app() -> axum::Router {
        build_app(AppState::new(AppConfig {
            service_name: "mnote-web".into(),
            service_version: "0.1.0".into(),
            bind_addr: "127.0.0.1:0".into(),
            public_bind_addr: "127.0.0.1:3000".into(),
            legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
            enable_legacy_next_compat: true,
            enable_debug_shell_routes: false,
            enable_editor_actor: true,
            hermes_base_path: "/api/hermes".into(),
            compat_next_base_path: "/api/compat/next".into(),
            convex_url: None,
            convex_admin_key: None,
            allow_dev_fixtures: true,
            query_fixtures_json: Some(
                r#"{"documents:getMeta":{"id":"doc_1","workspace_id":"ws_demo","title":"页面"},"mindmaps:get":{"data":{"data":{"text":"KMIND","uid":"root"},"children":[]},"revision":1}}"#
                    .into(),
            ),
            mutation_fixtures_json: Some(
                r#"{"mindmaps:put":{"ok":true,"document_id":"doc_1","mindmap_id":"mind_1","updated_at":"2026-05-12T00:00:00Z"},"bridgeLogs:recordCommandLog":{"ok":true,"id":"clog_fixture"},"bridgeLogs:recordDomainEvent":{"ok":true,"id":"evt_fixture"}}"#
                    .into(),
            ),
            dev_user_id: "dev-user".into(),
            dev_user_name: "开发用户".into(),
            dev_user_email: "dev@mnote.local".into(),
        }))
    }

    fn temp_root(name: &str) -> PathBuf {
        let root = std::env::temp_dir().join(format!("{}-{}", name, std::process::id()));
        let _ = std::fs::remove_dir_all(&root);
        std::fs::create_dir_all(&root).expect("create temp root");
        root
    }

    fn encode_query_value(value: &str) -> String {
        value.replace(':', "%3A").replace('/', "%2F")
    }

    fn init_local_workspace(root: &PathBuf) -> String {
        let root_uri = format!("file://{}", root.display());
        crate::routes::local_folder_source::initialize_local_workspace_for_actor(
            "dev-user", &root_uri,
        )
        .expect("init local workspace");
        root_uri
    }

    #[tokio::test]
    async fn mindmap_put_derives_workspace_and_returns_tree_artifacts() {
        let response = app()
            .oneshot(
                Request::builder()
                    .method("POST")
                    .uri("/api/mindmap/doc_1/mind_1")
                    .header("content-type", "application/json")
                    .body(Body::from(
                        json!({
                            "data": {
                                "data": {"text": "KMIND", "uid": "root"},
                                "children": []
                            },
                            "createOnly": true
                        })
                        .to_string(),
                    ))
                    .expect("request"),
            )
            .await
            .expect("response");

        assert_eq!(response.status(), StatusCode::OK);
        let body = to_bytes(response.into_body(), usize::MAX)
            .await
            .expect("body");
        let payload: Value = serde_json::from_slice(&body).expect("json");

        assert_eq!(payload["artifacts"]["commandLog"]["workspaceId"], "ws_demo");
        assert_eq!(payload["artifacts"]["commandLog"]["targetPageId"], "doc_1");
        assert_eq!(
            payload["artifacts"]["commandLog"]["targetBlockId"],
            "mind_1"
        );
        assert_eq!(
            payload["artifacts"]["domainEvent"]["eventType"],
            "tree.resource.mindmap.put"
        );
        assert_eq!(
            payload["artifacts"]["domainEvent"]["payload"]["streamDelta"],
            json!({
                "op": "resync_required",
                "reason": "mindmap.put",
                "documentId": "doc_1",
                "blockId": "mind_1"
            })
        );
    }

    #[tokio::test]
    async fn mindmap_command_apply_returns_object_artifacts_without_tree_resync() {
        let response = app()
            .oneshot(
                Request::builder()
                    .method("POST")
                    .uri("/api/mindmap/doc_1/mind_1")
                    .header("content-type", "application/json")
                    .body(Body::from(
                        json!({
                            "commandName": "mindmap.command.apply",
                            "commands": [
                                {
                                    "type": "updateText",
                                    "mindmapId": "mind_1",
                                    "nodeId": "root",
                                    "text": "KMIND 已更新"
                                }
                            ],
                            "projectionRevision": 1
                        })
                        .to_string(),
                    ))
                    .expect("request"),
            )
            .await
            .expect("response");

        assert_eq!(response.status(), StatusCode::OK);
        let body = to_bytes(response.into_body(), usize::MAX)
            .await
            .expect("body");
        let payload: Value = serde_json::from_slice(&body).expect("json");

        assert_eq!(payload["commandName"], "mindmap.command.apply");
        assert_eq!(
            payload["artifacts"]["domainEvent"]["eventType"],
            "mindmap.content.updated"
        );
        assert_eq!(
            payload["artifacts"]["domainEvent"]["payload"]["streamDelta"],
            json!({
                "op": "noop"
            })
        );
    }

    #[tokio::test]
    async fn local_folder_mindmap_put_writes_file_and_get_reads_back() {
        let root = temp_root("mnote-local-mindmap-api-put-get");
        let root_uri = init_local_workspace(&root);
        std::fs::create_dir_all(root.join("Page")).expect("create page dir");
        std::fs::write(root.join("Page").join("Page.md"), "").expect("write page");
        let encoded_root = encode_query_value(&root_uri);
        let encoded_mindmap_id = encode_query_value("思维导图123456.json");

        let put_response = app()
            .oneshot(
                Request::builder()
                    .method("POST")
                    .uri(format!(
                        "/api/mindmap/local-md:Page~2FPage.md/{encoded_mindmap_id}?sourceKind=local_folder&rootUri={encoded_root}"
                    ))
                    .header("content-type", "application/json")
                    .header("x-mnote-actor-id", "dev-user")
                    .header("x-mnote-actor-type", "user")
                    .body(Body::from(
                        json!({
                            "data": {
                                "data": {"text": "本地主题", "uid": "root"},
                                "children": []
                            },
                            "createOnly": false
                        })
                        .to_string(),
                    ))
                    .expect("request"),
            )
            .await
            .expect("response");
        assert_eq!(put_response.status(), StatusCode::OK);

        let put_body = to_bytes(put_response.into_body(), usize::MAX)
            .await
            .expect("body");
        let put_payload: Value = serde_json::from_slice(&put_body).expect("json");
        assert_eq!(put_payload["sourceKind"], "local_folder");
        assert_eq!(put_payload["commandName"], "mindmaps.put");
        assert_eq!(
            put_payload["writeResult"]["relativePath"],
            "Page/思维导图123456.json"
        );
        assert!(
            root.join("Page").join("思维导图123456.json").is_file(),
            "mindmap json should be persisted next to the page markdown"
        );

        let get_response = app()
            .oneshot(
                Request::builder()
                    .uri(format!(
                        "/api/mindmap/local-md:Page~2FPage.md/{encoded_mindmap_id}?sourceKind=local_folder&rootUri={encoded_root}&view=simple_mind_map_scene"
                    ))
                    .header("x-mnote-actor-id", "dev-user")
                    .header("x-mnote-actor-type", "user")
                    .body(Body::empty())
                    .expect("request"),
            )
            .await
            .expect("response");
        assert_eq!(get_response.status(), StatusCode::OK);
        let get_body = to_bytes(get_response.into_body(), usize::MAX)
            .await
            .expect("body");
        let get_payload: Value = serde_json::from_slice(&get_body).expect("json");
        assert_eq!(get_payload["root"]["data"]["text"], "本地主题");

        let _ = std::fs::remove_dir_all(&root);
    }

    #[tokio::test]
    async fn local_folder_mindmap_command_apply_updates_persisted_file() {
        let root = temp_root("mnote-local-mindmap-api-command-apply");
        let root_uri = init_local_workspace(&root);
        std::fs::create_dir_all(root.join("Page")).expect("create page dir");
        std::fs::write(root.join("Page").join("Page.md"), "").expect("write page");
        std::fs::write(
            root.join("Page").join("思维导图123456.json"),
            json!({
                "data": {"text": "旧主题", "uid": "root"},
                "children": []
            })
            .to_string(),
        )
        .expect("write mindmap");
        let encoded_root = encode_query_value(&root_uri);
        let encoded_mindmap_id = encode_query_value("思维导图123456.json");

        let response = app()
            .oneshot(
                Request::builder()
                    .method("POST")
                    .uri(format!(
                        "/api/mindmap/local-md:Page~2FPage.md/{encoded_mindmap_id}?sourceKind=local_folder&rootUri={encoded_root}"
                    ))
                    .header("content-type", "application/json")
                    .header("x-mnote-actor-id", "dev-user")
                    .header("x-mnote-actor-type", "user")
                    .body(Body::from(
                        json!({
                            "commandName": "mindmap.command.apply",
                            "commands": [
                                {
                                    "type": "updateText",
                                    "mindmapId": "思维导图123456.json",
                                    "nodeId": "root",
                                    "text": "新主题"
                                }
                            ],
                            "projectionRevision": 1
                        })
                        .to_string(),
                    ))
                    .expect("request"),
            )
            .await
            .expect("response");

        assert_eq!(response.status(), StatusCode::OK);
        let body = to_bytes(response.into_body(), usize::MAX)
            .await
            .expect("body");
        let payload: Value = serde_json::from_slice(&body).expect("json");
        assert_eq!(payload["sourceKind"], "local_folder");
        assert_eq!(payload["commandName"], "mindmap.command.apply");
        assert_eq!(payload["root"]["data"]["text"], "新主题");

        let saved = std::fs::read_to_string(root.join("Page").join("思维导图123456.json"))
            .expect("read saved mindmap");
        let saved_json: Value = serde_json::from_str(&saved).expect("saved json");
        assert_eq!(saved_json["data"]["text"], "新主题");

        let _ = std::fs::remove_dir_all(&root);
    }
}
