use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum KernelNodeType {
    Workspace,
    Folder,
    Page,
    Section,
    Asset,
    Table,
    Book,
    Pdf,
    Mindmap,
    MindmapNode,
    Summary,
    AiNote,
    ReferenceAnchor,
    ContentNode,
    IndexNode,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum KernelEdgeType {
    ParentOf,
    ChildOf,
    Contains,
    References,
    BacklinksTo,
    SourceOf,
    DerivedFrom,
    Summarizes,
    Indexes,
    PointsTo,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum KernelProjectionKind {
    SidebarTree,
    PageTree,
    FileTree,
    Mindmap,
    ReadView,
    SearchResults,
    RagIndex,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum WorkspaceSourceKind {
    LocalFolder,
    ConvexWorkspace,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum WorkspaceSourceCapability {
    LoadSnapshot,
    Watch,
    PreflightCommand,
    ExecuteCommand,
    ResolvePageAggregate,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceSource {
    pub source_kind: WorkspaceSourceKind,
    pub root_uri: String,
    pub workspace_id: String,
    #[serde(default)]
    pub capabilities: Vec<WorkspaceSourceCapability>,
}

fn default_page_body_content_format() -> String {
    "editorBlocks".into()
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PageBodyWriteRequest {
    pub document_id: String,
    pub workspace_id: String,
    pub source_kind: WorkspaceSourceKind,
    pub root_uri: String,
    #[serde(
        default,
        alias = "expected_file_version",
        alias = "conflictDetectionKey",
        alias = "conflict_detection_key"
    )]
    pub expected_file_version: Option<String>,
    #[serde(default, alias = "write_intent_id")]
    pub write_intent_id: Option<String>,
    #[serde(default, alias = "save_operation_id")]
    pub save_operation_id: Option<String>,
    #[serde(default)]
    pub base_content_hash: Option<String>,
    #[serde(default = "default_page_body_content_format")]
    pub content_format: String,
    pub content: Value,
    #[serde(default)]
    pub editor_source: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum KernelProjectionCapability {
    Expand,
    Open,
    Drag,
    Drop,
    Select,
    CreateChild,
    Rename,
    Archive,
    Restore,
    ContextMenu,
    Reorder,
    OpenAsset,
    Pick,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum KernelProjectionRowKind {
    Document,
    Index,
    Asset,
    AssetFolder,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum KernelProjectionResourceKind {
    Workspace,
    Document,
    Index,
    Asset,
    AssetFolder,
    Mindmap,
    Attachment,
    OnlyOffice,
    Code,
    Table,
    Book,
    Pdf,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum KernelProjectionAssetKind {
    File,
    Mindmap,
    Table,
    Book,
    Pdf,
    Image,
    Video,
    Audio,
    Unknown,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum KernelObjectKind {
    Page,
    Index,
    Mindmap,
    Attachment,
    OnlyOffice,
    Code,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct KernelObjectIdentity {
    pub object_kind: KernelObjectKind,
    pub document_id: Option<String>,
    pub block_id: Option<String>,
    pub asset_id: Option<String>,
}

/// DocumentBuffer 打开状态 — 表达当前编辑器缓冲区对文件的打开态、dirty 状态和文件版本仲裁。
///
/// 五种状态：Clean（未修改）、Dirty（已修改未保存）、Stale（版本过时）、
/// ExternalModified（外部已修改）、Deleted（外部已删除）。
///
/// Phase A2 收口后，tiptap autosave、AI 写入、外部 watcher 均围绕此状态仲裁。
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DocBufferDirtyState {
    Clean,
    Dirty,
    Stale,
    ExternalModified,
    Deleted,
}

impl Default for DocBufferDirtyState {
    fn default() -> Self {
        Self::Clean
    }
}

/// DocumentBuffer 最小字段模型 — 表示一个已打开文档的缓冲区状态。
///
/// 不是持久化结构，而是运行时模型，用于仲裁写入冲突和外部修改检测。
/// 每个打开的编辑器（tiptap / AI session / external editor）对应一个实例。
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct DocumentBuffer {
    /// 文档在 workspace 中的统一路径身份
    pub workspace_path: ObjectWorkspacePath,
    /// 最后已知文件版本（来自 aggregate.fileVersion 或 disk mtime content-hash）
    pub file_version: Option<String>,
    /// 最后加载/保存时的内容 hash
    pub base_content_hash: Option<String>,
    /// 当前编辑器内容 hash（dirty 时与 base 不同）
    pub current_content_hash: Option<String>,
    /// 缓冲区 dirty 状态
    pub dirty_state: DocBufferDirtyState,
    /// 最后加载时间戳 (ms since epoch)
    pub last_loaded_at: Option<i64>,
    /// 最后保存时间戳 (ms since epoch)
    pub last_saved_at: Option<i64>,
    /// 外部修改者
    pub external_actor: Option<String>,
    /// 最近一次由 MNote 发起写入的意图 ID。
    #[serde(default)]
    pub last_write_intent_id: Option<String>,
    /// 最近一次保存请求实例 ID。
    #[serde(default)]
    pub last_save_operation_id: Option<String>,
}

impl DocumentBuffer {
    pub fn is_dirty(&self) -> bool {
        self.dirty_state == DocBufferDirtyState::Dirty
            || self.dirty_state == DocBufferDirtyState::Stale
    }

    pub fn mark_dirty(&mut self, content_hash: String) {
        self.current_content_hash = Some(content_hash);
        self.dirty_state = DocBufferDirtyState::Dirty;
    }

    pub fn mark_saved(&mut self, file_version: String, content_hash: String) {
        self.mark_saved_with_operation(file_version, content_hash, None, None);
    }

    pub fn mark_saved_with_operation(
        &mut self,
        file_version: String,
        content_hash: String,
        write_intent_id: Option<String>,
        save_operation_id: Option<String>,
    ) {
        self.file_version = Some(file_version);
        self.base_content_hash = Some(content_hash);
        self.current_content_hash = None;
        self.dirty_state = DocBufferDirtyState::Clean;
        if write_intent_id
            .as_deref()
            .map(str::trim)
            .is_some_and(|value| !value.is_empty())
        {
            self.last_write_intent_id = write_intent_id;
        }
        if save_operation_id
            .as_deref()
            .map(str::trim)
            .is_some_and(|value| !value.is_empty())
        {
            self.last_save_operation_id = save_operation_id;
        }
        self.last_saved_at = Some(
            std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap_or_default()
                .as_millis() as i64,
        );
    }

    pub fn mark_external_modified(&mut self, actor: Option<String>) {
        self.external_actor = actor;
        if self.is_dirty() {
            self.dirty_state = DocBufferDirtyState::Stale;
        } else {
            self.dirty_state = DocBufferDirtyState::ExternalModified;
        }
    }

    pub fn mark_deleted(&mut self) {
        self.dirty_state = DocBufferDirtyState::Deleted;
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct KernelBlockAssetRelation {
    pub document_id: String,
    pub block_id: String,
    pub asset_id: String,
    pub asset_kind: KernelProjectionAssetKind,
}

/// 统一 workspace 路径身份 — 表达"哪个 workspace root 下的哪个对象/文件/资源"。
///
/// 这是 Phase A1 收口后的主身份类型，用于：
/// - File Tree / Page Tree / Resource Tree 的 resourceMeta
/// - 本地文件写入时的身份验证
/// - 前端 sidebar/filetree 的统一 identity 消费
///
/// 相比旧的 `KernelObjectIdentity`，增加了 workspace 上下文：
/// `workspace_id`、`source_kind`、`root_uri`、`relative_path`。
///
/// 所有新代码应优先使用此类型；`KernelObjectIdentity` 保留为内核投影的兼容子集。
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ObjectWorkspacePath {
    pub workspace_id: String,
    pub source_kind: WorkspaceSourceKind,
    pub root_uri: String,
    pub relative_path: String,
    pub object_identity: KernelObjectIdentity,
    pub resource_kind: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum KernelGraphDirection {
    Outgoing,
    Incoming,
    Both,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct KernelSubtreeRef {
    pub root_node_id: String,
    #[serde(default)]
    pub path: Vec<String>,
    pub depth: Option<u32>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct KernelNodeMetadata {
    pub title: Option<String>,
    pub icon: Option<String>,
    #[serde(default)]
    pub tags: Vec<String>,
    pub created_at: Option<String>,
    pub updated_at: Option<String>,
    #[serde(default)]
    pub extra: BTreeMap<String, Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct KernelContentPayload {
    pub format: String,
    pub body: Value,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct KernelRefsPayload {
    #[serde(default)]
    pub reference_node_ids: Vec<String>,
    #[serde(default)]
    pub evidence_node_ids: Vec<String>,
    #[serde(default)]
    pub source_node_ids: Vec<String>,
    #[serde(default)]
    pub extra: BTreeMap<String, Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct KernelAuditStamp {
    pub version: u64,
    pub revision: Option<u64>,
    pub request_id: Option<String>,
    pub trace_id: Option<String>,
    pub actor_id: Option<String>,
}

impl Default for KernelAuditStamp {
    fn default() -> Self {
        Self {
            version: 1,
            revision: None,
            request_id: None,
            trace_id: None,
            actor_id: None,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct KernelNode {
    pub id: String,
    pub node_type: KernelNodeType,
    pub workspace_id: Option<String>,
    pub parent_id: Option<String>,
    pub subtree: Option<KernelSubtreeRef>,
    pub metadata: KernelNodeMetadata,
    pub content: Option<KernelContentPayload>,
    pub refs: Option<KernelRefsPayload>,
    #[serde(default)]
    pub audit: KernelAuditStamp,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct KernelEdge {
    pub id: String,
    pub edge_type: KernelEdgeType,
    pub workspace_id: Option<String>,
    pub from_node_id: String,
    pub to_node_id: String,
    #[serde(default)]
    pub metadata: BTreeMap<String, Value>,
    #[serde(default)]
    pub audit: KernelAuditStamp,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct KernelProjectionFilter {
    #[serde(default)]
    pub node_types: Vec<KernelNodeType>,
    #[serde(default)]
    pub edge_types: Vec<KernelEdgeType>,
    pub query: Option<String>,
    pub max_results: Option<usize>,
    #[serde(default)]
    pub include_deleted: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct KernelGetNode {
    pub node_id: String,
    pub workspace_id: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct KernelGetSubtree {
    pub subtree: KernelSubtreeRef,
    pub workspace_id: Option<String>,
    #[serde(default)]
    pub include_edges: bool,
    #[serde(default)]
    pub filters: KernelProjectionFilter,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct KernelListChildren {
    pub parent_node_id: String,
    pub workspace_id: Option<String>,
    #[serde(default)]
    pub node_types: Vec<KernelNodeType>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct KernelListEdges {
    pub node_id: String,
    pub workspace_id: Option<String>,
    #[serde(default)]
    pub edge_types: Vec<KernelEdgeType>,
    pub direction: Option<KernelGraphDirection>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct KernelTraverseGraph {
    pub start_node_id: String,
    pub workspace_id: Option<String>,
    #[serde(default)]
    pub edge_types: Vec<KernelEdgeType>,
    pub max_depth: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct KernelProjectionRequest {
    pub projection: KernelProjectionKind,
    pub workspace_id: Option<String>,
    pub root_node_id: Option<String>,
    pub subtree: Option<KernelSubtreeRef>,
    #[serde(default)]
    pub filters: KernelProjectionFilter,
    #[serde(default)]
    pub include_content: bool,
    #[serde(default)]
    pub include_edges: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct KernelSubtreeResult {
    pub root_node_id: String,
    pub nodes: Vec<KernelNode>,
    pub edges: Vec<KernelEdge>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct KernelEdgeListResult {
    pub node_id: String,
    pub edges: Vec<KernelEdge>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct KernelGraphVisit {
    pub node_id: String,
    pub depth: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct KernelGraphTraversalResult {
    pub start_node_id: String,
    pub visited: Vec<KernelGraphVisit>,
    pub edges: Vec<KernelEdge>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct KernelProjectionItem {
    pub row_id: String,
    pub row_kind: KernelProjectionRowKind,
    pub node_id: String,
    pub parent_node_id: Option<String>,
    pub node_type: KernelNodeType,
    pub projection_kind: KernelProjectionKind,
    pub title: Option<String>,
    pub depth: u32,
    pub position: Option<i64>,
    pub child_count: u32,
    pub expandable: bool,
    pub expanded_by_default: bool,
    #[serde(default)]
    pub capabilities: Vec<KernelProjectionCapability>,
    pub resource_meta: Option<KernelProjectionResourceMeta>,
    pub icon_hint: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct KernelProjectionResourceMeta {
    pub resource_kind: Option<KernelProjectionResourceKind>,
    pub document_id: Option<String>,
    pub asset_id: Option<String>,
    pub workspace_id: Option<String>,
    pub asset_kind: Option<KernelProjectionAssetKind>,
    pub icon_hint: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub object_identity: Option<KernelObjectIdentity>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub block_asset_relation: Option<KernelBlockAssetRelation>,
    #[serde(default)]
    pub extra: BTreeMap<String, Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct KernelProjectionResult {
    pub projection_id: String,
    pub projection: KernelProjectionKind,
    pub root_node_id: Option<String>,
    pub items: Vec<KernelProjectionItem>,
    pub edges: Vec<KernelEdge>,
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub meta: BTreeMap<String, Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DocumentReadNodeType {
    Page,
    Section,
    ContentNode,
    ReferenceAnchor,
    Mindmap,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct DocumentReadNodeMeta {
    pub title: Option<String>,
    pub text_snippet: Option<String>,
    pub block_type: Option<String>,
    pub heading_level: Option<u32>,
    pub numbering: Option<String>,
    pub child_count: u32,
    pub order: u32,
    #[serde(default)]
    pub path: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct DocumentReadNode {
    pub id: String,
    pub parent_node_id: Option<String>,
    pub node_type: DocumentReadNodeType,
    pub block_id: Option<String>,
    pub anchor_block_id: Option<String>,
    pub depth: u32,
    pub metadata: DocumentReadNodeMeta,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct DocumentReadSubtree {
    pub root_node_id: String,
    pub nodes: Vec<DocumentReadNode>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct DocumentReadOutlineEntry {
    pub id: String,
    pub node_id: String,
    pub anchor_block_id: Option<String>,
    pub title: String,
    pub level: u32,
    pub numbering: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DocumentReadEvidenceKind {
    Page,
    Heading,
    Paragraph,
    List,
    Todo,
    Quote,
    Code,
    Media,
    Reference,
    Table,
    Mindmap,
    Text,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct DocumentReadEvidenceItem {
    pub id: String,
    pub node_id: String,
    pub block_id: Option<String>,
    pub kind: DocumentReadEvidenceKind,
    pub snippet: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct DocumentReadStats {
    pub block_count: u32,
    pub heading_count: u32,
    pub evidence_count: u32,
    pub max_depth: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct DocumentReadPageSubtree {
    pub projection_id: String,
    pub projection: String,
    pub root_node_id: String,
    pub root_node: DocumentReadNode,
    pub subtree: DocumentReadSubtree,
    #[serde(default)]
    pub outline: Vec<DocumentReadOutlineEntry>,
    #[serde(default)]
    pub evidence: Vec<DocumentReadEvidenceItem>,
    pub stats: DocumentReadStats,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct DocumentContentResult {
    pub content: Value,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub editor_document: Option<Value>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub tiptap_document: Option<Value>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub block_document: Option<Value>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub block_projection_version: Option<u32>,
    pub revision: u64,
    pub conflict_detection_key: String,
    pub title: Option<String>,
    pub page_subtree: DocumentReadPageSubtree,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct KernelCreateNode {
    pub node: KernelNode,
    pub position: Option<i64>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct KernelUpdateNode {
    pub node_id: String,
    pub metadata: Option<KernelNodeMetadata>,
    pub content: Option<KernelContentPayload>,
    pub refs: Option<KernelRefsPayload>,
    pub audit: Option<KernelAuditStamp>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct KernelMoveSubtree {
    pub subtree: KernelSubtreeRef,
    pub new_parent_node_id: Option<String>,
    pub sort_order: Option<i64>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct KernelAttachEdge {
    pub edge: KernelEdge,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct KernelDetachEdge {
    pub edge_id: Option<String>,
    pub from_node_id: Option<String>,
    pub to_node_id: Option<String>,
    pub edge_type: Option<KernelEdgeType>,
}

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

    #[test]
    fn kernel_projection_request_roundtrip_keeps_subtree_and_filters() {
        let request = KernelProjectionRequest {
            projection: KernelProjectionKind::SidebarTree,
            workspace_id: Some("ws_1".into()),
            root_node_id: Some("page_root".into()),
            subtree: Some(KernelSubtreeRef {
                root_node_id: "page_root".into(),
                path: vec!["page_root".into(), "page_child".into()],
                depth: Some(2),
            }),
            filters: KernelProjectionFilter {
                node_types: vec![KernelNodeType::Page, KernelNodeType::Folder],
                edge_types: vec![KernelEdgeType::ParentOf],
                query: Some("预算".into()),
                max_results: Some(20),
                include_deleted: false,
            },
            include_content: false,
            include_edges: true,
        };

        let value = serde_json::to_value(&request).expect("request 应可序列化");
        assert_eq!(value["projection"], json!("sidebar_tree"));
        assert_eq!(value["subtree"]["rootNodeId"], json!("page_root"));
        assert_eq!(value["filters"]["query"], json!("预算"));
        assert_eq!(value["filters"]["maxResults"], json!(20));

        let decoded: KernelProjectionRequest =
            serde_json::from_value(value).expect("request 应可反序列化");
        assert_eq!(decoded, request);
    }

    #[test]
    fn kernel_node_serializes_content_refs_and_audit_boundaries() {
        let node = KernelNode {
            id: "node_1".into(),
            node_type: KernelNodeType::Summary,
            workspace_id: Some("ws_1".into()),
            parent_id: Some("page_1".into()),
            subtree: Some(KernelSubtreeRef {
                root_node_id: "page_1".into(),
                path: vec!["page_1".into(), "node_1".into()],
                depth: Some(1),
            }),
            metadata: KernelNodeMetadata {
                title: Some("摘要节点".into()),
                icon: None,
                tags: vec!["summary".into()],
                created_at: Some("2026-04-16T00:00:00Z".into()),
                updated_at: Some("2026-04-16T00:00:00Z".into()),
                extra: BTreeMap::new(),
            },
            content: Some(KernelContentPayload {
                format: "markdown".into(),
                body: json!({"text": "摘要正文"}),
            }),
            refs: Some(KernelRefsPayload {
                reference_node_ids: vec!["page_1".into()],
                evidence_node_ids: vec!["asset_1".into()],
                source_node_ids: vec!["pdf_1".into()],
                extra: BTreeMap::new(),
            }),
            audit: KernelAuditStamp {
                version: 3,
                revision: Some(9),
                request_id: Some("req_1".into()),
                trace_id: Some("trace_1".into()),
                actor_id: Some("user_1".into()),
            },
        };

        let value = serde_json::to_value(&node).expect("node 应可序列化");
        assert_eq!(value["nodeType"], json!("summary"));
        assert_eq!(value["content"]["format"], json!("markdown"));
        assert_eq!(value["refs"]["referenceNodeIds"], json!(["page_1"]));
        assert_eq!(value["audit"]["traceId"], json!("trace_1"));
    }

    #[test]
    fn kernel_projection_item_serializes_contract_fields() {
        let item = KernelProjectionItem {
            row_id: "asset:page_1".into(),
            row_kind: KernelProjectionRowKind::Asset,
            node_id: "page_1".into(),
            parent_node_id: None,
            node_type: KernelNodeType::Page,
            projection_kind: KernelProjectionKind::FileTree,
            title: Some("页面 1".into()),
            depth: 0,
            position: Some(1),
            child_count: 2,
            expandable: true,
            expanded_by_default: true,
            capabilities: vec![
                KernelProjectionCapability::Expand,
                KernelProjectionCapability::Open,
                KernelProjectionCapability::CreateChild,
            ],
            resource_meta: Some(KernelProjectionResourceMeta {
                resource_kind: Some(KernelProjectionResourceKind::Document),
                document_id: Some("page_1".into()),
                asset_id: None,
                workspace_id: Some("ws_1".into()),
                asset_kind: Some(KernelProjectionAssetKind::File),
                icon_hint: Some("page".into()),
                object_identity: None,
                block_asset_relation: None,
                extra: BTreeMap::new(),
            }),
            icon_hint: Some("page".into()),
        };

        let value = serde_json::to_value(&item).expect("projection item 应可序列化");
        assert_eq!(value["rowId"], json!("asset:page_1"));
        assert_eq!(value["rowKind"], json!("asset"));
        assert_eq!(value["projectionKind"], json!("file_tree"));
        assert_eq!(
            value["capabilities"],
            json!(["expand", "open", "create-child"])
        );
        assert_eq!(value["resourceMeta"]["resourceKind"], json!("document"));
        assert_eq!(value["iconHint"], json!("page"));
    }

    #[test]
    fn workspace_source_serializes_minimal_command_source_contract() {
        let source = WorkspaceSource {
            source_kind: WorkspaceSourceKind::ConvexWorkspace,
            root_uri: "convex://workspace/ws_1".into(),
            workspace_id: "ws_1".into(),
            capabilities: vec![
                WorkspaceSourceCapability::LoadSnapshot,
                WorkspaceSourceCapability::PreflightCommand,
                WorkspaceSourceCapability::ExecuteCommand,
            ],
        };

        let value = serde_json::to_value(&source).expect("workspace source 应可序列化");
        assert_eq!(value["sourceKind"], json!("convex_workspace"));
        assert_eq!(value["rootUri"], json!("convex://workspace/ws_1"));
        assert_eq!(value["workspaceId"], json!("ws_1"));
        assert_eq!(
            value["capabilities"],
            json!(["load-snapshot", "preflight-command", "execute-command"])
        );

        let decoded: WorkspaceSource =
            serde_json::from_value(value).expect("workspace source 应可反序列化");
        assert_eq!(decoded, source);
    }

    #[test]
    fn page_body_write_request_uses_file_version_contract() {
        let request: PageBodyWriteRequest = serde_json::from_value(json!({
            "documentId": "local-md:README.md",
            "workspaceId": "local-ws:user:my-space",
            "sourceKind": "local_folder",
            "rootUri": "file:///mnt/Data1T/Mnote_data/users/user/workspaces/my-space",
            "expectedFileVersion": "local-md:local-md:README.md:1:2:hash",
            "writeIntentId": "intent:editor:abc",
            "saveOperationId": "save:op:abc",
            "baseContentHash": "sha256:base",
            "contentFormat": "editorBlocks",
            "content": [{"type": "paragraph", "content": [{"type": "text", "text": "正文"}]}],
            "editorSource": "tiptap"
        }))
        .expect("page.body.write request 应可反序列化");

        assert_eq!(request.document_id, "local-md:README.md");
        assert_eq!(request.source_kind, WorkspaceSourceKind::LocalFolder);
        assert_eq!(
            request.expected_file_version.as_deref(),
            Some("local-md:local-md:README.md:1:2:hash")
        );
        assert_eq!(
            request.write_intent_id.as_deref(),
            Some("intent:editor:abc")
        );
        assert_eq!(request.save_operation_id.as_deref(), Some("save:op:abc"));
        assert_eq!(request.content_format, "editorBlocks");
        assert_eq!(request.editor_source.as_deref(), Some("tiptap"));

        let value = serde_json::to_value(&request).expect("request 应可序列化");
        assert_eq!(
            value["expectedFileVersion"],
            json!("local-md:local-md:README.md:1:2:hash")
        );
        assert_eq!(value["writeIntentId"], json!("intent:editor:abc"));
        assert_eq!(value["saveOperationId"], json!("save:op:abc"));
        assert_eq!(value["baseContentHash"], json!("sha256:base"));
        assert_eq!(value["contentFormat"], json!("editorBlocks"));
        assert_eq!(value["editorSource"], json!("tiptap"));
    }

    #[test]
    fn page_body_write_request_accepts_legacy_conflict_key_alias() {
        let request: PageBodyWriteRequest = serde_json::from_value(json!({
            "documentId": "local-md:README.md",
            "workspaceId": "local-ws:user:my-space",
            "sourceKind": "local_folder",
            "rootUri": "file:///tmp/workspace",
            "conflictDetectionKey": "legacy-key",
            "content": []
        }))
        .expect("legacy compat request 应可反序列化");

        assert_eq!(request.expected_file_version.as_deref(), Some("legacy-key"));
        assert_eq!(request.content_format, "editorBlocks");
        assert_eq!(request.base_content_hash, None);
        assert_eq!(request.editor_source, None);
    }

    #[test]
    fn object_workspace_path_round_trip() {
        let path = ObjectWorkspacePath {
            workspace_id: "local-ws:user:my-space".into(),
            source_kind: WorkspaceSourceKind::LocalFolder,
            root_uri: "file:///mnt/Data1T/Mnote_data/users/user/workspaces/my-space".into(),
            relative_path: "docs/README.md".into(),
            object_identity: KernelObjectIdentity {
                object_kind: KernelObjectKind::Page,
                document_id: Some("local-md:docs~2FREADME.md".into()),
                block_id: None,
                asset_id: None,
            },
            resource_kind: Some("document".into()),
        };

        let value = serde_json::to_value(&path).expect("ObjectWorkspacePath 应可序列化");
        assert_eq!(value["workspaceId"], json!("local-ws:user:my-space"));
        assert_eq!(value["sourceKind"], json!("local_folder"));
        assert_eq!(
            value["rootUri"],
            json!("file:///mnt/Data1T/Mnote_data/users/user/workspaces/my-space")
        );
        assert_eq!(value["relativePath"], json!("docs/README.md"));
        assert_eq!(value["objectIdentity"]["objectKind"], json!("page"));
        assert_eq!(
            value["objectIdentity"]["documentId"],
            json!("local-md:docs~2FREADME.md")
        );
        assert_eq!(value["resourceKind"], json!("document"));

        let decoded: ObjectWorkspacePath =
            serde_json::from_value(value).expect("ObjectWorkspacePath 应可反序列化");
        assert_eq!(decoded, path);
    }

    #[test]
    fn object_workspace_path_accepts_resource_kind_null() {
        #[derive(Deserialize)]
        struct Minimal {
            resource_kind: Option<String>,
        }
        let value = serde_json::json!({
            "workspaceId": "ws_1",
            "sourceKind": "convex_workspace",
            "rootUri": "convex://ws",
            "relativePath": "doc.md",
            "objectIdentity": {
                "objectKind": "page",
                "documentId": "doc_1",
                "blockId": null,
                "assetId": null
            }
        });
        let path: ObjectWorkspacePath =
            serde_json::from_value(value).expect("resource_kind null 应接受");
        assert_eq!(path.resource_kind, None);
    }

    #[test]
    fn document_buffer_round_trip() {
        let identity = KernelObjectIdentity {
            object_kind: KernelObjectKind::Page,
            document_id: Some("local-md:doc.md".into()),
            block_id: None,
            asset_id: None,
        };
        let path = ObjectWorkspacePath {
            workspace_id: "ws_1".into(),
            source_kind: WorkspaceSourceKind::LocalFolder,
            root_uri: "file:///tmp/root".into(),
            relative_path: "doc.md".into(),
            object_identity: identity,
            resource_kind: Some("document".into()),
        };
        let buf = DocumentBuffer {
            workspace_path: path,
            file_version: Some("v1".into()),
            base_content_hash: Some("sha256:abc".into()),
            current_content_hash: None,
            dirty_state: DocBufferDirtyState::Clean,
            last_loaded_at: Some(1_700_000_000_000i64),
            last_saved_at: Some(1_700_000_000_001i64),
            external_actor: None,
            last_write_intent_id: Some("intent:editor:abc".into()),
            last_save_operation_id: Some("save:op:abc".into()),
        };

        let value = serde_json::to_value(&buf).expect("DocumentBuffer 应可序列化");
        assert_eq!(value["dirtyState"], json!("clean"));
        assert_eq!(value["fileVersion"], json!("v1"));
        assert_eq!(value["lastWriteIntentId"], json!("intent:editor:abc"));
        assert_eq!(value["lastSaveOperationId"], json!("save:op:abc"));
        assert_eq!(value["workspacePath"]["relativePath"], json!("doc.md"));

        let decoded: DocumentBuffer =
            serde_json::from_value(value).expect("DocumentBuffer 应可反序列化");
        assert_eq!(decoded, buf);
    }

    #[test]
    fn document_buffer_state_transitions() {
        let identity = KernelObjectIdentity {
            object_kind: KernelObjectKind::Page,
            document_id: Some("local-md:doc.md".into()),
            block_id: None,
            asset_id: None,
        };
        let path = ObjectWorkspacePath {
            workspace_id: "ws_1".into(),
            source_kind: WorkspaceSourceKind::LocalFolder,
            root_uri: "file:///tmp/root".into(),
            relative_path: "doc.md".into(),
            object_identity: identity,
            resource_kind: Some("document".into()),
        };
        let mut buf = DocumentBuffer {
            workspace_path: path,
            file_version: Some("v1".into()),
            base_content_hash: Some("sha256:abc".into()),
            current_content_hash: None,
            dirty_state: DocBufferDirtyState::Clean,
            last_loaded_at: None,
            last_saved_at: None,
            external_actor: None,
            last_write_intent_id: None,
            last_save_operation_id: None,
        };

        assert!(!buf.is_dirty());
        assert_eq!(buf.dirty_state, DocBufferDirtyState::Clean);

        buf.mark_dirty("sha256:dirty".into());
        assert!(buf.is_dirty());
        assert_eq!(buf.dirty_state, DocBufferDirtyState::Dirty);

        buf.mark_external_modified(Some("external-editor".into()));
        assert!(buf.is_dirty());
        assert_eq!(buf.dirty_state, DocBufferDirtyState::Stale);
        assert_eq!(buf.external_actor.as_deref(), Some("external-editor"));

        buf.mark_saved("v2".into(), "sha256:saved".into());
        assert!(!buf.is_dirty());
        assert_eq!(buf.dirty_state, DocBufferDirtyState::Clean);
        assert_eq!(buf.file_version.as_deref(), Some("v2"));

        buf.mark_external_modified(None);
        assert_eq!(buf.dirty_state, DocBufferDirtyState::ExternalModified);

        buf.mark_deleted();
        assert_eq!(buf.dirty_state, DocBufferDirtyState::Deleted);
    }
}
