use clap::{Args, Subcommand};

use cfgd_core::config::{self, AiConfig};
use cfgd_core::generate::{PresentYamlRequest, PresentYamlResponse};
use cfgd_core::output::Printer;

use crate::ai::client::{AnthropicClient, ContentBlock};
use crate::ai::conversation::Conversation;
use crate::ai::tools;
use crate::generate;
use crate::packages;

use super::{Cli, config_dir};

#[derive(Debug, Args)]
pub struct GenerateArgs {
    #[command(subcommand)]
    pub target: Option<GenerateTarget>,

    /// Override AI model
    #[arg(long)]
    pub model: Option<String>,

    /// Override AI provider
    #[arg(long)]
    pub provider: Option<String>,

    /// Skip confirmation prompts
    #[arg(long, short)]
    pub yes: bool,

    /// Only scan dotfiles and shell config; print findings without AI generation
    #[arg(long)]
    pub scan_only: bool,

    /// Shell to scan for aliases and exports (default: auto-detect from $SHELL)
    #[arg(long)]
    pub shell: Option<String>,

    /// Home directory to scan (default: $HOME)
    #[arg(long, value_hint = clap::ValueHint::DirPath)]
    pub home: Option<String>,
}

#[derive(Debug, Subcommand)]
pub enum GenerateTarget {
    /// Generate a module for a specific tool
    Module {
        /// Tool name to generate module for
        name: String,
    },
    /// Generate a profile
    Profile {
        /// Profile name to generate
        name: String,
    },
}

pub fn cmd_generate(cli: &Cli, printer: &Printer, args: &GenerateArgs) -> anyhow::Result<()> {
    // Support legacy --scan-only mode
    if args.scan_only {
        return cmd_generate_scan_only(printer, args);
    }

    // 1. Load config, resolve AiConfig
    let ai_config = match config::load_config(&cli.config) {
        Ok(cfg) => cfg.spec.ai.clone().unwrap_or_default(),
        Err(cfgd_core::errors::CfgdError::Config(cfgd_core::errors::ConfigError::NotFound {
            ..
        })) => AiConfig::default(),
        Err(e) => {
            tracing::warn!(error = %e, "failed to parse config; using AI defaults");
            AiConfig::default()
        }
    };

    let provider = args.provider.as_deref().unwrap_or(&ai_config.provider);
    let model = args.model.as_deref().unwrap_or(&ai_config.model);

    // 2. Resolve API key
    let api_key = std::env::var(&ai_config.api_key_env).map_err(|_| {
        cfgd_core::errors::GenerateError::ApiKeyNotFound {
            env_var: ai_config.api_key_env.clone(),
        }
    })?;

    // 3. Consent disclosure (unless --yes)
    if !args.yes {
        printer.warning(&format!(
            "This command sends file contents and system information to {}'s API to generate your configuration.",
            provider
        ));
        printer.info(
            "Only files in your home directory are accessible, and private keys/credentials are excluded.",
        );
        let proceed = inquire::Confirm::new("Continue?")
            .with_default(true)
            .prompt()?;
        if !proceed {
            printer.info("Aborted.");
            return Ok(());
        }
    }

    // 4. Create session
    let repo_root = config_dir(cli);
    let mut session = cfgd_core::generate::session::GenerateSession::new(repo_root);

    // 5. Build system prompt
    let skill = generate::GENERATE_SKILL;
    let mode_context = match &args.target {
        None => "Mode: full — scan system, propose structure, generate all profiles and modules."
            .to_string(),
        Some(GenerateTarget::Module { name }) => {
            format!("Mode: module — generate module for tool '{}'.", name)
        }
        Some(GenerateTarget::Profile { name }) => {
            format!("Mode: profile — generate profile '{}'.", name)
        }
    };
    let system_prompt = format!("{}\n\n## Current Session\n\n{}", skill, mode_context);

    // 6. Create client and conversation
    let client = AnthropicClient::new(api_key, model.to_string());
    let mut conversation = Conversation::new(system_prompt);
    let tool_defs = tools::tool_definitions();

    // 7. Build initial user message
    let initial_message = match &args.target {
        None => "Please scan my system and help me organize my configuration into cfgd profiles and modules.".to_string(),
        Some(GenerateTarget::Module { name }) => {
            format!("Please help me create a cfgd module for '{}'.", name)
        }
        Some(GenerateTarget::Profile { name }) => {
            format!("Please help me create a cfgd profile named '{}'.", name)
        }
    };
    conversation.add_user_message(&initial_message);

    // 8. Get package managers for tool dispatch
    let managers: Vec<Box<dyn cfgd_core::providers::PackageManager>> =
        packages::all_package_managers();
    let home = dirs_from_env();

    // 9. Conversation loop
    const MAX_TURNS: usize = 100;
    let mut turn = 0usize;
    loop {
        if turn >= MAX_TURNS {
            printer.warning(&format!(
                "Conversation reached the {MAX_TURNS}-turn limit; stopping."
            ));
            break;
        }
        turn += 1;
        let response = client.send_message(
            conversation.messages(),
            conversation.system_prompt(),
            &tool_defs,
            8192,
        )?;

        tracing::debug!(
            id = %response.id,
            stop_reason = ?response.stop_reason,
            "API response received"
        );

        conversation.track_usage(response.usage.input_tokens, response.usage.output_tokens);

        let mut tool_results: Vec<ContentBlock> = vec![];
        let mut has_tool_calls = false;

        for block in &response.content {
            match block {
                ContentBlock::Text { text } => {
                    printer.info(text);
                }
                ContentBlock::ToolUse { id, name, input } => {
                    has_tool_calls = true;

                    if name == "present_yaml" {
                        let result = handle_present_yaml(printer, id, input, args.yes)?;
                        tool_results.push(result);
                    } else {
                        let result =
                            tools::dispatch_tool_call(name, input, &mut session, &home, &managers);
                        tool_results.push(ContentBlock::ToolResult {
                            tool_use_id: id.clone(),
                            content: result.content,
                            is_error: if result.is_error { Some(true) } else { None },
                        });
                    }
                }
                _ => {}
            }
        }

        // Add assistant message to history
        conversation.add_assistant_message(response.content.clone());

        if has_tool_calls {
            conversation.add_tool_results(tool_results);
        } else {
            break;
        }
    }

    // 10. Show summary
    let generated = session.list_generated();
    if !generated.is_empty() {
        printer.header("Generated files");
        for item in &generated {
            printer.success(&format!("{}: {}", item.name, item.path.display()));
        }

        let commit = if args.yes {
            true
        } else {
            inquire::Confirm::new("Commit all generated files?")
                .with_default(true)
                .prompt()?
        };
        if commit {
            let mut add_cmd = std::process::Command::new("git");
            add_cmd.arg("add");
            for g in &generated {
                add_cmd.arg(g.path.as_os_str());
            }
            let add_out = add_cmd.output()?;
            if !add_out.status.success() {
                printer.warning(&format!(
                    "git add failed: {}",
                    String::from_utf8_lossy(&add_out.stderr).trim()
                ));
            } else {
                let commit_out = std::process::Command::new("git")
                    .args([
                        "commit",
                        "-m",
                        "feat: add AI-generated configuration profiles and modules",
                    ])
                    .output()?;
                if commit_out.status.success() {
                    printer.success("Changes committed.");
                } else {
                    printer.warning(&format!(
                        "git commit failed: {}",
                        String::from_utf8_lossy(&commit_out.stderr).trim()
                    ));
                }
            }
        }
    }

    // 11. Show token usage
    let (input_tokens, output_tokens) = conversation.total_tokens();
    printer.info(&format!(
        "Token usage: {} input, {} output, {} total",
        input_tokens,
        output_tokens,
        input_tokens + output_tokens
    ));

    printer.info("Run 'cfgd apply --dry-run' to preview what would be applied.");

    Ok(())
}

fn handle_present_yaml(
    printer: &Printer,
    tool_use_id: &str,
    input: &serde_json::Value,
    auto_accept: bool,
) -> anyhow::Result<ContentBlock> {
    let req: PresentYamlRequest = serde_json::from_value(input.clone())?;

    printer.header(&format!("Generated {} — {}", req.kind, req.description));
    printer.syntax_highlight(&req.content, "yaml");

    let response = if auto_accept {
        PresentYamlResponse::Accept
    } else {
        let options = vec!["Accept", "Reject", "Give feedback", "Step through"];
        let choice = inquire::Select::new("What would you like to do?", options).prompt()?;

        match choice {
            "Accept" => PresentYamlResponse::Accept,
            "Reject" => PresentYamlResponse::Reject,
            "Give feedback" => {
                let feedback = inquire::Text::new("Your feedback:").prompt()?;
                PresentYamlResponse::Feedback { message: feedback }
            }
            "Step through" => PresentYamlResponse::StepThrough,
            _ => PresentYamlResponse::Reject,
        }
    };

    Ok(ContentBlock::ToolResult {
        tool_use_id: tool_use_id.to_string(),
        content: serde_json::to_string(&serde_json::to_value(response)?)?,
        is_error: None,
    })
}

/// Legacy scan-only mode: scan dotfiles and shell config without AI.
fn cmd_generate_scan_only(printer: &Printer, args: &GenerateArgs) -> anyhow::Result<()> {
    let home_path = if let Some(h) = &args.home {
        std::path::PathBuf::from(h)
    } else {
        dirs_from_env()
    };

    let detected_shell = args
        .shell
        .clone()
        .or_else(|| {
            std::env::var("SHELL")
                .ok()
                .and_then(|s| s.rsplit('/').next().map(|n| n.to_string()))
        })
        .unwrap_or_else(|| "zsh".to_string());

    printer.header("Scanning dotfiles");

    let dotfiles = generate::scan::scan_dotfiles(&home_path)?;
    let tool_set: std::collections::HashSet<String> = dotfiles
        .iter()
        .filter_map(|e| e.tool_guess.clone())
        .collect();

    if dotfiles.is_empty() {
        printer.info("No dotfiles found");
    } else {
        printer.info(&format!("Found {} dotfile entries", dotfiles.len()));
        if !tool_set.is_empty() {
            let mut sorted_tools: Vec<String> = tool_set.into_iter().collect();
            sorted_tools.sort();
            printer.info(&format!("Detected tools: {}", sorted_tools.join(", ")));
        }
    }

    printer.header(&format!("Scanning {} config", detected_shell));
    let shell_result = generate::scan::scan_shell_config(&detected_shell, &home_path)?;
    if !shell_result.aliases.is_empty() {
        printer.info(&format!("Found {} aliases", shell_result.aliases.len()));
    }
    if !shell_result.exports.is_empty() {
        printer.info(&format!("Found {} exports", shell_result.exports.len()));
    }
    if !shell_result.path_additions.is_empty() {
        printer.info(&format!(
            "Found {} PATH additions",
            shell_result.path_additions.len()
        ));
    }
    if let Some(pm) = &shell_result.plugin_manager {
        printer.info(&format!("Plugin manager: {}", pm));
    }

    printer.success("Scan complete — use without --scan-only to generate config");
    Ok(())
}

fn dirs_from_env() -> std::path::PathBuf {
    std::env::var_os("HOME")
        .map(std::path::PathBuf::from)
        .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
}

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

    #[test]
    fn generate_args_default() {
        let args = GenerateArgs {
            target: None,
            model: None,
            provider: None,
            yes: false,
            scan_only: false,
            shell: None,
            home: None,
        };
        assert!(args.target.is_none());
        assert!(!args.yes);
        assert!(!args.scan_only);
    }

    #[test]
    fn generate_args_with_model_override() {
        let args = GenerateArgs {
            target: None,
            model: Some("claude-opus-4-20250514".into()),
            provider: Some("claude".into()),
            yes: true,
            scan_only: false,
            shell: None,
            home: None,
        };
        assert_eq!(args.model.as_deref(), Some("claude-opus-4-20250514"));
        assert_eq!(args.provider.as_deref(), Some("claude"));
        assert!(args.yes);
    }

    #[test]
    fn generate_target_module() {
        let target = GenerateTarget::Module {
            name: "neovim".into(),
        };
        match target {
            GenerateTarget::Module { name } => assert_eq!(name, "neovim"),
            _ => panic!("Expected Module"),
        }
    }

    #[test]
    fn generate_target_profile() {
        let target = GenerateTarget::Profile {
            name: "work".into(),
        };
        match target {
            GenerateTarget::Profile { name } => assert_eq!(name, "work"),
            _ => panic!("Expected Profile"),
        }
    }

    #[test]
    fn dirs_from_env_returns_path() {
        let path = dirs_from_env();
        assert!(!path.as_os_str().is_empty());
    }

    // --- dirs_from_env behavior ---

    #[test]
    fn dirs_from_env_uses_home_env() {
        // HOME is typically set in test environments
        let home = std::env::var_os("HOME");
        let result = dirs_from_env();
        if let Some(h) = home {
            assert_eq!(
                result,
                std::path::PathBuf::from(h),
                "should use HOME env var"
            );
        } else {
            assert_eq!(
                result,
                std::path::PathBuf::from("/tmp"),
                "should fall back to /tmp when HOME is not set"
            );
        }
    }

    // --- GenerateArgs construction tests ---

    #[test]
    fn generate_args_scan_only_mode() {
        let args = GenerateArgs {
            target: None,
            model: None,
            provider: None,
            yes: false,
            scan_only: true,
            shell: Some("bash".into()),
            home: Some("/home/testuser".into()),
        };
        assert!(args.scan_only);
        assert_eq!(args.shell.as_deref(), Some("bash"));
        assert_eq!(args.home.as_deref(), Some("/home/testuser"));
    }

    #[test]
    fn generate_args_module_target() {
        let args = GenerateArgs {
            target: Some(GenerateTarget::Module { name: "git".into() }),
            model: None,
            provider: None,
            yes: false,
            scan_only: false,
            shell: None,
            home: None,
        };
        match args.target {
            Some(GenerateTarget::Module { ref name }) => assert_eq!(name, "git"),
            _ => panic!("Expected Module target"),
        }
    }

    #[test]
    fn generate_args_profile_target() {
        let args = GenerateArgs {
            target: Some(GenerateTarget::Profile {
                name: "work-laptop".into(),
            }),
            model: None,
            provider: None,
            yes: false,
            scan_only: false,
            shell: None,
            home: None,
        };
        match args.target {
            Some(GenerateTarget::Profile { ref name }) => assert_eq!(name, "work-laptop"),
            _ => panic!("Expected Profile target"),
        }
    }

    // --- cmd_generate_scan_only tests ---

    #[test]
    fn cmd_generate_scan_only_with_empty_home() {
        let dir = tempfile::tempdir().unwrap();
        let (printer, buf) = Printer::for_test();
        let args = GenerateArgs {
            target: None,
            model: None,
            provider: None,
            yes: false,
            scan_only: true,
            shell: Some("zsh".into()),
            home: Some(dir.path().to_str().unwrap().to_string()),
        };

        let result = cmd_generate_scan_only(&printer, &args);
        assert!(
            result.is_ok(),
            "scan_only should succeed: {:?}",
            result.err()
        );

        let output = buf.lock().unwrap();
        assert!(
            output.contains("Scanning dotfiles"),
            "should show scanning header, got: {output}"
        );
        assert!(
            output.contains("Scan complete"),
            "should show scan complete, got: {output}"
        );
    }

    #[test]
    fn cmd_generate_scan_only_with_shell_configs() {
        let dir = tempfile::tempdir().unwrap();
        // Create some shell config files that the scanner recognizes
        std::fs::write(
            dir.path().join(".bashrc"),
            "alias ll='ls -la'\nexport EDITOR=vim\nPATH=\"$HOME/bin:$PATH\"\n",
        )
        .unwrap();
        std::fs::write(dir.path().join(".bash_profile"), "source ~/.bashrc\n").unwrap();

        let (printer, buf) = Printer::for_test();
        let args = GenerateArgs {
            target: None,
            model: None,
            provider: None,
            yes: false,
            scan_only: true,
            shell: Some("bash".into()),
            home: Some(dir.path().to_str().unwrap().to_string()),
        };

        let result = cmd_generate_scan_only(&printer, &args);
        assert!(
            result.is_ok(),
            "scan_only should succeed: {:?}",
            result.err()
        );

        let output = buf.lock().unwrap();
        assert!(
            output.contains("Scanning dotfiles"),
            "should scan dotfiles, got: {output}"
        );
        assert!(
            output.contains("Scanning bash config"),
            "should scan bash config, got: {output}"
        );
    }

    #[test]
    fn cmd_generate_scan_only_with_dotfiles() {
        let dir = tempfile::tempdir().unwrap();
        // Create recognizable dotfiles
        std::fs::write(dir.path().join(".vimrc"), "set number\n").unwrap();
        std::fs::write(dir.path().join(".gitconfig"), "[user]\nname = Test\n").unwrap();
        std::fs::create_dir_all(dir.path().join(".config/nvim")).unwrap();
        std::fs::write(
            dir.path().join(".config/nvim/init.lua"),
            "-- neovim config\n",
        )
        .unwrap();

        let (printer, buf) = Printer::for_test();
        let args = GenerateArgs {
            target: None,
            model: None,
            provider: None,
            yes: false,
            scan_only: true,
            shell: Some("zsh".into()),
            home: Some(dir.path().to_str().unwrap().to_string()),
        };

        let result = cmd_generate_scan_only(&printer, &args);
        assert!(
            result.is_ok(),
            "scan_only should succeed: {:?}",
            result.err()
        );

        let output = buf.lock().unwrap();
        assert!(
            output.contains("dotfile"),
            "should report dotfile entries, got: {output}"
        );
    }

    #[test]
    fn cmd_generate_scan_only_default_shell_is_zsh() {
        let dir = tempfile::tempdir().unwrap();
        let (printer, buf) = Printer::for_test();

        // Remove SHELL env var to test default
        let original_shell = std::env::var("SHELL").ok();

        let args = GenerateArgs {
            target: None,
            model: None,
            provider: None,
            yes: false,
            scan_only: true,
            shell: None, // Use auto-detection
            home: Some(dir.path().to_str().unwrap().to_string()),
        };

        let result = cmd_generate_scan_only(&printer, &args);
        assert!(result.is_ok());

        let output = buf.lock().unwrap();
        // Should detect shell from $SHELL env or default to zsh
        if let Some(ref shell) = original_shell {
            let shell_name = shell.rsplit('/').next().unwrap_or("zsh");
            assert!(
                output.contains(&format!("Scanning {} config", shell_name)),
                "should detect shell from env, got: {output}"
            );
        }
    }

    #[test]
    fn cmd_generate_scan_only_shell_with_aliases_and_exports() {
        let dir = tempfile::tempdir().unwrap();
        std::fs::write(
            dir.path().join(".zshrc"),
            concat!(
                "alias g='git'\n",
                "alias dc='docker compose'\n",
                "export GOPATH=$HOME/go\n",
                "export RUST_LOG=debug\n",
                "export PATH=$HOME/.cargo/bin:$PATH\n",
            ),
        )
        .unwrap();

        let (printer, buf) = Printer::for_test();
        let args = GenerateArgs {
            target: None,
            model: None,
            provider: None,
            yes: false,
            scan_only: true,
            shell: Some("zsh".into()),
            home: Some(dir.path().to_str().unwrap().to_string()),
        };

        let result = cmd_generate_scan_only(&printer, &args);
        assert!(result.is_ok());

        let output = buf.lock().unwrap();
        // The scanner should find aliases and exports
        assert!(
            output.contains("aliases") || output.contains("exports") || output.contains("PATH"),
            "should report aliases/exports/PATH additions, got: {output}"
        );
    }

    // --- handle_present_yaml tests ---

    #[test]
    fn handle_present_yaml_auto_accept() {
        let (printer, _buf) = Printer::for_test();
        let input = serde_json::json!({
            "content": "apiVersion: cfgd.io/v1alpha1\nkind: Profile\nmetadata:\n  name: test\nspec: {}\n",
            "kind": "Profile",
            "description": "Test profile"
        });

        let result = handle_present_yaml(&printer, "tool-123", &input, true);
        assert!(result.is_ok());

        match result.unwrap() {
            crate::ai::client::ContentBlock::ToolResult {
                tool_use_id,
                content,
                is_error,
            } => {
                assert_eq!(tool_use_id, "tool-123");
                assert!(is_error.is_none(), "should not be an error");
                // The content should be a JSON-serialized PresentYamlResponse::Accept
                let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
                assert_eq!(
                    parsed["action"], "accept",
                    "auto_accept should produce Accept response"
                );
            }
            _ => panic!("Expected ToolResult"),
        }
    }

    #[test]
    fn handle_present_yaml_shows_header_and_syntax() {
        let (printer, buf) = Printer::for_test();
        let input = serde_json::json!({
            "content": "key: value\n",
            "kind": "Module",
            "description": "My module"
        });

        let result = handle_present_yaml(&printer, "tool-456", &input, true);
        assert!(result.is_ok());

        let output = buf.lock().unwrap();
        assert!(
            output.contains("Module") || output.contains("My module"),
            "should show kind/description in header, got: {output}"
        );
    }

    #[test]
    fn handle_present_yaml_invalid_input_fails() {
        let (printer, _buf) = Printer::for_test();
        // Missing required fields
        let input = serde_json::json!({
            "not_a_valid_field": "bad"
        });

        let result = handle_present_yaml(&printer, "tool-789", &input, true);
        assert!(result.is_err(), "should fail with invalid input");
    }

    // --- cmd_generate_scan_only with custom home and various shell types ---

    #[test]
    fn cmd_generate_scan_only_with_fish_shell() {
        let dir = tempfile::tempdir().unwrap();
        // Fish config goes in .config/fish/config.fish
        let fish_dir = dir.path().join(".config").join("fish");
        std::fs::create_dir_all(&fish_dir).unwrap();
        std::fs::write(
            fish_dir.join("config.fish"),
            "alias ll 'ls -la'\nset -x EDITOR vim\n",
        )
        .unwrap();

        let (printer, buf) = Printer::for_test();
        let args = GenerateArgs {
            target: None,
            model: None,
            provider: None,
            yes: false,
            scan_only: true,
            shell: Some("fish".into()),
            home: Some(dir.path().to_str().unwrap().to_string()),
        };

        let result = cmd_generate_scan_only(&printer, &args);
        assert!(result.is_ok());

        let output = buf.lock().unwrap();
        assert!(
            output.contains("Scanning fish config"),
            "should scan fish config, got: {output}"
        );
    }

    #[test]
    fn cmd_generate_scan_only_no_dotfiles_reports_none() {
        let dir = tempfile::tempdir().unwrap();
        // Completely empty home directory

        let (printer, buf) = Printer::for_test();
        let args = GenerateArgs {
            target: None,
            model: None,
            provider: None,
            yes: false,
            scan_only: true,
            shell: Some("bash".into()),
            home: Some(dir.path().to_str().unwrap().to_string()),
        };

        let result = cmd_generate_scan_only(&printer, &args);
        assert!(result.is_ok());

        let output = buf.lock().unwrap();
        assert!(
            output.contains("No dotfiles found") || output.contains("dotfile"),
            "should report on dotfiles, got: {output}"
        );
    }

    #[test]
    fn cmd_generate_scan_only_detects_tool_from_dotfiles() {
        let dir = tempfile::tempdir().unwrap();
        // Create known dotfiles that map to tools
        std::fs::write(dir.path().join(".tmux.conf"), "set -g mouse on\n").unwrap();
        std::fs::write(dir.path().join(".vimrc"), "set number\n").unwrap();

        let (printer, buf) = Printer::for_test();
        let args = GenerateArgs {
            target: None,
            model: None,
            provider: None,
            yes: false,
            scan_only: true,
            shell: Some("zsh".into()),
            home: Some(dir.path().to_str().unwrap().to_string()),
        };

        let result = cmd_generate_scan_only(&printer, &args);
        assert!(result.is_ok());

        let output = buf.lock().unwrap();
        // Scanner should detect tmux and vim from the dotfiles
        assert!(
            output.contains("Detected tools") || output.contains("dotfile"),
            "should detect tools from dotfiles, got: {output}"
        );
    }

    #[test]
    fn cmd_generate_scan_only_with_plugin_manager() {
        let dir = tempfile::tempdir().unwrap();
        // oh-my-zsh indicator
        std::fs::write(
            dir.path().join(".zshrc"),
            "export ZSH=\"$HOME/.oh-my-zsh\"\nsource $ZSH/oh-my-zsh.sh\n",
        )
        .unwrap();

        let (printer, buf) = Printer::for_test();
        let args = GenerateArgs {
            target: None,
            model: None,
            provider: None,
            yes: false,
            scan_only: true,
            shell: Some("zsh".into()),
            home: Some(dir.path().to_str().unwrap().to_string()),
        };

        let result = cmd_generate_scan_only(&printer, &args);
        assert!(result.is_ok());

        let output = buf.lock().unwrap();
        // Should detect oh-my-zsh as plugin manager
        assert!(
            output.contains("Plugin manager")
                || output.contains("oh-my-zsh")
                || output.contains("Scan complete"),
            "should complete scan, got: {output}"
        );
    }
}
