/// JSON Schema types with strong typing
///
/// This module provides strongly-typed JSON Schema structures that plugins
/// use to describe their methods and parameters.
///
/// Schema generation is fully automatic via schemars. By using proper types
/// (uuid::Uuid instead of String) and doc comments, schemars generates complete
/// schemas with format annotations, descriptions, and required arrays.

use schemars::{JsonSchema, schema_for};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

use super::bidirectional::{StandardRequest, StandardResponse};

// =============================================================================
// HTTP Method Enum
// =============================================================================

/// HTTP method for REST endpoint routing
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "UPPERCASE")]
pub enum HttpMethod {
    /// GET: Idempotent read operations with no side effects
    Get,
    /// POST: Create operations or non-idempotent actions (default)
    Post,
    /// PUT: Replace/update operations (idempotent)
    Put,
    /// DELETE: Remove operations (idempotent)
    Delete,
    /// PATCH: Partial update operations
    Patch,
}

impl Default for HttpMethod {
    fn default() -> Self {
        HttpMethod::Post
    }
}

impl HttpMethod {
    /// Parse from string (case-insensitive)
    pub fn from_str(s: &str) -> Option<Self> {
        match s.to_uppercase().as_str() {
            "GET" => Some(HttpMethod::Get),
            "POST" => Some(HttpMethod::Post),
            "PUT" => Some(HttpMethod::Put),
            "DELETE" => Some(HttpMethod::Delete),
            "PATCH" => Some(HttpMethod::Patch),
            _ => None,
        }
    }

    /// Convert to uppercase string
    pub fn as_str(&self) -> &'static str {
        match self {
            HttpMethod::Get => "GET",
            HttpMethod::Post => "POST",
            HttpMethod::Put => "PUT",
            HttpMethod::Delete => "DELETE",
            HttpMethod::Patch => "PATCH",
        }
    }
}

// ============================================================================
// Plugin Schema
// ============================================================================

/// A plugin's schema with methods and child summaries.
///
/// Children are represented as summaries (namespace, description, hash) rather
/// than full recursive schemas. This enables lazy traversal - clients can fetch
/// child schemas individually via `{namespace}.schema`.
///
/// - Leaf plugins have `children = None`
/// - Hub plugins have `children = Some([ChildSummary, ...])`
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct PluginSchema {
    /// The plugin's namespace (e.g., "echo", "plexus")
    pub namespace: String,

    /// The plugin's version (e.g., "1.0.0")
    pub version: String,

    /// Short description of the plugin (max 15 words)
    pub description: String,

    /// Detailed description of the plugin (optional)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub long_description: Option<String>,

    /// Hash of ONLY this plugin's methods (ignores children)
    /// Changes when method signatures, names, or descriptions change
    pub self_hash: String,

    /// Hash of ONLY child plugin hashes (None for leaf plugins)
    /// Changes when any child's hash changes (recursively)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub children_hash: Option<String>,

    /// Composite hash = hash(self_hash + children_hash)
    /// Use this if you want a single hash for the entire subtree
    /// Backward compatible with previous single-hash system
    pub hash: String,

    /// Methods exposed by this plugin
    pub methods: Vec<MethodSchema>,

    /// Child plugin summaries (None = leaf plugin, Some = hub plugin)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub children: Option<Vec<ChildSummary>>,

    /// JSON Schema for the HTTP request type this activation extracts from incoming connections.
    ///
    /// Present when the activation declares `request = MyRequest` in `#[plexus::activation(...)]`.
    /// The schema includes `x-plexus-source` extension fields on each property describing
    /// where each field is sourced from (cookie, header, query param, peer address, etc.).
    ///
    /// Clients can use this to understand what request data the activation expects and
    /// to generate appropriate authentication/context documentation.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub request: Option<serde_json::Value>,
}

/// Result of a schema query - either full plugin or single method
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum SchemaResult {
    /// Full plugin schema (when no method specified)
    Plugin(PluginSchema),
    /// Single method schema (when method specified)
    Method(MethodSchema),
}

/// Schema for a single method exposed by a plugin
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MethodSchema {
    /// Method name (e.g., "echo", "check")
    pub name: String,

    /// Human-readable description of what this method does
    pub description: String,

    /// Content hash of the method definition (for cache invalidation)
    /// Generated by hashing the method signature within hub-macro
    pub hash: String,

    /// JSON Schema for the method's parameters (None if no params)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub params: Option<schemars::Schema>,

    /// JSON Schema for the method's return type (None if not specified)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub returns: Option<schemars::Schema>,

    /// Whether this method streams multiple events (true) or returns a single result (false)
    ///
    /// - `streaming: true` → returns `AsyncGenerator<T>` (multiple events)
    /// - `streaming: false` → returns `Promise<T>` (single event, collected)
    ///
    /// All methods use the same streaming protocol under the hood, but this flag
    /// tells clients how to present the result.
    #[serde(default)]
    pub streaming: bool,

    /// Whether this method supports bidirectional communication
    ///
    /// When true, the server can send requests to the client during method execution
    /// and wait for responses (e.g., confirmations, prompts, selections).
    #[serde(default)]
    pub bidirectional: bool,

    /// HTTP method for REST endpoints (GET, POST, PUT, DELETE, PATCH)
    ///
    /// This field is used by the HTTP gateway to determine which HTTP method
    /// to use when exposing this method as a REST endpoint. Defaults to POST
    /// for backward compatibility.
    ///
    /// - GET: Idempotent read operations (no side effects)
    /// - POST: Create operations or non-idempotent actions (default)
    /// - PUT: Replace/update operations (idempotent)
    /// - DELETE: Remove operations (idempotent)
    /// - PATCH: Partial update operations
    #[serde(default)]
    pub http_method: HttpMethod,

    /// JSON Schema for the request type sent from server to client
    ///
    /// Only relevant when `bidirectional: true`. Describes the structure of
    /// requests the server may send during method execution.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub request_type: Option<schemars::Schema>,

    /// JSON Schema for the response type sent from client to server
    ///
    /// Only relevant when `bidirectional: true`. Describes the structure of
    /// responses the client should send in reply to server requests.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub response_type: Option<schemars::Schema>,
}

impl PluginSchema {
    /// Compute all three hashes (self, children, composite)
    fn compute_hashes(
        methods: &[MethodSchema],
        children: Option<&[ChildSummary]>,
    ) -> (String, Option<String>, String) {
        use std::collections::hash_map::DefaultHasher;
        use std::hash::{Hash, Hasher};

        // Compute self_hash (methods only)
        let mut self_hasher = DefaultHasher::new();
        for m in methods {
            m.hash.hash(&mut self_hasher);
        }
        let self_hash = format!("{:016x}", self_hasher.finish());

        // Compute children_hash (children only)
        let children_hash = children.map(|kids| {
            let mut children_hasher = DefaultHasher::new();
            for c in kids {
                c.hash.hash(&mut children_hasher);
            }
            format!("{:016x}", children_hasher.finish())
        });

        // Compute composite hash (both)
        let mut composite_hasher = DefaultHasher::new();
        self_hash.hash(&mut composite_hasher);
        if let Some(ref ch) = children_hash {
            ch.hash(&mut composite_hasher);
        }
        let hash = format!("{:016x}", composite_hasher.finish());

        (self_hash, children_hash, hash)
    }

    /// Validate no name collisions exist within a plugin
    ///
    /// Checks for:
    /// - Duplicate method names
    /// - Duplicate child names (for hubs)
    /// - Method/child name collisions (for hubs)
    ///
    /// Panics if a collision is detected (system error).
    fn validate_no_collisions(
        namespace: &str,
        methods: &[MethodSchema],
        children: Option<&[ChildSummary]>,
    ) {
        use std::collections::HashSet;

        let mut seen: HashSet<&str> = HashSet::new();

        // Check method names
        for m in methods {
            if !seen.insert(&m.name) {
                panic!(
                    "Name collision in plugin '{}': duplicate method '{}'",
                    namespace, m.name
                );
            }
        }

        // Check child names (and collisions with methods)
        if let Some(kids) = children {
            for c in kids {
                if !seen.insert(&c.namespace) {
                    // Could be duplicate child or collision with method
                    let collision_type = if methods.iter().any(|m| m.name == c.namespace) {
                        "method/child collision"
                    } else {
                        "duplicate child"
                    };
                    panic!(
                        "Name collision in plugin '{}': {} for '{}'",
                        namespace, collision_type, c.namespace
                    );
                }
            }
        }
    }

    /// Create a new leaf plugin schema (no children)
    pub fn leaf(
        namespace: impl Into<String>,
        version: impl Into<String>,
        description: impl Into<String>,
        methods: Vec<MethodSchema>,
    ) -> Self {
        let namespace = namespace.into();
        Self::validate_no_collisions(&namespace, &methods, None);
        let (self_hash, children_hash, hash) = Self::compute_hashes(&methods, None);
        Self {
            namespace,
            version: version.into(),
            description: description.into(),
            long_description: None,
            self_hash,
            children_hash,
            hash,
            methods,
            children: None,
            request: None,
        }
    }

    /// Create a new leaf plugin schema with long description
    pub fn leaf_with_long_description(
        namespace: impl Into<String>,
        version: impl Into<String>,
        description: impl Into<String>,
        long_description: impl Into<String>,
        methods: Vec<MethodSchema>,
    ) -> Self {
        let namespace = namespace.into();
        Self::validate_no_collisions(&namespace, &methods, None);
        let (self_hash, children_hash, hash) = Self::compute_hashes(&methods, None);
        Self {
            namespace,
            version: version.into(),
            description: description.into(),
            long_description: Some(long_description.into()),
            self_hash,
            children_hash,
            hash,
            methods,
            children: None,
            request: None,
        }
    }

    /// Create a new hub plugin schema (with child summaries)
    pub fn hub(
        namespace: impl Into<String>,
        version: impl Into<String>,
        description: impl Into<String>,
        methods: Vec<MethodSchema>,
        children: Vec<ChildSummary>,
    ) -> Self {
        let namespace = namespace.into();
        Self::validate_no_collisions(&namespace, &methods, Some(&children));
        let (self_hash, children_hash, hash) = Self::compute_hashes(&methods, Some(&children));
        Self {
            namespace,
            version: version.into(),
            description: description.into(),
            long_description: None,
            self_hash,
            children_hash,
            hash,
            methods,
            children: Some(children),
            request: None,
        }
    }

    /// Create a new hub plugin schema with long description
    pub fn hub_with_long_description(
        namespace: impl Into<String>,
        version: impl Into<String>,
        description: impl Into<String>,
        long_description: impl Into<String>,
        methods: Vec<MethodSchema>,
        children: Vec<ChildSummary>,
    ) -> Self {
        let namespace = namespace.into();
        Self::validate_no_collisions(&namespace, &methods, Some(&children));
        let (self_hash, children_hash, hash) = Self::compute_hashes(&methods, Some(&children));
        Self {
            namespace,
            version: version.into(),
            description: description.into(),
            long_description: Some(long_description.into()),
            self_hash,
            children_hash,
            hash,
            methods,
            children: Some(children),
            request: None,
        }
    }

    /// Check if this is a hub (has children)
    pub fn is_hub(&self) -> bool {
        self.children.is_some()
    }

    /// Check if this is a leaf (no children)
    pub fn is_leaf(&self) -> bool {
        self.children.is_none()
    }
}

/// Summary of a child plugin
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ChildSummary {
    /// The child's namespace
    pub namespace: String,

    /// Human-readable description
    pub description: String,

    /// Content hash for cache invalidation
    pub hash: String,
}

/// Schema summary containing only hashes (for cache validation)
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct PluginHashes {
    pub namespace: String,
    pub self_hash: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub children_hash: Option<String>,
    pub hash: String,
    /// Child plugin hashes (for recursive checking)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub children: Option<Vec<ChildHashes>>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ChildHashes {
    pub namespace: String,
    pub hash: String,
}

impl MethodSchema {
    /// Create a new method schema with name, description, and hash
    ///
    /// The hash should be computed from the method definition string
    /// within the hub-macro at compile time.
    pub fn new(
        name: impl Into<String>,
        description: impl Into<String>,
        hash: impl Into<String>,
    ) -> Self {
        Self {
            name: name.into(),
            description: description.into(),
            hash: hash.into(),
            params: None,
            returns: None,
            streaming: false,
            bidirectional: false,
            http_method: HttpMethod::default(),
            request_type: None,
            response_type: None,
        }
    }

    /// Add parameter schema
    pub fn with_params(mut self, params: schemars::Schema) -> Self {
        self.params = Some(params);
        self
    }

    /// Add return type schema
    pub fn with_returns(mut self, returns: schemars::Schema) -> Self {
        self.returns = Some(returns);
        self
    }

    /// Set the streaming flag
    ///
    /// - `true` → method streams multiple events (use `AsyncGenerator<T>`)
    /// - `false` → method returns single result (use `Promise<T>`)
    pub fn with_streaming(mut self, streaming: bool) -> Self {
        self.streaming = streaming;
        self
    }

    /// Set the HTTP method for REST endpoints
    ///
    /// Defaults to POST for backward compatibility.
    ///
    /// # Guidelines
    /// - GET: Idempotent read operations with no side effects
    /// - POST: Create operations or non-idempotent actions
    /// - PUT: Replace/update operations (idempotent)
    /// - DELETE: Remove operations (idempotent)
    /// - PATCH: Partial update operations
    pub fn with_http_method(mut self, http_method: HttpMethod) -> Self {
        self.http_method = http_method;
        self
    }

    /// Set whether this method supports bidirectional communication
    ///
    /// When true, the server can send requests to the client during method
    /// execution and wait for responses.
    pub fn with_bidirectional(mut self, bidirectional: bool) -> Self {
        self.bidirectional = bidirectional;
        self
    }

    /// Set the JSON Schema for server-to-client request types
    ///
    /// Only relevant when `bidirectional: true`. Use `schema_for!(YourRequestType)`
    /// to generate the schema.
    pub fn with_request_type(mut self, schema: schemars::Schema) -> Self {
        self.request_type = Some(schema);
        self
    }

    /// Set the JSON Schema for client-to-server response types
    ///
    /// Only relevant when `bidirectional: true`. Use `schema_for!(YourResponseType)`
    /// to generate the schema.
    pub fn with_response_type(mut self, schema: schemars::Schema) -> Self {
        self.response_type = Some(schema);
        self
    }

    /// Configure method for standard bidirectional communication
    ///
    /// Sets `bidirectional: true` and configures request/response types to use
    /// `StandardRequest` and `StandardResponse`, which support common UI patterns
    /// like confirmations, prompts, and selections.
    pub fn with_standard_bidirectional(self) -> Self {
        self.with_bidirectional(true)
            .with_request_type(schema_for!(StandardRequest).into())
            .with_response_type(schema_for!(StandardResponse).into())
    }
}

// ============================================================================
// JSON Schema Types
// ============================================================================

/// A complete JSON Schema with metadata
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Schema {
    /// The JSON Schema specification version
    #[serde(rename = "$schema", skip_serializing_if = "Option::is_none", default)]
    pub schema_version: Option<String>,

    /// Title of the schema
    #[serde(skip_serializing_if = "Option::is_none")]
    pub title: Option<String>,

    /// Description of what this schema represents
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,

    /// The schema type (typically "object" for root, can be string or array)
    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
    pub schema_type: Option<serde_json::Value>,

    /// Properties for object types
    #[serde(skip_serializing_if = "Option::is_none")]
    pub properties: Option<HashMap<String, SchemaProperty>>,

    /// Required properties
    #[serde(skip_serializing_if = "Option::is_none")]
    pub required: Option<Vec<String>>,

    /// Enum variants (for discriminated unions)
    #[serde(rename = "oneOf", skip_serializing_if = "Option::is_none")]
    pub one_of: Option<Vec<Schema>>,

    /// Schema definitions (for $defs or definitions)
    #[serde(rename = "$defs", skip_serializing_if = "Option::is_none")]
    pub defs: Option<HashMap<String, serde_json::Value>>,

    /// Any additional schema properties
    #[serde(flatten)]
    pub additional: HashMap<String, serde_json::Value>,
}

/// Schema type enumeration
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum SchemaType {
    Object,
    Array,
    String,
    Number,
    Integer,
    Boolean,
    Null,
}

/// A property definition in a schema
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaProperty {
    /// The type of this property (can be a single type or array of types for nullable)
    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
    pub property_type: Option<serde_json::Value>,

    /// Description of this property
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,

    /// Format hint (e.g., "uuid", "date-time", "email")
    #[serde(skip_serializing_if = "Option::is_none")]
    pub format: Option<String>,

    /// For array types, the schema of items
    #[serde(skip_serializing_if = "Option::is_none")]
    pub items: Option<Box<SchemaProperty>>,

    /// For object types, nested properties
    #[serde(skip_serializing_if = "Option::is_none")]
    pub properties: Option<HashMap<String, SchemaProperty>>,

    /// Required properties (for object types)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub required: Option<Vec<String>>,

    /// Default value for this property
    #[serde(skip_serializing_if = "Option::is_none")]
    pub default: Option<serde_json::Value>,

    /// Enum values if this is an enum
    #[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
    pub enum_values: Option<Vec<serde_json::Value>>,

    /// Reference to another schema definition
    #[serde(rename = "$ref", skip_serializing_if = "Option::is_none")]
    pub reference: Option<String>,

    /// Any additional property metadata
    #[serde(flatten)]
    pub additional: HashMap<String, serde_json::Value>,
}

impl Schema {
    /// Create a new schema with basic metadata
    pub fn new(title: impl Into<String>, description: impl Into<String>) -> Self {
        Self {
            schema_version: Some("http://json-schema.org/draft-07/schema#".to_string()),
            title: Some(title.into()),
            description: Some(description.into()),
            schema_type: None,
            properties: None,
            required: None,
            one_of: None,
            defs: None,
            additional: HashMap::new(),
        }
    }

    /// Create an object schema
    pub fn object() -> Self {
        Self {
            schema_version: Some("http://json-schema.org/draft-07/schema#".to_string()),
            title: None,
            description: None,
            schema_type: Some(serde_json::json!("object")),
            properties: Some(HashMap::new()),
            required: None,
            one_of: None,
            defs: None,
            additional: HashMap::new(),
        }
    }

    /// Add a property to this schema
    pub fn with_property(mut self, name: impl Into<String>, property: SchemaProperty) -> Self {
        self.properties
            .get_or_insert_with(HashMap::new)
            .insert(name.into(), property);
        self
    }

    /// Mark a property as required
    pub fn with_required(mut self, name: impl Into<String>) -> Self {
        self.required
            .get_or_insert_with(Vec::new)
            .push(name.into());
        self
    }

    /// Set the description
    pub fn with_description(mut self, description: impl Into<String>) -> Self {
        self.description = Some(description.into());
        self
    }

    /// Extract a single method's schema from the oneOf array
    ///
    /// Searches the oneOf variants for a method matching the given name.
    /// Returns the variant schema if found, None otherwise.
    pub fn get_method_schema(&self, method_name: &str) -> Option<Schema> {
        let variants = self.one_of.as_ref()?;

        for variant in variants {
            // Check if this variant has a "method" property with const or enum
            if let Some(props) = &variant.properties {
                if let Some(method_prop) = props.get("method") {
                    // Try "const" first (schemars uses this for literal values)
                    if let Some(const_val) = method_prop.additional.get("const") {
                        if const_val.as_str() == Some(method_name) {
                            return Some(variant.clone());
                        }
                    }
                    // Fall back to enum_values
                    if let Some(enum_vals) = &method_prop.enum_values {
                        if enum_vals.first().and_then(|v| v.as_str()) == Some(method_name) {
                            return Some(variant.clone());
                        }
                    }
                }
            }
        }
        None
    }

    /// List all method names from the oneOf array
    pub fn list_methods(&self) -> Vec<String> {
        let Some(variants) = &self.one_of else {
            return Vec::new();
        };

        variants
            .iter()
            .filter_map(|variant| {
                let props = variant.properties.as_ref()?;
                let method_prop = props.get("method")?;

                // Try "const" first
                if let Some(const_val) = method_prop.additional.get("const") {
                    return const_val.as_str().map(String::from);
                }
                // Fall back to enum_values
                method_prop
                    .enum_values
                    .as_ref()?
                    .first()?
                    .as_str()
                    .map(String::from)
            })
            .collect()
    }
}

impl SchemaProperty {
    /// Create a string property
    pub fn string() -> Self {
        Self {
            property_type: Some(serde_json::json!("string")),
            description: None,
            format: None,
            items: None,
            properties: None,
            required: None,
            default: None,
            enum_values: None,
            reference: None,
            additional: HashMap::new(),
        }
    }

    /// Create a UUID property (string with format)
    pub fn uuid() -> Self {
        Self {
            property_type: Some(serde_json::json!("string")),
            description: None,
            format: Some("uuid".to_string()),
            items: None,
            properties: None,
            required: None,
            default: None,
            enum_values: None,
            reference: None,
            additional: HashMap::new(),
        }
    }

    /// Create an integer property
    pub fn integer() -> Self {
        Self {
            property_type: Some(serde_json::json!("integer")),
            description: None,
            format: None,
            items: None,
            properties: None,
            required: None,
            default: None,
            enum_values: None,
            reference: None,
            additional: HashMap::new(),
        }
    }

    /// Create an object property
    pub fn object() -> Self {
        Self {
            property_type: Some(serde_json::json!("object")),
            description: None,
            format: None,
            items: None,
            properties: Some(HashMap::new()),
            required: None,
            default: None,
            enum_values: None,
            reference: None,
            additional: HashMap::new(),
        }
    }

    /// Create an array property
    pub fn array(items: SchemaProperty) -> Self {
        Self {
            property_type: Some(serde_json::json!("array")),
            description: None,
            format: None,
            items: Some(Box::new(items)),
            properties: None,
            required: None,
            default: None,
            enum_values: None,
            reference: None,
            additional: HashMap::new(),
        }
    }

    /// Add a description
    pub fn with_description(mut self, description: impl Into<String>) -> Self {
        self.description = Some(description.into());
        self
    }

    /// Add a default value
    pub fn with_default(mut self, default: serde_json::Value) -> Self {
        self.default = Some(default);
        self
    }

    /// Add nested properties (for object types)
    pub fn with_property(mut self, name: impl Into<String>, property: SchemaProperty) -> Self {
        self.properties
            .get_or_insert_with(HashMap::new)
            .insert(name.into(), property);
        self
    }
}

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

    #[test]
    fn test_schema_creation() {
        let schema = Schema::object()
            .with_property("id", SchemaProperty::uuid().with_description("The unique identifier"))
            .with_property("name", SchemaProperty::string().with_description("The name"))
            .with_required("id");

        assert_eq!(schema.schema_type, Some(serde_json::json!("object")));
        assert!(schema.properties.is_some());
        assert_eq!(schema.required, Some(vec!["id".to_string()]));
    }

    #[test]
    fn test_serialization() {
        let schema = Schema::object()
            .with_property("id", SchemaProperty::uuid());

        let json = serde_json::to_string_pretty(&schema).unwrap();
        assert!(json.contains("uuid"));
    }

    #[test]
    fn test_self_hash_changes_on_method_change() {
        let schema1 = PluginSchema::leaf(
            "test",
            "1.0",
            "desc",
            vec![MethodSchema::new("foo", "bar", "hash1")],
        );

        let schema2 = PluginSchema::leaf(
            "test",
            "1.0",
            "desc",
            vec![MethodSchema::new("foo", "baz", "hash2")],  // Changed description
        );

        assert_ne!(schema1.self_hash, schema2.self_hash, "self_hash should change when methods change");
        assert_eq!(schema1.children_hash, schema2.children_hash, "children_hash should stay same (both None)");
        assert_ne!(schema1.hash, schema2.hash, "composite hash should change");
    }

    #[test]
    fn test_children_hash_changes_on_child_change() {
        let child1 = ChildSummary {
            namespace: "child".into(),
            description: "desc".into(),
            hash: "old_hash".into(),
        };

        let child2 = ChildSummary {
            namespace: "child".into(),
            description: "desc".into(),
            hash: "new_hash".into(),
        };

        let schema1 = PluginSchema::hub(
            "parent",
            "1.0",
            "desc",
            vec![],
            vec![child1],
        );

        let schema2 = PluginSchema::hub(
            "parent",
            "1.0",
            "desc",
            vec![],
            vec![child2],
        );

        assert_eq!(schema1.self_hash, schema2.self_hash, "self_hash should stay same (no methods changed)");
        assert_ne!(schema1.children_hash, schema2.children_hash, "children_hash should change when child hash changes");
        assert_ne!(schema1.hash, schema2.hash, "composite hash should change");
    }

    #[test]
    fn test_leaf_has_no_children_hash() {
        let schema = PluginSchema::leaf(
            "leaf",
            "1.0",
            "desc",
            vec![MethodSchema::new("method", "desc", "hash")],
        );

        assert!(schema.children_hash.is_none(), "leaf plugin should have None for children_hash");
        assert_ne!(schema.self_hash, schema.hash, "leaf plugin's composite hash is hash(self_hash), not equal to self_hash");
    }
}
