//! Common helper functions for framework adapters.
//!
//! This module provides shared logic used by multiple framework adapters,
//! reducing code duplication across Claude, Gemini, and Codex adapters.

use super::ParsedHookInput;
use std::path::PathBuf;

// ============================================================================
// ParsedHookInput Builder
// ============================================================================

/// Builder for extracting fields from JSON into [`ParsedHookInput`].
///
/// This builder provides a fluent API for extracting fields from JSON hook payloads,
/// supporting both simple top-level fields and nested paths using dot notation.
///
/// # Example
///
/// ```
/// use mi6_core::framework::common::ParsedHookInputBuilder;
/// use serde_json::json;
///
/// let json = json!({
///     "session_id": "abc123",
///     "tool_input": {
///         "subagent_type": "Explore"
///     }
/// });
///
/// let parsed = ParsedHookInputBuilder::new(&json)
///     .session_id("session_id")
///     .subagent_type("tool_input.subagent_type")
///     .build();
///
/// assert_eq!(parsed.session_id, Some("abc123".to_string()));
/// assert_eq!(parsed.subagent_type, Some("Explore".to_string()));
/// ```
pub struct ParsedHookInputBuilder<'a> {
    json: &'a serde_json::Value,
    result: ParsedHookInput,
}

impl<'a> ParsedHookInputBuilder<'a> {
    /// Create a new builder for extracting fields from the given JSON value.
    pub fn new(json: &'a serde_json::Value) -> Self {
        Self {
            json,
            result: ParsedHookInput::default(),
        }
    }

    /// Extract a string value from a JSON path.
    ///
    /// Supports dot notation for nested paths: "tool_input.subagent_type"
    fn get_str(&self, path: &str) -> Option<String> {
        get_json_path(self.json, path).and_then(|v| v.as_str().map(String::from))
    }

    /// Extract an i64 value from a JSON path.
    fn get_i64(&self, path: &str) -> Option<i64> {
        get_json_path(self.json, path).and_then(|v| v.as_i64())
    }

    /// Extract an f64 value from a JSON path.
    fn get_f64(&self, path: &str) -> Option<f64> {
        get_json_path(self.json, path).and_then(|v| v.as_f64())
    }

    /// Set session_id from the given JSON path.
    pub fn session_id(mut self, path: &str) -> Self {
        self.result.session_id = self.get_str(path);
        self
    }

    /// Set session_id with fallback paths. Uses the first path that yields a value.
    pub fn session_id_or(mut self, paths: &[&str]) -> Self {
        for path in paths {
            if let Some(val) = self.get_str(path) {
                self.result.session_id = Some(val);
                return self;
            }
        }
        self
    }

    /// Set tool_use_id from the given JSON path.
    pub fn tool_use_id(mut self, path: &str) -> Self {
        self.result.tool_use_id = self.get_str(path);
        self
    }

    /// Set tool_use_id with fallback paths. Uses the first path that yields a value.
    pub fn tool_use_id_or(mut self, paths: &[&str]) -> Self {
        for path in paths {
            if let Some(val) = self.get_str(path) {
                self.result.tool_use_id = Some(val);
                return self;
            }
        }
        self
    }

    /// Set tool_name from the given JSON path.
    pub fn tool_name(mut self, path: &str) -> Self {
        self.result.tool_name = self.get_str(path);
        self
    }

    /// Set tool_name with fallback paths. Uses the first path that yields a value.
    pub fn tool_name_or(mut self, paths: &[&str]) -> Self {
        for path in paths {
            if let Some(val) = self.get_str(path) {
                self.result.tool_name = Some(val);
                return self;
            }
        }
        self
    }

    /// Set subagent_type from the given JSON path.
    pub fn subagent_type(mut self, path: &str) -> Self {
        self.result.subagent_type = self.get_str(path);
        self
    }

    /// Set spawned_agent_id from the given JSON path.
    pub fn spawned_agent_id(mut self, path: &str) -> Self {
        self.result.spawned_agent_id = self.get_str(path);
        self
    }

    /// Set permission_mode from the given JSON path.
    pub fn permission_mode(mut self, path: &str) -> Self {
        self.result.permission_mode = self.get_str(path);
        self
    }

    /// Set transcript_path from the given JSON path.
    pub fn transcript_path(mut self, path: &str) -> Self {
        self.result.transcript_path = self.get_str(path);
        self
    }

    /// Set cwd from the given JSON path.
    pub fn cwd(mut self, path: &str) -> Self {
        self.result.cwd = self.get_str(path);
        self
    }

    /// Set cwd with fallback paths. Uses the first path that yields a value.
    pub fn cwd_or(mut self, paths: &[&str]) -> Self {
        for path in paths {
            if let Some(val) = self.get_str(path) {
                self.result.cwd = Some(val);
                return self;
            }
        }
        self
    }

    /// Set session_source from the given JSON path.
    pub fn session_source(mut self, path: &str) -> Self {
        self.result.session_source = self.get_str(path);
        self
    }

    /// Set agent_id from the given JSON path.
    pub fn agent_id(mut self, path: &str) -> Self {
        self.result.agent_id = self.get_str(path);
        self
    }

    /// Set agent_transcript_path from the given JSON path.
    pub fn agent_transcript_path(mut self, path: &str) -> Self {
        self.result.agent_transcript_path = self.get_str(path);
        self
    }

    /// Set compact_trigger from the given JSON path.
    pub fn compact_trigger(mut self, path: &str) -> Self {
        self.result.compact_trigger = self.get_str(path);
        self
    }

    /// Set model from the given JSON path.
    pub fn model(mut self, path: &str) -> Self {
        self.result.model = self.get_str(path);
        self
    }

    /// Set model with fallback paths. Uses the first path that yields a value.
    pub fn model_or(mut self, paths: &[&str]) -> Self {
        for path in paths {
            if let Some(val) = self.get_str(path) {
                self.result.model = Some(val);
                return self;
            }
        }
        self
    }

    /// Set duration_ms from the given JSON path.
    pub fn duration_ms(mut self, path: &str) -> Self {
        self.result.duration_ms = self.get_i64(path);
        self
    }

    /// Set duration_ms with fallback paths. Uses the first path that yields a value.
    pub fn duration_ms_or(mut self, paths: &[&str]) -> Self {
        for path in paths {
            if let Some(val) = self.get_i64(path) {
                self.result.duration_ms = Some(val);
                return self;
            }
        }
        self
    }

    /// Set tokens_input from the given JSON path.
    pub fn tokens_input(mut self, path: &str) -> Self {
        self.result.tokens_input = self.get_i64(path);
        self
    }

    /// Set tokens_output from the given JSON path.
    pub fn tokens_output(mut self, path: &str) -> Self {
        self.result.tokens_output = self.get_i64(path);
        self
    }

    /// Set tokens_cache_read from the given JSON path.
    pub fn tokens_cache_read(mut self, path: &str) -> Self {
        self.result.tokens_cache_read = self.get_i64(path);
        self
    }

    /// Set tokens_cache_write from the given JSON path.
    pub fn tokens_cache_write(mut self, path: &str) -> Self {
        self.result.tokens_cache_write = self.get_i64(path);
        self
    }

    /// Set cost_usd from the given JSON path.
    pub fn cost_usd(mut self, path: &str) -> Self {
        self.result.cost_usd = self.get_f64(path);
        self
    }

    /// Set prompt from the given JSON path.
    pub fn prompt(mut self, path: &str) -> Self {
        self.result.prompt = self.get_str(path);
        self
    }

    /// Consume the builder and return the constructed [`ParsedHookInput`].
    pub fn build(self) -> ParsedHookInput {
        self.result
    }
}

/// Navigate a JSON value using dot-notation path.
///
/// # Example
///
/// ```ignore
/// let json = json!({"a": {"b": {"c": "value"}}});
/// assert_eq!(get_json_path(&json, "a.b.c"), Some(&json!("value")));
/// ```
fn get_json_path<'a>(json: &'a serde_json::Value, path: &str) -> Option<&'a serde_json::Value> {
    let mut current = json;
    for part in path.split('.') {
        current = current.get(part)?;
    }
    Some(current)
}

/// Extract the first array element from a JSON path as a string.
///
/// Useful for fields like Cursor's `workspace_roots` where we want the first entry.
pub fn get_first_array_element(json: &serde_json::Value, path: &str) -> Option<String> {
    get_json_path(json, path)
        .and_then(|v| v.as_array())
        .and_then(|arr| arr.first())
        .and_then(|v| v.as_str())
        .map(String::from)
}

// ============================================================================
// Existing helper functions
// ============================================================================

/// Check if a framework is installed on the system.
///
/// A framework is considered installed if either:
/// 1. Its config directory exists (e.g., `~/.claude`, `~/.gemini`)
/// 2. Its CLI tool is available in PATH
///
/// # Arguments
/// * `config_path` - Optional path to the framework's config file
/// * `cli_name` - Name of the CLI executable (e.g., "claude", "gemini")
pub fn is_framework_installed(config_path: Option<PathBuf>, cli_name: &str) -> bool {
    // Check if config directory exists
    if let Some(path) = config_path
        && let Some(parent) = path.parent()
        && parent.exists()
    {
        return true;
    }
    // Check if CLI is in PATH
    which::which(cli_name).is_ok()
}

/// Check if a command string is an mi6 hook command.
///
/// Uses specific prefix matching to avoid false positives with user commands
/// that might coincidentally contain "mi6" in their path or arguments.
pub fn is_mi6_command(cmd: &str) -> bool {
    let cmd = cmd.trim();
    // Match commands that start with "mi6 ingest" or have a path ending in "/mi6 ingest"
    cmd.starts_with("mi6 ingest") || cmd.contains("/mi6 ingest")
}

/// Check if a hook entry contains an mi6 command (nested hooks structure).
///
/// This handles the hook format used by Claude and Gemini:
/// ```json
/// { "matcher": "", "hooks": [{ "command": "mi6 ingest event ..." }] }
/// ```
fn is_mi6_hook_nested(entry: &serde_json::Value) -> bool {
    entry
        .get("hooks")
        .and_then(|h| h.as_array())
        .is_some_and(|hooks| {
            hooks.iter().any(|hook| {
                hook.get("command")
                    .and_then(|c| c.as_str())
                    .is_some_and(is_mi6_command)
            })
        })
}

/// Merge generated hooks into existing JSON settings, preserving user hooks.
///
/// This function handles the common pattern of merging hook configurations:
/// - For each event type, appends mi6 hooks to existing user hooks
/// - If an mi6 hook already exists for an event, it is replaced (not duplicated)
/// - User hooks (non-mi6) are never removed or modified
/// - Preserves all other settings
///
/// # Hook Structure
///
/// This function handles the nested hooks format used by Claude and Gemini:
/// ```json
/// {
///   "hooks": {
///     "BeforeTool": [
///       { "matcher": "", "hooks": [{ "command": "user-tool" }] },
///       { "matcher": "", "hooks": [{ "command": "mi6 ingest event BeforeTool" }] }
///     ]
///   }
/// }
/// ```
///
/// # Arguments
/// * `generated` - The generated hooks configuration (must have "hooks" key)
/// * `existing` - Optional existing settings content
///
/// # Returns
/// The merged settings JSON with user hooks preserved
pub fn merge_json_hooks(
    generated: serde_json::Value,
    existing: Option<serde_json::Value>,
) -> serde_json::Value {
    let mut settings = existing.unwrap_or_else(|| serde_json::json!({}));

    let Some(new_hooks) = generated.get("hooks").and_then(|h| h.as_object()) else {
        return settings;
    };

    // Ensure hooks object exists
    if settings.get("hooks").is_none() {
        settings["hooks"] = serde_json::json!({});
    }

    let Some(existing_hooks) = settings.get_mut("hooks").and_then(|h| h.as_object_mut()) else {
        return settings;
    };

    for (event_type, new_hook_array) in new_hooks {
        let Some(new_hooks_arr) = new_hook_array.as_array() else {
            continue;
        };

        if let Some(existing_event_hooks) = existing_hooks.get_mut(event_type) {
            if let Some(existing_arr) = existing_event_hooks.as_array_mut() {
                // Remove any existing mi6 hooks to avoid duplicates
                existing_arr.retain(|entry| !is_mi6_hook_nested(entry));

                // Append the new mi6 hooks
                for hook in new_hooks_arr {
                    existing_arr.push(hook.clone());
                }
            }
        } else {
            // Event type doesn't exist, add the entire array
            existing_hooks.insert(event_type.clone(), new_hook_array.clone());
        }
    }

    settings
}

/// Remove mi6 hooks from JSON settings.
///
/// This function handles the common pattern of removing mi6-related hooks:
/// - Iterates over all event types in the hooks object
/// - Filters out entries that contain "mi6" in their command
/// - Removes empty arrays and objects
/// - Optionally removes OTel environment variables
///
/// # Arguments
/// * `existing` - The existing settings content
/// * `otel_keys` - List of OTel-related environment variable names to remove
///
/// # Returns
/// Some(modified settings) if any mi6 hooks were removed, None otherwise
pub fn remove_json_hooks(
    existing: serde_json::Value,
    otel_keys: &[&str],
) -> Option<serde_json::Value> {
    let mut settings = existing;
    let mut modified = false;

    // Remove only mi6 hooks from settings, preserving user's custom hooks
    if let Some(hooks) = settings.get_mut("hooks")
        && let Some(hooks_obj) = hooks.as_object_mut()
    {
        // Process each event type
        let keys: Vec<String> = hooks_obj.keys().cloned().collect();
        for key in keys {
            if let Some(event_hooks) = hooks_obj.get_mut(&key)
                && let Some(arr) = event_hooks.as_array_mut()
            {
                // Filter out entries that contain mi6 hooks
                let original_len = arr.len();
                arr.retain(|entry| !is_mi6_hook_nested(entry));

                if arr.len() != original_len {
                    modified = true;
                }

                // Remove the event key entirely if array is now empty
                if arr.is_empty() {
                    hooks_obj.remove(&key);
                }
            }
        }

        // If hooks object is now empty, remove it entirely
        if hooks_obj.is_empty()
            && let Some(obj) = settings.as_object_mut()
        {
            obj.remove("hooks");
        }
    }

    // Remove OTel env vars
    if let Some(env) = settings.get_mut("env")
        && let Some(env_obj) = env.as_object_mut()
    {
        for key in otel_keys {
            if env_obj.remove(*key).is_some() {
                modified = true;
            }
        }

        // If env object is now empty, remove it entirely
        if env_obj.is_empty()
            && let Some(obj) = settings.as_object_mut()
        {
            obj.remove("env");
        }
    }

    if modified { Some(settings) } else { None }
}

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

    // ========================================================================
    // ParsedHookInputBuilder tests
    // ========================================================================

    #[test]
    fn test_builder_simple_fields() {
        let json = json!({
            "session_id": "test-session",
            "tool_use_id": "tool-123",
            "tool_name": "Bash",
            "cwd": "/projects/test"
        });

        let parsed = ParsedHookInputBuilder::new(&json)
            .session_id("session_id")
            .tool_use_id("tool_use_id")
            .tool_name("tool_name")
            .cwd("cwd")
            .build();

        assert_eq!(parsed.session_id, Some("test-session".to_string()));
        assert_eq!(parsed.tool_use_id, Some("tool-123".to_string()));
        assert_eq!(parsed.tool_name, Some("Bash".to_string()));
        assert_eq!(parsed.cwd, Some("/projects/test".to_string()));
    }

    #[test]
    fn test_builder_nested_paths() {
        let json = json!({
            "tool_input": {
                "subagent_type": "Explore"
            },
            "tool_response": {
                "agentId": "agent-456"
            },
            "llm_request": {
                "model": "claude-3-opus"
            }
        });

        let parsed = ParsedHookInputBuilder::new(&json)
            .subagent_type("tool_input.subagent_type")
            .spawned_agent_id("tool_response.agentId")
            .model("llm_request.model")
            .build();

        assert_eq!(parsed.subagent_type, Some("Explore".to_string()));
        assert_eq!(parsed.spawned_agent_id, Some("agent-456".to_string()));
        assert_eq!(parsed.model, Some("claude-3-opus".to_string()));
    }

    #[test]
    fn test_builder_numeric_fields() {
        let json = json!({
            "duration_ms": 1234,
            "tokens_input": 100,
            "tokens_output": 50,
            "cost_usd": 0.005
        });

        let parsed = ParsedHookInputBuilder::new(&json)
            .duration_ms("duration_ms")
            .tokens_input("tokens_input")
            .tokens_output("tokens_output")
            .cost_usd("cost_usd")
            .build();

        assert_eq!(parsed.duration_ms, Some(1234));
        assert_eq!(parsed.tokens_input, Some(100));
        assert_eq!(parsed.tokens_output, Some(50));
        assert_eq!(parsed.cost_usd, Some(0.005));
    }

    #[test]
    fn test_builder_missing_fields() {
        let json = json!({
            "session_id": "test"
        });

        let parsed = ParsedHookInputBuilder::new(&json)
            .session_id("session_id")
            .tool_name("nonexistent")
            .cwd("also_missing")
            .build();

        assert_eq!(parsed.session_id, Some("test".to_string()));
        assert_eq!(parsed.tool_name, None);
        assert_eq!(parsed.cwd, None);
    }

    #[test]
    fn test_builder_fallback_paths() {
        // cwd present - should use it
        let json_with_cwd = json!({
            "cwd": "/primary",
            "workspace_roots": ["/fallback"]
        });

        let parsed = ParsedHookInputBuilder::new(&json_with_cwd)
            .cwd_or(&["cwd", "workspace_roots.0"])
            .build();

        assert_eq!(parsed.cwd, Some("/primary".to_string()));

        // cwd missing - should use fallback
        let json_no_cwd = json!({
            "other_field": "value"
        });

        let parsed = ParsedHookInputBuilder::new(&json_no_cwd)
            .cwd_or(&["cwd", "workspace_dir"])
            .build();

        assert_eq!(parsed.cwd, None);
    }

    #[test]
    fn test_builder_duration_fallback() {
        // duration_ms present
        let json_ms = json!({
            "duration_ms": 5000
        });

        let parsed = ParsedHookInputBuilder::new(&json_ms)
            .duration_ms_or(&["duration_ms", "duration"])
            .build();

        assert_eq!(parsed.duration_ms, Some(5000));

        // Only duration present (fallback)
        let json_fallback = json!({
            "duration": 3000
        });

        let parsed = ParsedHookInputBuilder::new(&json_fallback)
            .duration_ms_or(&["duration_ms", "duration"])
            .build();

        assert_eq!(parsed.duration_ms, Some(3000));
    }

    #[test]
    fn test_builder_deeply_nested_path() {
        let json = json!({
            "a": {
                "b": {
                    "c": {
                        "value": "deep"
                    }
                }
            }
        });

        let parsed = ParsedHookInputBuilder::new(&json)
            .session_id("a.b.c.value")
            .build();

        assert_eq!(parsed.session_id, Some("deep".to_string()));
    }

    #[test]
    fn test_get_first_array_element() {
        let json = json!({
            "workspace_roots": ["/first", "/second", "/third"]
        });

        let result = get_first_array_element(&json, "workspace_roots");
        assert_eq!(result, Some("/first".to_string()));

        // Empty array
        let json_empty = json!({
            "workspace_roots": []
        });

        let result = get_first_array_element(&json_empty, "workspace_roots");
        assert_eq!(result, None);

        // Missing field
        let result = get_first_array_element(&json, "missing");
        assert_eq!(result, None);
    }

    // ========================================================================
    // Existing tests
    // ========================================================================

    #[test]
    fn test_is_framework_installed_cli_in_path() {
        // "ls" should be in PATH on any Unix system
        assert!(is_framework_installed(None, "ls"));
    }

    #[test]
    fn test_is_framework_installed_config_dir_exists() {
        // Create a temp directory to simulate a config directory
        let temp_dir = std::env::temp_dir().join("mi6_test_config_dir");
        std::fs::create_dir_all(&temp_dir).unwrap();
        let config_path = temp_dir.join("settings.json");

        // Config dir exists, CLI doesn't matter
        assert!(is_framework_installed(
            Some(config_path.clone()),
            "nonexistent_cli_xyz_123"
        ));

        // Cleanup
        std::fs::remove_dir_all(&temp_dir).ok();
    }

    #[test]
    fn test_is_framework_installed_nothing() {
        // Non-existent CLI should return false
        assert!(!is_framework_installed(None, "nonexistent_cli_xyz_123"));
    }

    #[test]
    fn test_is_mi6_command() {
        // Should match mi6 ingest commands
        assert!(is_mi6_command("mi6 ingest event SessionStart"));
        assert!(is_mi6_command(
            "mi6 ingest event BeforeTool --framework gemini"
        ));
        assert!(is_mi6_command("/usr/local/bin/mi6 ingest event PreToolUse"));
        assert!(is_mi6_command("  mi6 ingest event Test  ")); // with whitespace

        // Should NOT match other commands that happen to contain "mi6"
        assert!(!is_mi6_command("my-logger --output /var/log/mi6.log"));
        assert!(!is_mi6_command("echo mi6 is cool"));
        assert!(!is_mi6_command("mi6-wrapper some-command")); // different executable
        assert!(!is_mi6_command("other-tool"));
    }

    #[test]
    fn test_merge_json_hooks_new() {
        let generated = json!({
            "hooks": {
                "SessionStart": [{"matcher": "", "hooks": []}]
            }
        });

        let merged = merge_json_hooks(generated, None);

        assert!(merged.get("hooks").is_some());
        assert!(merged["hooks"].get("SessionStart").is_some());
    }

    #[test]
    fn test_merge_json_hooks_existing() {
        let generated = json!({
            "hooks": {
                "PreToolUse": [{"matcher": "", "hooks": [{"command": "mi6 ingest event PreToolUse"}]}]
            }
        });
        let existing = json!({
            "theme": "dark",
            "hooks": {
                "SessionStart": [{"matcher": "", "hooks": [{"command": "other-tool"}]}]
            }
        });

        let merged = merge_json_hooks(generated, Some(existing));

        // Should preserve existing settings
        assert_eq!(merged["theme"], "dark");
        // Should merge hooks - both should exist
        assert!(merged["hooks"].get("SessionStart").is_some());
        assert!(merged["hooks"].get("PreToolUse").is_some());
    }

    #[test]
    fn test_merge_json_hooks_preserves_user_hooks_for_same_event() {
        // This is the key test case from issue #440:
        // User has custom hooks for an event, and mi6 enable should append
        // its hook instead of replacing the user's hook.
        let generated = json!({
            "hooks": {
                "BeforeTool": [{"matcher": "*", "hooks": [{"command": "mi6 ingest event BeforeTool"}]}]
            }
        });
        let existing = json!({
            "hooks": {
                "BeforeTool": [{"matcher": "", "hooks": [{"command": "my-custom-logger --event tool"}]}]
            }
        });

        let merged = merge_json_hooks(generated, Some(existing));

        // Should have both hooks for BeforeTool
        let before_tool_hooks = merged["hooks"]["BeforeTool"].as_array().unwrap();
        assert_eq!(
            before_tool_hooks.len(),
            2,
            "Should have 2 hooks for BeforeTool"
        );

        // First hook should be the user's custom hook (preserved)
        assert!(
            before_tool_hooks[0]["hooks"][0]["command"]
                .as_str()
                .unwrap()
                .contains("my-custom-logger"),
            "User's custom hook should be preserved"
        );

        // Second hook should be mi6's hook (appended)
        assert!(
            before_tool_hooks[1]["hooks"][0]["command"]
                .as_str()
                .unwrap()
                .contains("mi6 ingest"),
            "mi6 hook should be appended"
        );
    }

    #[test]
    fn test_merge_json_hooks_updates_existing_mi6_hook() {
        // When mi6 is re-enabled, it should replace the old mi6 hook
        // instead of creating duplicates
        let generated = json!({
            "hooks": {
                "BeforeTool": [{"matcher": "*", "hooks": [{"command": "mi6 ingest event BeforeTool --new-flag"}]}]
            }
        });
        let existing = json!({
            "hooks": {
                "BeforeTool": [
                    {"matcher": "", "hooks": [{"command": "my-custom-logger"}]},
                    {"matcher": "", "hooks": [{"command": "mi6 ingest event BeforeTool --old-flag"}]}
                ]
            }
        });

        let merged = merge_json_hooks(generated, Some(existing));

        let before_tool_hooks = merged["hooks"]["BeforeTool"].as_array().unwrap();
        assert_eq!(before_tool_hooks.len(), 2, "Should still have 2 hooks");

        // First hook should be the user's custom hook
        assert!(
            before_tool_hooks[0]["hooks"][0]["command"]
                .as_str()
                .unwrap()
                .contains("my-custom-logger"),
            "User's custom hook should be preserved"
        );

        // Second hook should be the NEW mi6 hook (not the old one)
        let mi6_command = before_tool_hooks[1]["hooks"][0]["command"]
            .as_str()
            .unwrap();
        assert!(
            mi6_command.contains("--new-flag"),
            "New mi6 hook should be present"
        );
        assert!(
            !mi6_command.contains("--old-flag"),
            "Old mi6 hook should be replaced"
        );
    }

    #[test]
    fn test_merge_json_hooks_preserves_multiple_user_hooks() {
        // User might have multiple custom hooks for the same event
        let generated = json!({
            "hooks": {
                "SessionStart": [{"matcher": "*", "hooks": [{"command": "mi6 ingest event SessionStart"}]}]
            }
        });
        let existing = json!({
            "hooks": {
                "SessionStart": [
                    {"matcher": "", "hooks": [{"command": "custom-logger-1"}]},
                    {"matcher": "", "hooks": [{"command": "custom-logger-2"}]},
                    {"matcher": "", "hooks": [{"command": "custom-logger-3"}]}
                ]
            }
        });

        let merged = merge_json_hooks(generated, Some(existing));

        let session_hooks = merged["hooks"]["SessionStart"].as_array().unwrap();
        assert_eq!(
            session_hooks.len(),
            4,
            "Should have all 3 user hooks + 1 mi6 hook"
        );

        // Verify all custom loggers are preserved
        let commands: Vec<&str> = session_hooks
            .iter()
            .filter_map(|h| h["hooks"][0]["command"].as_str())
            .collect();
        assert!(commands.iter().any(|c| c.contains("custom-logger-1")));
        assert!(commands.iter().any(|c| c.contains("custom-logger-2")));
        assert!(commands.iter().any(|c| c.contains("custom-logger-3")));
        assert!(commands.iter().any(|c| c.contains("mi6 ingest")));
    }

    #[test]
    fn test_remove_json_hooks_with_mi6() {
        let existing = json!({
            "theme": "dark",
            "hooks": {
                "SessionStart": [{
                    "matcher": "",
                    "hooks": [{"type": "command", "command": "mi6 ingest event SessionStart"}]
                }]
            },
            "env": {
                "OTEL_LOGS_EXPORTER": "otlp",
                "MY_VAR": "value"
            }
        });

        let otel_keys = &["OTEL_LOGS_EXPORTER"];
        let result = remove_json_hooks(existing, otel_keys);

        assert!(result.is_some());
        let settings = result.unwrap();
        // Theme should be preserved
        assert_eq!(settings["theme"], "dark");
        // Hooks should be removed (was only mi6 hooks)
        assert!(settings.get("hooks").is_none());
        // Only OTEL key removed, MY_VAR preserved
        assert!(settings["env"].get("OTEL_LOGS_EXPORTER").is_none());
        assert_eq!(settings["env"]["MY_VAR"], "value");
    }

    #[test]
    fn test_remove_json_hooks_preserves_non_mi6() {
        let existing = json!({
            "hooks": {
                "SessionStart": [
                    {
                        "matcher": "",
                        "hooks": [{"type": "command", "command": "mi6 ingest event SessionStart"}]
                    },
                    {
                        "matcher": "",
                        "hooks": [{"type": "command", "command": "other-tool log"}]
                    }
                ]
            }
        });

        let result = remove_json_hooks(existing, &[]);

        assert!(result.is_some());
        let settings = result.unwrap();
        // Hooks object should still exist with the non-mi6 hook
        assert!(settings.get("hooks").is_some());
        let session_hooks = settings["hooks"]["SessionStart"].as_array().unwrap();
        assert_eq!(session_hooks.len(), 1);
        assert!(
            session_hooks[0]["hooks"][0]["command"]
                .as_str()
                .unwrap()
                .contains("other-tool")
        );
    }

    #[test]
    fn test_remove_json_hooks_no_mi6() {
        let existing = json!({
            "hooks": {
                "SessionStart": [{
                    "matcher": "",
                    "hooks": [{"type": "command", "command": "other-tool log"}]
                }]
            }
        });

        let result = remove_json_hooks(existing, &[]);

        // Should return None since no mi6 hooks were found
        assert!(result.is_none());
    }
}
