use collections::{HashMap, IndexMap};
use schemars::{JsonSchema, json_schema};
use serde::{Deserialize, Serialize};
use settings_macros::{MergeFrom, with_fallible_options};
use std::sync::Arc;
use std::{borrow::Cow, path::PathBuf};

use crate::ExtendingVec;

use crate::DockPosition;

/// Where to position the threads sidebar.
#[derive(
    Clone,
    Copy,
    Debug,
    Default,
    PartialEq,
    Eq,
    Serialize,
    Deserialize,
    JsonSchema,
    MergeFrom,
    strum::VariantArray,
    strum::VariantNames,
)]
#[serde(rename_all = "snake_case")]
pub enum SidebarDockPosition {
    /// Always show the sidebar on the left side.
    #[default]
    Left,
    /// Always show the sidebar on the right side.
    Right,
}

#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub enum SidebarSide {
    #[default]
    Left,
    Right,
}

/// How thinking blocks should be displayed by default in the agent panel.
#[derive(
    Clone,
    Copy,
    Debug,
    Default,
    PartialEq,
    Eq,
    Serialize,
    Deserialize,
    JsonSchema,
    MergeFrom,
    strum::VariantArray,
    strum::VariantNames,
)]
#[serde(rename_all = "snake_case")]
pub enum ThinkingBlockDisplay {
    /// Thinking blocks fully expand during streaming, then auto-collapse
    /// when the model finishes thinking. Users can re-expand after collapse.
    #[default]
    Auto,
    /// Thinking blocks auto-expand with a height constraint during streaming,
    /// then remain in their constrained state when complete. Users can click
    /// to fully expand or collapse.
    Preview,
    /// Thinking blocks are always fully expanded by default (no height constraint).
    AlwaysExpanded,
    /// Thinking blocks are always collapsed by default.
    AlwaysCollapsed,
}

#[with_fallible_options]
#[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, Default)]
pub struct AgentSettingsContent {
    /// Whether the Agent is enabled.
    ///
    /// Default: true
    pub enabled: Option<bool>,
    /// Whether to show the agent panel button in the status bar.
    ///
    /// Default: true
    pub button: Option<bool>,
    /// Where to dock the agent panel.
    ///
    /// Default: left
    pub dock: Option<DockPosition>,
    /// Whether the agent panel should use flexible (proportional) sizing.
    ///
    /// Default: true
    pub flexible: Option<bool>,
    /// Where to position the threads sidebar.
    ///
    /// Default: left
    pub sidebar_side: Option<SidebarDockPosition>,
    /// Default width in pixels when the agent panel is docked to the left or right.
    ///
    /// Default: 640
    #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
    pub default_width: Option<f32>,
    /// Default height in pixels when the agent panel is docked to the bottom.
    ///
    /// Default: 320
    #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
    pub default_height: Option<f32>,
    /// Whether to limit the content width in the agent panel. When enabled,
    /// content will be constrained to `max_content_width` and centered when
    /// the panel is wider than that value, for optimal readability.
    ///
    /// Default: true
    pub limit_content_width: Option<bool>,
    /// Maximum content width in pixels for the agent panel. Content will be
    /// centered when the panel is wider than this value.
    ///
    /// Default: 850
    #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
    pub max_content_width: Option<f32>,
    /// The default model to use when creating new chats and for other features when a specific model is not specified.
    pub default_model: Option<LanguageModelSelection>,
    /// The model to use for subagents spawned via the `spawn_agent` tool. Defaults to the parent agent's model when not specified.
    pub subagent_model: Option<LanguageModelSelection>,
    /// Favorite models to show at the top of the model selector.
    #[serde(default)]
    pub favorite_models: Vec<LanguageModelSelection>,
    /// Model to use for the inline assistant. Defaults to default_model when not specified.
    pub inline_assistant_model: Option<LanguageModelSelection>,
    /// Model to use for the inline assistant when streaming tools are enabled.
    ///
    /// Default: true
    pub inline_assistant_use_streaming_tools: Option<bool>,
    /// Model to use for generating git commit messages. Defaults to default_model when not specified.
    pub commit_message_model: Option<LanguageModelSelection>,
    /// Model to use for generating thread summaries. Defaults to default_model when not specified.
    pub thread_summary_model: Option<LanguageModelSelection>,
    /// Additional models with which to generate alternatives when performing inline assists.
    pub inline_alternatives: Option<Vec<LanguageModelSelection>>,
    /// The default profile to use in the Agent.
    ///
    /// Default: write
    pub default_profile: Option<Arc<str>>,
    /// The available agent profiles.
    pub profiles: Option<IndexMap<Arc<str>, AgentProfileContent>>,
    /// Where to show a popup notification when the agent is waiting for user input.
    ///
    /// Default: "primary_screen"
    pub notify_when_agent_waiting: Option<NotifyWhenAgentWaiting>,
    /// When to play a sound when the agent has either completed its response, or needs user input.
    ///
    /// Default: never
    pub play_sound_when_agent_done: Option<PlaySoundWhenAgentDone>,
    /// Whether to display agent edits in single-file editors in addition to the review multibuffer pane.
    ///
    /// Default: false
    pub single_file_review: Option<bool>,
    /// Additional parameters for language model requests. When making a request
    /// to a model, parameters will be taken from the last entry in this list
    /// that matches the model's provider and name. In each entry, both provider
    /// and model are optional, so that you can specify parameters for either
    /// one.
    ///
    /// Default: []
    #[serde(default)]
    pub model_parameters: Vec<LanguageModelParameters>,
    /// Whether to show thumb buttons for feedback in the agent panel.
    ///
    /// Default: true
    pub enable_feedback: Option<bool>,
    /// Whether to have edit cards in the agent panel expanded, showing a preview of the full diff.
    ///
    /// Default: true
    pub expand_edit_card: Option<bool>,
    /// Whether to have terminal cards in the agent panel expanded, showing the whole command output.
    ///
    /// Default: true
    pub expand_terminal_card: Option<bool>,
    /// How thinking blocks should be displayed by default in the agent panel.
    ///
    /// Default: automatic
    pub thinking_display: Option<ThinkingBlockDisplay>,
    /// Whether clicking the stop button on a running terminal tool should also cancel the agent's generation.
    /// Note that this only applies to the stop button, not to ctrl+c inside the terminal.
    ///
    /// Default: true
    pub cancel_generation_on_terminal_stop: Option<bool>,
    /// Whether to always use cmd-enter (or ctrl-enter on Linux or Windows) to send messages in the agent panel.
    ///
    /// Default: false
    pub use_modifier_to_send: Option<bool>,
    /// Minimum number of lines of height the agent message editor should have.
    ///
    /// Default: 4
    pub message_editor_min_lines: Option<usize>,
    /// Whether to show turn statistics (elapsed time during generation, final turn duration).
    ///
    /// Default: false
    pub show_turn_stats: Option<bool>,
    /// Whether to show the merge conflict indicator in the status bar
    /// that offers to resolve conflicts using the agent.
    ///
    /// Default: true
    pub show_merge_conflict_indicator: Option<bool>,
    /// Per-tool permission rules for granular control over which tool actions
    /// require confirmation.
    ///
    /// The global `default` applies when no tool-specific rules match.
    /// For external agent servers (e.g. Claude Agent) that define their own
    /// permission modes, "deny" and "confirm" still take precedence — the
    /// external agent's permission system is only used when Zed would allow
    /// the action. Per-tool regex patterns (`always_allow`, `always_deny`,
    /// `always_confirm`) match against the tool's text input (command, path,
    /// URL, etc.).
    pub tool_permissions: Option<ToolPermissionsContent>,
}

impl AgentSettingsContent {
    pub fn set_dock(&mut self, dock: DockPosition) {
        self.dock = Some(dock);
    }

    pub fn set_sidebar_side(&mut self, position: SidebarDockPosition) {
        self.sidebar_side = Some(position);
    }

    pub fn set_flexible_size(&mut self, flexible: bool) {
        self.flexible = Some(flexible);
    }

    pub fn set_model(&mut self, language_model: LanguageModelSelection) {
        self.default_model = Some(language_model)
    }

    pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
        self.inline_assistant_model = Some(LanguageModelSelection {
            provider: provider.into(),
            model,
            enable_thinking: false,
            effort: None,
            speed: None,
        });
    }

    pub fn set_profile(&mut self, profile_id: Arc<str>) {
        self.default_profile = Some(profile_id);
    }

    pub fn add_favorite_model(&mut self, model: LanguageModelSelection) {
        // Note: this is intentional to not compare using `PartialEq`here.
        // Full equality would treat entries that differ just in thinking/effort/speed
        // as distinct and silently produce duplicates.
        if !self
            .favorite_models
            .iter()
            .any(|m| m.provider == model.provider && m.model == model.model)
        {
            self.favorite_models.push(model);
        }
    }

    pub fn remove_favorite_model(&mut self, model: &LanguageModelSelection) {
        self.favorite_models
            .retain(|m| !(m.provider == model.provider && m.model == model.model));
    }

    pub fn update_favorite_model<F>(&mut self, provider: &str, model: &str, f: F)
    where
        F: FnOnce(&mut LanguageModelSelection),
    {
        if let Some(entry) = self
            .favorite_models
            .iter_mut()
            .find(|m| m.provider.0 == provider && m.model == model)
        {
            f(entry);
        }
    }

    pub fn set_tool_default_permission(&mut self, tool_id: &str, mode: ToolPermissionMode) {
        let tool_permissions = self.tool_permissions.get_or_insert_default();
        let tool_rules = tool_permissions
            .tools
            .entry(Arc::from(tool_id))
            .or_default();
        tool_rules.default = Some(mode);
    }

    pub fn add_tool_allow_pattern(&mut self, tool_name: &str, pattern: String) {
        let tool_permissions = self.tool_permissions.get_or_insert_default();
        let tool_rules = tool_permissions
            .tools
            .entry(Arc::from(tool_name))
            .or_default();
        let always_allow = tool_rules.always_allow.get_or_insert_default();
        if !always_allow.0.iter().any(|r| r.pattern == pattern) {
            always_allow.0.push(ToolRegexRule {
                pattern,
                case_sensitive: None,
            });
        }
    }

    pub fn add_tool_deny_pattern(&mut self, tool_name: &str, pattern: String) {
        let tool_permissions = self.tool_permissions.get_or_insert_default();
        let tool_rules = tool_permissions
            .tools
            .entry(Arc::from(tool_name))
            .or_default();
        let always_deny = tool_rules.always_deny.get_or_insert_default();
        if !always_deny.0.iter().any(|r| r.pattern == pattern) {
            always_deny.0.push(ToolRegexRule {
                pattern,
                case_sensitive: None,
            });
        }
    }
}

#[with_fallible_options]
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema, MergeFrom)]
pub struct AgentProfileContent {
    pub name: Arc<str>,
    #[serde(default)]
    pub tools: IndexMap<Arc<str>, bool>,
    /// Whether all context servers are enabled by default.
    pub enable_all_context_servers: Option<bool>,
    #[serde(default)]
    pub context_servers: IndexMap<Arc<str>, ContextServerPresetContent>,
    /// The default language model selected when using this profile.
    pub default_model: Option<LanguageModelSelection>,
}

#[with_fallible_options]
#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
pub struct ContextServerPresetContent {
    pub tools: IndexMap<Arc<str>, bool>,
}

#[derive(
    Copy,
    Clone,
    Default,
    Debug,
    Serialize,
    Deserialize,
    JsonSchema,
    MergeFrom,
    PartialEq,
    strum::VariantArray,
    strum::VariantNames,
)]
#[serde(rename_all = "snake_case")]
pub enum NotifyWhenAgentWaiting {
    #[default]
    PrimaryScreen,
    AllScreens,
    Never,
}

#[derive(
    Copy,
    Clone,
    Default,
    Debug,
    Serialize,
    Deserialize,
    JsonSchema,
    MergeFrom,
    PartialEq,
    strum::VariantArray,
    strum::VariantNames,
)]
#[serde(rename_all = "snake_case")]
pub enum PlaySoundWhenAgentDone {
    #[default]
    Never,
    WhenHidden,
    Always,
}

impl PlaySoundWhenAgentDone {
    pub fn should_play(&self, visible: bool) -> bool {
        match self {
            PlaySoundWhenAgentDone::Never => false,
            PlaySoundWhenAgentDone::WhenHidden => !visible,
            PlaySoundWhenAgentDone::Always => true,
        }
    }
}

#[with_fallible_options]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
pub struct LanguageModelSelection {
    pub provider: LanguageModelProviderSetting,
    pub model: String,
    #[serde(default)]
    pub enable_thinking: bool,
    pub effort: Option<String>,
    pub speed: Option<language_model_core::Speed>,
}

#[with_fallible_options]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
pub struct LanguageModelParameters {
    pub provider: Option<LanguageModelProviderSetting>,
    pub model: Option<String>,
    #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
    pub temperature: Option<f32>,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, MergeFrom)]
pub struct LanguageModelProviderSetting(pub String);

impl JsonSchema for LanguageModelProviderSetting {
    fn schema_name() -> Cow<'static, str> {
        "LanguageModelProviderSetting".into()
    }

    fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
        // list the builtin providers as a subset so that we still auto complete them in the settings
        json_schema!({
            "anyOf": [
                {
                    "type": "string",
                    "enum": [
                        "amazon-bedrock",
                        "anthropic",
                        "copilot_chat",
                        "deepseek",
                        "google",
                        "lmstudio",
                        "mistral",
                        "ollama",
                        "openai",
                        "opencode",
                        "openrouter",
                        "vercel_ai_gateway",
                        "x_ai",
                        "zed.dev"
                    ]
                },
                {
                    "type": "string",
                }
            ]
        })
    }
}

impl From<String> for LanguageModelProviderSetting {
    fn from(provider: String) -> Self {
        Self(provider)
    }
}

impl From<&str> for LanguageModelProviderSetting {
    fn from(provider: &str) -> Self {
        Self(provider.to_string())
    }
}

#[with_fallible_options]
#[derive(Default, PartialEq, Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug)]
#[serde(transparent)]
pub struct AllAgentServersSettings(pub HashMap<String, CustomAgentServerSettings>);

impl std::ops::Deref for AllAgentServersSettings {
    type Target = HashMap<String, CustomAgentServerSettings>;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl std::ops::DerefMut for AllAgentServersSettings {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

#[with_fallible_options]
#[derive(Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum CustomAgentServerSettings {
    Custom {
        #[serde(rename = "command")]
        path: PathBuf,
        #[serde(default, skip_serializing_if = "Vec::is_empty")]
        args: Vec<String>,
        /// Default: {}
        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
        env: HashMap<String, String>,
        /// The default mode to use for this agent.
        ///
        /// Note: Not only all agents support modes.
        ///
        /// Default: None
        default_mode: Option<String>,
        /// The default model to use for this agent.
        ///
        /// This should be the model ID as reported by the agent.
        ///
        /// Default: None
        default_model: Option<String>,
        /// The favorite models for this agent.
        ///
        /// These are the model IDs as reported by the agent.
        ///
        /// Default: []
        #[serde(default, skip_serializing_if = "Vec::is_empty")]
        favorite_models: Vec<String>,
        /// Default values for session config options.
        ///
        /// This is a map from config option ID to value ID.
        ///
        /// Default: {}
        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
        default_config_options: HashMap<String, String>,
        /// Favorited values for session config options.
        ///
        /// This is a map from config option ID to a list of favorited value IDs.
        ///
        /// Default: {}
        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
        favorite_config_option_values: HashMap<String, Vec<String>>,
    },
    // Used for the ACP extension migration
    #[serde(alias = "extension")]
    Registry {
        /// Additional environment variables to pass to the agent.
        ///
        /// Default: {}
        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
        env: HashMap<String, String>,
        /// The default mode to use for this agent.
        ///
        /// Note: Not only all agents support modes.
        ///
        /// Default: None
        default_mode: Option<String>,
        /// The default model to use for this agent.
        ///
        /// This should be the model ID as reported by the agent.
        ///
        /// Default: None
        default_model: Option<String>,
        /// The favorite models for this agent.
        ///
        /// These are the model IDs as reported by the agent.
        ///
        /// Default: []
        #[serde(default, skip_serializing_if = "Vec::is_empty")]
        favorite_models: Vec<String>,
        /// Default values for session config options.
        ///
        /// This is a map from config option ID to value ID.
        ///
        /// Default: {}
        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
        default_config_options: HashMap<String, String>,
        /// Favorited values for session config options.
        ///
        /// This is a map from config option ID to a list of favorited value IDs.
        ///
        /// Default: {}
        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
        favorite_config_option_values: HashMap<String, Vec<String>>,
    },
}

#[with_fallible_options]
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
pub struct ToolPermissionsContent {
    /// Global default permission when no tool-specific rules match.
    /// Individual tools can override this with their own default.
    /// Default: confirm
    #[serde(alias = "default_mode")]
    pub default: Option<ToolPermissionMode>,

    /// Per-tool permission rules.
    /// Keys are tool names (e.g. terminal, edit_file, fetch) including MCP
    /// tools (e.g. mcp:server_name:tool_name). Any tool name is accepted;
    /// even tools without meaningful text input can have a `default` set.
    #[serde(default)]
    pub tools: HashMap<Arc<str>, ToolRulesContent>,
}

#[with_fallible_options]
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
pub struct ToolRulesContent {
    /// Default mode when no regex rules match.
    /// When unset, inherits from the global `tool_permissions.default`.
    #[serde(alias = "default_mode")]
    pub default: Option<ToolPermissionMode>,

    /// Regexes for inputs to auto-approve.
    /// For terminal: matches command. For file tools: matches path. For fetch: matches URL.
    /// For `copy_path` and `move_path`, patterns are matched independently against each
    /// path (source and destination).
    /// Patterns accumulate across settings layers (user, project, profile) and cannot be
    /// removed by a higher-priority layer—only new patterns can be added.
    /// Default: []
    pub always_allow: Option<ExtendingVec<ToolRegexRule>>,

    /// Regexes for inputs to auto-reject.
    /// **SECURITY**: These take precedence over ALL other rules, across ALL settings layers.
    /// For `copy_path` and `move_path`, patterns are matched independently against each
    /// path (source and destination).
    /// Patterns accumulate across settings layers (user, project, profile) and cannot be
    /// removed by a higher-priority layer—only new patterns can be added.
    /// Default: []
    pub always_deny: Option<ExtendingVec<ToolRegexRule>>,

    /// Regexes for inputs that must always prompt.
    /// Takes precedence over always_allow but not always_deny.
    /// For `copy_path` and `move_path`, patterns are matched independently against each
    /// path (source and destination).
    /// Patterns accumulate across settings layers (user, project, profile) and cannot be
    /// removed by a higher-priority layer—only new patterns can be added.
    /// Default: []
    pub always_confirm: Option<ExtendingVec<ToolRegexRule>>,
}

#[with_fallible_options]
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
pub struct ToolRegexRule {
    /// The regex pattern to match.
    #[serde(default)]
    pub pattern: String,

    /// Whether the regex is case-sensitive.
    /// Default: false (case-insensitive)
    pub case_sensitive: Option<bool>,
}

#[derive(
    Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom,
)]
#[serde(rename_all = "snake_case")]
pub enum ToolPermissionMode {
    /// Auto-approve without prompting.
    Allow,
    /// Auto-reject with an error.
    Deny,
    /// Always prompt for confirmation (default behavior).
    #[default]
    Confirm,
}

impl std::fmt::Display for ToolPermissionMode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ToolPermissionMode::Allow => write!(f, "Allow"),
            ToolPermissionMode::Deny => write!(f, "Deny"),
            ToolPermissionMode::Confirm => write!(f, "Confirm"),
        }
    }
}

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

    #[test]
    fn test_set_tool_default_permission_creates_structure() {
        let mut settings = AgentSettingsContent::default();
        assert!(settings.tool_permissions.is_none());

        settings.set_tool_default_permission("terminal", ToolPermissionMode::Allow);

        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
        assert_eq!(terminal_rules.default, Some(ToolPermissionMode::Allow));
    }

    #[test]
    fn test_set_tool_default_permission_updates_existing() {
        let mut settings = AgentSettingsContent::default();

        settings.set_tool_default_permission("terminal", ToolPermissionMode::Confirm);
        settings.set_tool_default_permission("terminal", ToolPermissionMode::Allow);

        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
        assert_eq!(terminal_rules.default, Some(ToolPermissionMode::Allow));
    }

    #[test]
    fn test_set_tool_default_permission_for_mcp_tool() {
        let mut settings = AgentSettingsContent::default();

        settings.set_tool_default_permission("mcp:github:create_issue", ToolPermissionMode::Allow);

        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
        let mcp_rules = tool_permissions
            .tools
            .get("mcp:github:create_issue")
            .unwrap();
        assert_eq!(mcp_rules.default, Some(ToolPermissionMode::Allow));
    }

    #[test]
    fn test_add_tool_allow_pattern_creates_structure() {
        let mut settings = AgentSettingsContent::default();
        assert!(settings.tool_permissions.is_none());

        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());

        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
        let always_allow = terminal_rules.always_allow.as_ref().unwrap();
        assert_eq!(always_allow.0.len(), 1);
        assert_eq!(always_allow.0[0].pattern, "^cargo\\s");
    }

    #[test]
    fn test_add_tool_allow_pattern_appends_to_existing() {
        let mut settings = AgentSettingsContent::default();

        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
        settings.add_tool_allow_pattern("terminal", "^npm\\s".to_string());

        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
        let always_allow = terminal_rules.always_allow.as_ref().unwrap();
        assert_eq!(always_allow.0.len(), 2);
        assert_eq!(always_allow.0[0].pattern, "^cargo\\s");
        assert_eq!(always_allow.0[1].pattern, "^npm\\s");
    }

    #[test]
    fn test_add_tool_allow_pattern_does_not_duplicate() {
        let mut settings = AgentSettingsContent::default();

        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());

        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
        let always_allow = terminal_rules.always_allow.as_ref().unwrap();
        assert_eq!(
            always_allow.0.len(),
            1,
            "Duplicate patterns should not be added"
        );
    }

    #[test]
    fn test_add_tool_allow_pattern_for_different_tools() {
        let mut settings = AgentSettingsContent::default();

        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
        settings.add_tool_allow_pattern("fetch", "^https?://github\\.com".to_string());

        let tool_permissions = settings.tool_permissions.as_ref().unwrap();

        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
        assert_eq!(
            terminal_rules.always_allow.as_ref().unwrap().0[0].pattern,
            "^cargo\\s"
        );

        let fetch_rules = tool_permissions.tools.get("fetch").unwrap();
        assert_eq!(
            fetch_rules.always_allow.as_ref().unwrap().0[0].pattern,
            "^https?://github\\.com"
        );
    }

    #[test]
    fn test_add_tool_deny_pattern_creates_structure() {
        let mut settings = AgentSettingsContent::default();
        assert!(settings.tool_permissions.is_none());

        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());

        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
        let always_deny = terminal_rules.always_deny.as_ref().unwrap();
        assert_eq!(always_deny.0.len(), 1);
        assert_eq!(always_deny.0[0].pattern, "^rm\\s");
    }

    #[test]
    fn test_add_tool_deny_pattern_appends_to_existing() {
        let mut settings = AgentSettingsContent::default();

        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
        settings.add_tool_deny_pattern("terminal", "^sudo\\s".to_string());

        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
        let always_deny = terminal_rules.always_deny.as_ref().unwrap();
        assert_eq!(always_deny.0.len(), 2);
        assert_eq!(always_deny.0[0].pattern, "^rm\\s");
        assert_eq!(always_deny.0[1].pattern, "^sudo\\s");
    }

    #[test]
    fn test_add_tool_deny_pattern_does_not_duplicate() {
        let mut settings = AgentSettingsContent::default();

        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());

        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
        let always_deny = terminal_rules.always_deny.as_ref().unwrap();
        assert_eq!(
            always_deny.0.len(),
            1,
            "Duplicate patterns should not be added"
        );
    }

    #[test]
    fn test_add_tool_deny_and_allow_patterns_separate() {
        let mut settings = AgentSettingsContent::default();

        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());

        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();

        let always_allow = terminal_rules.always_allow.as_ref().unwrap();
        assert_eq!(always_allow.0.len(), 1);
        assert_eq!(always_allow.0[0].pattern, "^cargo\\s");

        let always_deny = terminal_rules.always_deny.as_ref().unwrap();
        assert_eq!(always_deny.0.len(), 1);
        assert_eq!(always_deny.0[0].pattern, "^rm\\s");
    }
}
