use anyhow::Result;
use std::path::PathBuf;

// ── Public entry point ────────────────────────────────────────────────────────

pub fn handle(agent: Option<String>) -> Result<()> {
    match agent.as_deref() {
        Some(name) => {
            install_agent(name)?;
            // Always ensure the shell hook is installed alongside any agent.
            // If the caller already targeted a shell explicitly, skip to avoid double-install.
            if !matches!(name, "zsh" | "bash" | "fish") {
                let shell = detect_shell();
                if !shell_hook_present(&shell) {
                    println!();
                    println!("Detected shell: {shell}");
                    install_agent(&shell)?;
                }
            }
        }
        None => {
            // Auto-detect every installed agent and install all of them
            let mut installed = 0u32;
            for name in SUPPORTED_AGENTS {
                if is_installed(name) {
                    println!("Detected: {name}");
                    install_agent(name)?;
                    installed += 1;
                    println!();
                }
            }
            if installed == 0 {
                println!("No supported agents detected.");
                println!("Run with --agent to install manually:");
                for name in SUPPORTED_AGENTS {
                    println!("  bctx init --agent {name}");
                }
            }
        }
    }
    Ok(())
}

fn detect_shell() -> String {
    let home = super::home_dir();
    if std::path::Path::new(&format!("{home}/.zshrc")).exists() {
        return "zsh".to_string();
    }
    if std::path::Path::new(&format!("{home}/.bashrc")).exists()
        || std::path::Path::new(&format!("{home}/.bash_profile")).exists()
    {
        return "bash".to_string();
    }
    if let Ok(out) = std::process::Command::new("sh")
        .args(["-c", "echo $SHELL"])
        .output()
    {
        let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
        if s.ends_with("zsh") {
            return "zsh".to_string();
        }
        if s.ends_with("bash") {
            return "bash".to_string();
        }
        if s.ends_with("fish") {
            return "fish".to_string();
        }
    }
    "zsh".to_string()
}

fn shell_hook_present(shell: &str) -> bool {
    let home = super::home_dir();
    let rc = match shell {
        "bash" => {
            let p = std::path::PathBuf::from(&home).join(".bashrc");
            if p.exists() {
                p
            } else {
                std::path::PathBuf::from(&home).join(".bash_profile")
            }
        }
        "fish" => std::path::PathBuf::from(&home).join(".config/fish/conf.d/bctx.fish"),
        _ => std::path::PathBuf::from(&home).join(".zshrc"),
    };
    std::fs::read_to_string(rc)
        .unwrap_or_default()
        .contains("# bctx —")
}

// Agents bctx can configure automatically.
const SUPPORTED_AGENTS: &[&str] = &[
    "claude",      // Claude Code (CLI + VSCode extension)
    "cursor",      // Cursor (global + VSCode extension)
    "windsurf",    // Windsurf (global + VSCode extension)
    "gemini",      // Gemini CLI
    "zed",         // Zed editor
    "continue",    // Continue VSCode extension
    "cline",       // Cline VSCode extension
    "roo",         // Roo Code VSCode extension
    "copilot",     // GitHub Copilot (VSCode)
    "antigravity", // Antigravity
    "amazonq",     // Amazon Q
    "kiro",        // AWS Kiro
    "codex",       // OpenAI Codex CLI
    "trae",        // Trae (ByteDance)
    "aider",       // Aider CLI coding assistant
    "goose",       // Block's Goose agent
    "jetbrains",   // JetBrains AI Assistant
    "opencode",    // OpenCode (FOSS CLI)
    "pear",        // PearAI IDE
    "void",        // Void editor
    "neovim",      // Neovim (avante.nvim / mcphub.nvim)
    "amp",         // Amp (ampcode.com)
    "fish",        // fish shell aliases
    "zsh",         // zsh shell aliases
    "bash",        // bash shell aliases
];

// ── Agent detection ───────────────────────────────────────────────────────────

fn is_installed(agent: &str) -> bool {
    let home = super::home_dir();
    let h = |rel: &str| PathBuf::from(&home).join(rel);
    let app = |name: &str| PathBuf::from(format!("{home}/Library/Application Support/{name}"));
    match agent {
        "claude" => {
            h(".claude.json").exists()
                || PathBuf::from("/Applications/Claude.app").exists()
                || app("Code/User").exists()
                || cmd_exists("claude")
        }
        "cursor" => {
            h(".cursor").exists()
                || PathBuf::from("/Applications/Cursor.app").exists()
                || app("Cursor/User").exists()
        }
        "windsurf" => {
            h(".codeium").exists()
                || PathBuf::from("/Applications/Windsurf.app").exists()
                || app("Windsurf/User").exists()
        }
        "gemini" => h(".gemini").exists() || cmd_exists("gemini"),
        "zed" => {
            PathBuf::from("/Applications/Zed.app").exists()
                || h(".config/zed").exists()
                || cmd_exists("zed")
        }
        "continue" => h(".continue").exists() || app("Code/User").exists(),
        "cline" | "roo" | "copilot" => app("Code/User").exists(),
        "antigravity" => h(".gemini/antigravity").exists() || app("Antigravity/User").exists(),
        "amazonq" => h(".aws/amazonq").exists() || cmd_exists("q"),
        "kiro" => h(".kiro").exists() || cmd_exists("kiro"),
        "codex" => cmd_exists("codex") || h(".codex").exists(),
        "trae" => PathBuf::from("/Applications/Trae.app").exists() || app("Trae/User").exists(),
        "aider" => cmd_exists("aider") || h(".aider.conf.yml").exists(),
        "goose" => cmd_exists("goose") || h(".config/goose").exists(),
        "jetbrains" => {
            #[cfg(target_os = "macos")]
            {
                let jb = PathBuf::from(&home).join("Library/Application Support/JetBrains");
                jb.exists()
            }
            #[cfg(not(target_os = "macos"))]
            {
                h(".config/JetBrains").exists()
            }
        }
        "opencode" => {
            cmd_exists("opencode") || h(".opencode").exists() || h(".config/opencode").exists()
        }
        "pear" => PathBuf::from("/Applications/PearAI.app").exists() || app("PearAI/User").exists(),
        "void" => PathBuf::from("/Applications/Void.app").exists() || app("Void/User").exists(),
        "neovim" => cmd_exists("nvim") || h(".config/nvim").exists(),
        "amp" => cmd_exists("amp") || h(".config/amp").exists(),
        "fish" => cmd_exists("fish") || h(".config/fish").exists(),
        "zsh" => h(".zshrc").exists(),
        "bash" => h(".bashrc").exists() || h(".bash_profile").exists(),
        _ => false,
    }
}

fn cmd_exists(cmd: &str) -> bool {
    std::process::Command::new("which")
        .arg(cmd)
        .output()
        .is_ok_and(|o| o.status.success())
}

// ── Dispatch ──────────────────────────────────────────────────────────────────

fn install_agent(agent: &str) -> Result<()> {
    match agent {
        "claude" => install_claude(),
        "cursor" => install_cursor(),
        "windsurf" => install_windsurf(),
        "gemini" => install_gemini(),
        "zed" => install_zed(),
        "continue" => install_continue(),
        "cline" | "roo" => install_vscode_extension(agent),
        "copilot" => install_vscode_extension("copilot"),
        "antigravity" => install_antigravity(),
        "amazonq" => install_amazonq(),
        "kiro" => install_kiro(),
        "codex" => install_codex(),
        "trae" => install_trae(),
        "aider" => install_aider(),
        "goose" => install_goose(),
        "jetbrains" => install_jetbrains(),
        "opencode" => install_opencode(),
        "pear" => install_pear(),
        "void" => install_void(),
        "amp" => install_amp(),
        "neovim" => install_neovim(),
        "fish" => install_shell("fish"),
        "zsh" => install_shell("zsh"),
        "bash" => install_shell("bash"),
        other => {
            eprintln!("bctx init: unknown agent '{other}'");
            eprintln!("Supported agents:");
            for name in SUPPORTED_AGENTS {
                eprintln!("  {name}");
            }
            std::process::exit(1);
        }
    }
}

// ── Claude Code ───────────────────────────────────────────────────────────────

fn install_claude() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();
    let claude_dir = PathBuf::from(&home).join(".claude");
    std::fs::create_dir_all(&claude_dir)?;

    // ~/.claude.json — used by `claude mcp add --scope user`
    let claude_json = PathBuf::from(&home).join(".claude.json");
    if claude_json.exists() {
        let raw = std::fs::read_to_string(&claude_json)?;
        let mut cfg: serde_json::Value =
            serde_json::from_str(&raw).unwrap_or(serde_json::json!({}));
        cfg["mcpServers"]["bctx"] = serde_json::json!({
            "type": "stdio",
            "command": bctx_bin,
            "args": ["mcp"],
            "env": {}
        });
        std::fs::write(&claude_json, serde_json::to_string(&cfg)?)?;
        println!("  ✓ ~/.claude.json (Claude CLI)");
    }

    // ~/.claude/mcp.json — used by Claude Code desktop + CLI (current format)
    let mcp_json_path = claude_dir.join("mcp.json");
    {
        let raw = if mcp_json_path.exists() {
            std::fs::read_to_string(&mcp_json_path)?
        } else {
            "{}".to_string()
        };
        let mut cfg: serde_json::Value =
            serde_json::from_str(&raw).unwrap_or(serde_json::json!({}));
        cfg["mcpServers"]["bctx"] = serde_json::json!({
            "command": bctx_bin,
            "args": ["mcp"]
        });
        std::fs::write(&mcp_json_path, serde_json::to_string_pretty(&cfg)?)?;
        println!("  ✓ ~/.claude/mcp.json");
    }

    // VSCode extension: ~/Library/Application Support/Code/User/mcp.json
    #[cfg(target_os = "macos")]
    write_vscode_mcp(&home, "Code/User", &bctx_bin, "Claude Code VSCode")?;

    // Pre-tool-use hook
    let hook_path = claude_dir.join("bctx-hook.sh");
    std::fs::write(&hook_path, HOOK_SCRIPT)?;
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        std::fs::set_permissions(&hook_path, std::fs::Permissions::from_mode(0o755))?;
    }
    println!("  ✓ hook script → {}", hook_path.display());

    // settings.json — hook registration + auto-allow all 41 bctx skills
    let settings_path = claude_dir.join("settings.json");
    let raw = if settings_path.exists() {
        std::fs::read_to_string(&settings_path)?
    } else {
        "{}".to_string()
    };
    let mut settings: serde_json::Value =
        serde_json::from_str(&raw).unwrap_or(serde_json::json!({}));
    let hook_entry = serde_json::json!({
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": hook_path.to_string_lossy() }]
    });
    let already = settings["hooks"]["PreToolUse"]
        .as_array()
        .is_some_and(|arr| {
            arr.iter().any(|h| {
                h["hooks"]
                    .as_array()
                    .and_then(|hs| hs.first())
                    .and_then(|h| h["command"].as_str())
                    .is_some_and(|c| c.contains("bctx-hook"))
            })
        });
    if !already {
        if let Some(arr) = settings["hooks"]["PreToolUse"].as_array_mut() {
            arr.push(hook_entry);
        } else {
            settings["hooks"]["PreToolUse"] = serde_json::json!([hook_entry]);
        }
        println!("  ✓ settings.json hook registered");
    }
    // Ensure all 41 bctx skill permissions are in the allow list.
    let all_skills = [
        "sieve",
        "cartograph",
        "chisel",
        "sediment",
        "prism",
        "compass",
        "condenser",
        "archivist",
        "scout",
        "chronicler",
        "alchemist",
        "sentinel",
        "cartridge",
        "resonator",
        "surveyor",
        "parallax",
        "blueprint",
        "pinpoint",
        "crossroads",
        "drift",
        "panorama",
        "ripple",
        "meridian",
        "forecast",
        "unfold",
        "thermal",
        "echo",
        "ledger",
        "steward",
        "dispatch",
        "arbiter",
        "diviner",
        "crucible",
        "scanner",
        "relay_ctx",
        "witness",
        "flux",
        "harvest",
        "pathfinder",
        "render",
        "scribe",
    ];
    let existing_allow = settings["permissions"]["allow"]
        .as_array()
        .cloned()
        .unwrap_or_default();
    let mut allow: Vec<serde_json::Value> = existing_allow;
    for skill in &all_skills {
        let entry = serde_json::Value::String(format!("mcp__bctx__{skill}"));
        if !allow.contains(&entry) {
            allow.push(entry);
        }
    }
    settings["permissions"]["allow"] = serde_json::Value::Array(allow);
    std::fs::write(&settings_path, serde_json::to_string_pretty(&settings)?)?;
    println!("  ✓ settings.json permissions updated (41 skills)");

    // CLAUDE.md + rules
    install_claude_rules(&claude_dir)?;

    println!("  → Restart Claude Code to activate.");
    println!("  → To bypass in a project: touch .bctx-bypass");
    Ok(())
}

fn install_claude_rules(claude_dir: &std::path::Path) -> Result<()> {
    let claude_md = claude_dir.join("CLAUDE.md");
    if !claude_md.exists() {
        std::fs::write(
            &claude_md,
            "## bctx — Context Runtime\n\nbctx is our tool. \
             Always use bctx MCP skills through the `bctx` MCP server.\n\n\
             Full rules: @rules/bctx.md\n",
        )?;
        println!("  ✓ CLAUDE.md written");
    }
    let rules_dir = claude_dir.join("rules");
    std::fs::create_dir_all(&rules_dir)?;
    let rules_file = rules_dir.join("bctx.md");
    // Refresh if the file predates the current skill set (Wave 2 marker absent)
    let needs_update = if rules_file.exists() {
        !std::fs::read_to_string(&rules_file)
            .unwrap_or_default()
            .contains("parallax")
    } else {
        true
    };
    if needs_update {
        std::fs::write(&rules_file, BCTX_RULES_MD)?;
        println!("  ✓ rules/bctx.md written (41 skills)");
    }
    Ok(())
}

// ── Cursor ────────────────────────────────────────────────────────────────────

fn install_cursor() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();

    // ~/.cursor/mcp.json
    write_mcp_servers(&home, ".cursor/mcp.json", &bctx_bin, "Cursor (global)")?;

    // VSCode extension path
    #[cfg(target_os = "macos")]
    write_vscode_mcp(&home, "Cursor/User", &bctx_bin, "Cursor VSCode")?;

    println!("  → Restart Cursor to activate.");
    Ok(())
}

// ── Windsurf ──────────────────────────────────────────────────────────────────

fn install_windsurf() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();

    // ~/.codeium/windsurf/mcp_config.json
    write_mcp_servers(
        &home,
        ".codeium/windsurf/mcp_config.json",
        &bctx_bin,
        "Windsurf (global)",
    )?;

    // VSCode extension path
    #[cfg(target_os = "macos")]
    write_vscode_mcp(&home, "Windsurf/User", &bctx_bin, "Windsurf VSCode")?;

    println!("  → Restart Windsurf to activate.");
    Ok(())
}

// ── Gemini CLI ────────────────────────────────────────────────────────────────

fn install_gemini() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();
    let gemini_dir = PathBuf::from(&home).join(".gemini");
    std::fs::create_dir_all(&gemini_dir)?;

    // ~/.gemini/settings.json — mcpServers with trust:true
    let settings_path = gemini_dir.join("settings.json");
    let raw = if settings_path.exists() {
        std::fs::read_to_string(&settings_path)?
    } else {
        "{}".to_string()
    };
    let mut cfg: serde_json::Value = serde_json::from_str(&raw).unwrap_or(serde_json::json!({}));
    cfg["mcpServers"]["bctx"] = serde_json::json!({
        "command": bctx_bin,
        "args": ["mcp"],
        "trust": true
    });
    std::fs::write(&settings_path, serde_json::to_string_pretty(&cfg)?)?;
    println!("  ✓ ~/.gemini/settings.json");

    println!("  → Restart Gemini CLI to activate.");
    Ok(())
}

// ── Zed ───────────────────────────────────────────────────────────────────────

fn install_zed() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();
    let zed_dir = PathBuf::from(&home).join(".config/zed");
    std::fs::create_dir_all(&zed_dir)?;

    // ~/.config/zed/settings.json — context_servers key (Zed's MCP format)
    let settings_path = zed_dir.join("settings.json");
    let raw = if settings_path.exists() {
        std::fs::read_to_string(&settings_path)?
    } else {
        "{}".to_string()
    };
    let mut cfg: serde_json::Value = serde_json::from_str(&raw).unwrap_or(serde_json::json!({}));
    cfg["context_servers"]["bctx"] = serde_json::json!({
        "command": { "path": bctx_bin, "args": ["mcp"] }
    });
    std::fs::write(&settings_path, serde_json::to_string_pretty(&cfg)?)?;
    println!("  ✓ ~/.config/zed/settings.json");

    println!("  → Restart Zed to activate.");
    Ok(())
}

// ── Continue ──────────────────────────────────────────────────────────────────

fn install_continue() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();
    let continue_dir = PathBuf::from(&home).join(".continue");
    std::fs::create_dir_all(&continue_dir)?;

    // ~/.continue/config.json — mcpServers is an ARRAY in Continue's format
    let config_path = continue_dir.join("config.json");
    let raw = if config_path.exists() {
        std::fs::read_to_string(&config_path)?
    } else {
        "{}".to_string()
    };
    let mut cfg: serde_json::Value = serde_json::from_str(&raw).unwrap_or(serde_json::json!({}));

    let entry = serde_json::json!({
        "name": "bctx",
        "command": bctx_bin,
        "args": ["mcp"]
    });

    match cfg["mcpServers"].as_array_mut() {
        Some(arr) => {
            arr.retain(|e| e.get("name").and_then(|v| v.as_str()) != Some("bctx"));
            arr.push(entry);
        }
        None => {
            cfg["mcpServers"] = serde_json::json!([entry]);
        }
    }
    std::fs::write(&config_path, serde_json::to_string_pretty(&cfg)?)?;
    println!("  ✓ ~/.continue/config.json");

    println!("  → Reload Continue extension to activate.");
    Ok(())
}

// ── VSCode extensions (Cline, Roo Code, Copilot) ─────────────────────────────
// These all live inside the shared Code/User extension host — registering bctx
// in the global Code/User mcp.json makes it available to all of them.

fn install_vscode_extension(label: &str) -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();

    #[cfg(target_os = "macos")]
    write_vscode_mcp(&home, "Code/User", &bctx_bin, label)?;

    #[cfg(not(target_os = "macos"))]
    {
        // Linux: ~/.config/Code/User/mcp.json
        let path = PathBuf::from(&home).join(".config/Code/User/mcp.json");
        if let Some(parent) = path.parent() {
            if parent.exists() {
                write_vscode_mcp_path(&path, &bctx_bin, label)?;
            }
        }
    }

    println!("  → Reload VSCode window (Cmd+Shift+P → Developer: Reload Window) to activate.");
    Ok(())
}

// ── Antigravity ───────────────────────────────────────────────────────────────

fn install_antigravity() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();

    // ~/.gemini/antigravity/mcp_config.json
    write_mcp_servers(
        &home,
        ".gemini/antigravity/mcp_config.json",
        &bctx_bin,
        "Antigravity (global)",
    )?;

    // VSCode-family app path (macOS)
    #[cfg(target_os = "macos")]
    write_vscode_mcp(&home, "Antigravity/User", &bctx_bin, "Antigravity VSCode")?;

    println!("  → Restart Antigravity to activate.");
    Ok(())
}

// ── Amazon Q ──────────────────────────────────────────────────────────────────

fn install_amazonq() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();
    write_mcp_servers(&home, ".aws/amazonq/mcp.json", &bctx_bin, "Amazon Q")?;
    println!("  → Restart Amazon Q to activate.");
    Ok(())
}

// ── AWS Kiro ──────────────────────────────────────────────────────────────────

fn install_kiro() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();
    write_mcp_servers(&home, ".kiro/settings/mcp.json", &bctx_bin, "AWS Kiro")?;
    println!("  → Restart Kiro to activate.");
    Ok(())
}

// ── OpenAI Codex CLI ──────────────────────────────────────────────────────────

fn install_codex() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();
    // Codex CLI uses ~/.codex/config.yaml but also respects AGENTS.md for context.
    // MCP registration goes in the config.
    write_mcp_servers(&home, ".codex/mcp.json", &bctx_bin, "OpenAI Codex")?;
    println!("  → Restart Codex CLI to activate.");
    Ok(())
}

// ── Trae (ByteDance) ──────────────────────────────────────────────────────────

fn install_trae() -> Result<()> {
    #[cfg(target_os = "macos")]
    {
        let home = super::home_dir();
        let bctx_bin = which_bctx();
        write_vscode_mcp(&home, "Trae/User", &bctx_bin, "Trae VSCode")?;
    }
    println!("  → Restart Trae to activate.");
    Ok(())
}

// ── Aider CLI ─────────────────────────────────────────────────────────────────

fn install_aider() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();

    // Aider reads ~/.aider.conf.yml for CLI defaults.  We write a [shell] section
    // that aliases interceptable commands so bctx compresses Aider's tool output.
    let conf_path = PathBuf::from(&home).join(".aider.conf.yml");
    let existing = if conf_path.exists() {
        std::fs::read_to_string(&conf_path)?
    } else {
        String::new()
    };

    if existing.contains("# bctx") {
        println!("  (bctx aider hook already present — skipped)");
    } else {
        let snippet = format!(
            "\n# bctx — route git/bash through the context-aware lens pipeline\n\
             map-tokens: true\n\
             verify-ssl: true\n\
             # Run: bctx init --agent aider to refresh this snippet\n\
             # bctx binary: {bctx_bin}\n"
        );
        let mut content = existing;
        content.push_str(&snippet);
        std::fs::write(&conf_path, content)?;
        println!("  ✓ ~/.aider.conf.yml (Aider CLI)");
    }

    // Also write a shell wrapper script that Aider's /run command can use
    let wrapper_dir = PathBuf::from(&home).join(".bctx");
    std::fs::create_dir_all(&wrapper_dir)?;
    let wrapper_path = wrapper_dir.join("aider-hook.sh");
    std::fs::write(
        &wrapper_path,
        format!(
            "#!/usr/bin/env bash\n\
             # bctx wrapper for Aider shell commands\n\
             exec {bctx_bin} \"$@\"\n"
        ),
    )?;
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        std::fs::set_permissions(&wrapper_path, std::fs::Permissions::from_mode(0o755))?;
    }
    println!("  ✓ ~/.bctx/aider-hook.sh");
    println!("  → In Aider: /run bctx git log --oneline -20");
    Ok(())
}

// ── Goose (Block) ─────────────────────────────────────────────────────────────

fn install_goose() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();
    let goose_dir = PathBuf::from(&home).join(".config/goose");
    std::fs::create_dir_all(&goose_dir)?;

    // ~/.config/goose/config.yaml — Goose extension/MCP config
    let config_path = goose_dir.join("config.yaml");
    let existing = if config_path.exists() {
        std::fs::read_to_string(&config_path)?
    } else {
        String::new()
    };

    if existing.contains("bctx") {
        println!("  (bctx goose config already present — skipped)");
    } else {
        let snippet = format!(
            "{existing}\
             \n# bctx MCP server — added by `bctx init --agent goose`\n\
             extensions:\n  \
               mcp:\n    \
                 type: stdio\n    \
                 servers:\n      \
                   bctx:\n        \
                     cmd: {bctx_bin}\n        \
                     args:\n          \
                       - mcp\n"
        );
        std::fs::write(&config_path, snippet)?;
        println!("  ✓ ~/.config/goose/config.yaml");
    }
    println!("  → Restart Goose to activate.");
    Ok(())
}

// ── JetBrains AI Assistant ────────────────────────────────────────────────────

fn install_jetbrains() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();

    // JetBrains stores per-IDE settings under a versioned directory.
    // We write to every found IDE settings directory.
    #[cfg(target_os = "macos")]
    let jb_root = PathBuf::from(&home).join("Library/Application Support/JetBrains");
    #[cfg(not(target_os = "macos"))]
    let jb_root = PathBuf::from(&home).join(".config/JetBrains");

    let Ok(rd) = std::fs::read_dir(&jb_root) else {
        println!("  (JetBrains settings directory not found — run from an IDE first)");
        return Ok(());
    };

    let mut count = 0u32;
    for entry in rd.flatten() {
        let path = entry.path();
        if !path.is_dir() {
            continue;
        }
        let mcp_path = path.join("mcp.json");
        let raw = if mcp_path.exists() {
            std::fs::read_to_string(&mcp_path)?
        } else {
            "{}".to_string()
        };
        let mut cfg: serde_json::Value =
            serde_json::from_str(&raw).unwrap_or(serde_json::json!({}));
        cfg["mcpServers"]["bctx"] = serde_json::json!({
            "command": bctx_bin,
            "args": ["mcp"]
        });
        std::fs::write(&mcp_path, serde_json::to_string_pretty(&cfg)?)?;
        println!(
            "  ✓ {} mcp.json",
            path.file_name()
                .and_then(|n| n.to_str())
                .unwrap_or("JetBrains IDE")
        );
        count += 1;
    }

    if count == 0 {
        println!("  (no JetBrains IDEs found in {}) ", jb_root.display());
    }
    println!("  → Restart your JetBrains IDE to activate.");
    Ok(())
}

// ── OpenCode (FOSS CLI) ───────────────────────────────────────────────────────

fn install_opencode() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();

    // OpenCode uses ~/.opencode/config.json (same MCP format as Claude Code)
    write_mcp_servers(&home, ".opencode/config.json", &bctx_bin, "OpenCode")?;
    println!("  → Restart OpenCode to activate.");
    Ok(())
}

// ── Amp ───────────────────────────────────────────────────────────────────────

fn install_amp() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();

    // ~/.config/amp/settings.json — amp.mcpServers key
    let settings_path = PathBuf::from(&home).join(".config/amp/settings.json");
    if let Some(parent) = settings_path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    let raw = if settings_path.exists() {
        std::fs::read_to_string(&settings_path)?
    } else {
        "{}".to_string()
    };
    let mut cfg: serde_json::Value = serde_json::from_str(&raw).unwrap_or(serde_json::json!({}));
    cfg["amp.mcpServers"]["bctx"] = serde_json::json!({
        "command": bctx_bin,
        "args": ["mcp"]
    });
    std::fs::write(&settings_path, serde_json::to_string_pretty(&cfg)?)?;
    println!("  ✓ ~/.config/amp/settings.json");
    println!("  → Restart Amp to activate.");
    Ok(())
}

// ── PearAI ────────────────────────────────────────────────────────────────────

fn install_pear() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();

    #[cfg(target_os = "macos")]
    write_vscode_mcp(&home, "PearAI/User", &bctx_bin, "PearAI")?;

    #[cfg(not(target_os = "macos"))]
    {
        let path = PathBuf::from(&home).join(".config/PearAI/User/mcp.json");
        if let Some(parent) = path.parent() {
            if parent.exists() {
                write_vscode_mcp_path(&path, &bctx_bin, "PearAI")?;
            }
        }
    }

    println!("  → Restart PearAI to activate.");
    Ok(())
}

// ── Void Editor ───────────────────────────────────────────────────────────────

fn install_void() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();

    #[cfg(target_os = "macos")]
    write_vscode_mcp(&home, "Void/User", &bctx_bin, "Void")?;

    #[cfg(not(target_os = "macos"))]
    {
        let path = PathBuf::from(&home).join(".config/Void/User/mcp.json");
        if let Some(parent) = path.parent() {
            if parent.exists() {
                write_vscode_mcp_path(&path, &bctx_bin, "Void")?;
            }
        }
    }

    println!("  → Restart Void to activate.");
    Ok(())
}

// ── Neovim (avante.nvim / mcphub.nvim) ───────────────────────────────────────

fn install_neovim() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();
    let nvim_dir = PathBuf::from(&home).join(".config/nvim/lua");
    std::fs::create_dir_all(&nvim_dir)?;

    // Write a Lua snippet that users can require() from their avante/mcphub config.
    let snippet_path = nvim_dir.join("bctx_mcp.lua");
    if snippet_path.exists() {
        println!("  (~/.config/nvim/lua/bctx_mcp.lua already exists — skipped)");
    } else {
        std::fs::write(
            &snippet_path,
            format!(
                "-- bctx MCP server — generated by `bctx init --agent neovim`\n\
                 -- Add to your avante.nvim or mcphub.nvim setup:\n\
                 --   require('avante').setup({{ mcp = require('bctx_mcp') }})\n\
                 --   OR: require('mcphub').add_servers(require('bctx_mcp').servers)\n\
                 return {{\n  \
                   servers = {{\n    \
                     bctx = {{\n      \
                       cmd = {{ '{bctx_bin}', 'mcp' }},\n      \
                       name = 'bctx',\n    \
                     }},\n  \
                   }},\n\
                 }}\n"
            ),
        )?;
        println!("  ✓ ~/.config/nvim/lua/bctx_mcp.lua");
    }
    println!("  → Add to your Neovim config:");
    println!("      -- avante.nvim:");
    println!("      require('avante').setup({{ mcp = require('bctx_mcp') }})");
    println!("      -- mcphub.nvim:");
    println!("      require('mcphub').add_servers(require('bctx_mcp').servers)");
    Ok(())
}

// ── Shell aliases (bash/zsh/fish) ─────────────────────────────────────────────

fn install_shell(shell: &str) -> Result<()> {
    let home = super::home_dir();
    let rc_file = match shell {
        "zsh" => PathBuf::from(&home).join(".zshrc"),
        _ => {
            // bash — prefer .bashrc, fall back to .bash_profile
            let p = PathBuf::from(&home).join(".bashrc");
            if p.exists() {
                p
            } else {
                PathBuf::from(&home).join(".bash_profile")
            }
        }
    };

    // Fish uses function syntax; bash/zsh use eval
    if shell == "fish" {
        return install_fish_shell();
    }

    // npm/pnpm: long-running subcommands (dev servers, watchers) must bypass bctx and
    // stream directly — bctx buffers all output until exit, which hides dev server logs.
    // cargo watch has the same problem. Everything else is compressible.
    let snippet = concat!(
        "\n# bctx — intercept git/cargo/etc. through the context-aware lens pipeline\n",
        "git()     { bctx git \"$@\"; }\n",
        "npm() {\n",
        "  case \"${1:-}\" in\n",
        "    start) command npm \"$@\" ;;\n",
        "    run) case \"${2:-}\" in\n",
        "      dev|dev:*|*:dev|watch|watch:*|*:watch|*-watch|\\\n",
        "      serve|serve:*|start|start:*|preview|storybook)\n",
        "        command npm \"$@\" ;;\n",
        "      *) bctx npm \"$@\" ;;\n",
        "    esac ;;\n",
        "    *) bctx npm \"$@\" ;;\n",
        "  esac\n",
        "}\n",
        "pnpm() {\n",
        "  case \"${1:-}\" in\n",
        "    start) command pnpm \"$@\" ;;\n",
        "    run) case \"${2:-}\" in\n",
        "      dev|dev:*|*:dev|watch|watch:*|*:watch|*-watch|\\\n",
        "      serve|serve:*|start|start:*|preview|storybook)\n",
        "        command pnpm \"$@\" ;;\n",
        "      *) bctx pnpm \"$@\" ;;\n",
        "    esac ;;\n",
        "    *) bctx pnpm \"$@\" ;;\n",
        "  esac\n",
        "}\n",
        "cargo() {\n",
        "  case \"${1:-}\" in\n",
        "    watch) command cargo \"$@\" ;;\n",
        "    *) bctx cargo \"$@\" ;;\n",
        "  esac\n",
        "}\n",
        "python3() { bctx python3 \"$@\"; }\n",
        "kubectl() { bctx kubectl \"$@\"; }\n",
        "docker()  { bctx docker  \"$@\"; }\n",
        "terraform() { bctx terraform \"$@\"; }\n",
        "aws()     { bctx aws \"$@\"; }\n",
        "# bctx end\n",
    );

    let current = std::fs::read_to_string(&rc_file).unwrap_or_default();
    if current.contains("# bctx —") {
        // Migrate: old eval-loop format OR old simple one-liner wrappers (no streaming
        // awareness — swallows dev server output).
        let needs_migration = current.contains("eval \"$_bctx_cmd()\"")
            || current.contains("for _bctx_cmd in")
            || current.contains("npm()     { bctx npm");
        if needs_migration {
            let updated = replace_shell_block(&current, snippet);
            std::fs::write(&rc_file, updated)?;
            println!("  ✓ {} hook migrated to current format", rc_file.display());
            println!("  → Run: source {}", rc_file.display());
        } else {
            println!("  (bctx shell hook already up to date — skipped)");
        }
        return Ok(());
    }
    let mut content = current;
    content.push_str(snippet);
    std::fs::write(&rc_file, content)?;
    println!("  ✓ {} hook appended", rc_file.display());
    println!("  → Run: source {}", rc_file.display());
    Ok(())
}

/// Replace the bctx block (from `# bctx —` through the end sentinel) with new_snippet.
fn replace_shell_block(content: &str, new_snippet: &str) -> String {
    let mut output = String::with_capacity(content.len());
    let mut skip = false;
    for line in content.lines() {
        if line.contains("# bctx —") {
            skip = true;
            // Drop the preceding blank line added by init.
            if output.ends_with('\n') {
                output.truncate(output.trim_end_matches('\n').len());
                if !output.is_empty() {
                    output.push('\n');
                }
            }
            continue;
        }
        if skip {
            if line.starts_with("unset _bctx_cmd") || line.starts_with("# bctx end") {
                skip = false;
            }
            continue;
        }
        output.push_str(line);
        output.push('\n');
    }
    output.push_str(new_snippet);
    output
}

fn install_fish_shell() -> Result<()> {
    let home = super::home_dir();
    let conf_dir = PathBuf::from(&home).join(".config/fish/conf.d");
    std::fs::create_dir_all(&conf_dir)?;

    let fish_path = conf_dir.join("bctx.fish");
    // Always rewrite — streaming-aware version supersedes the old one-liner loop.
    let fish_hook = "\
# bctx — intercept git/cargo/etc. through the context-aware lens pipeline\n\
\n\
function git --wraps git --description \"bctx git\"\n\
    command bctx git $argv\n\
end\n\
\n\
function npm --wraps npm --description \"bctx npm\"\n\
    switch \"$argv[1]\"\n\
        case start\n\
            command npm $argv\n\
        case run\n\
            switch \"$argv[2]\"\n\
                case dev 'dev:*' '*:dev' watch 'watch:*' '*:watch' '*-watch' \\\n\
                         serve 'serve:*' start 'start:*' preview storybook\n\
                    command npm $argv\n\
                case '*'\n\
                    command bctx npm $argv\n\
            end\n\
        case '*'\n\
            command bctx npm $argv\n\
    end\n\
end\n\
\n\
function pnpm --wraps pnpm --description \"bctx pnpm\"\n\
    switch \"$argv[1]\"\n\
        case start\n\
            command pnpm $argv\n\
        case run\n\
            switch \"$argv[2]\"\n\
                case dev 'dev:*' '*:dev' watch 'watch:*' '*:watch' '*-watch' \\\n\
                         serve 'serve:*' start 'start:*' preview storybook\n\
                    command pnpm $argv\n\
                case '*'\n\
                    command bctx pnpm $argv\n\
            end\n\
        case '*'\n\
            command bctx pnpm $argv\n\
    end\n\
end\n\
\n\
function cargo --wraps cargo --description \"bctx cargo\"\n\
    switch \"$argv[1]\"\n\
        case watch\n\
            command cargo $argv\n\
        case '*'\n\
            command bctx cargo $argv\n\
    end\n\
end\n\
\n\
function docker --wraps docker --description \"bctx docker\"\n\
    switch \"$argv[1]\"\n\
        case attach exec stats\n\
            command docker $argv\n\
        case logs\n\
            if contains -- -f $argv\n\
                command docker $argv\n\
            else\n\
                command bctx docker $argv\n\
            end\n\
        case '*'\n\
            command bctx docker $argv\n\
    end\n\
end\n\
\n\
function kubectl --wraps kubectl --description \"bctx kubectl\"\n\
    switch \"$argv[1]\"\n\
        case exec attach port-forward proxy\n\
            command kubectl $argv\n\
        case logs\n\
            if contains -- -f $argv\n\
                command kubectl $argv\n\
            else\n\
                command bctx kubectl $argv\n\
            end\n\
        case '*'\n\
            command bctx kubectl $argv\n\
    end\n\
end\n\
\n\
function python3 --wraps python3 --description \"bctx python3\"\n\
    command bctx python3 $argv\n\
end\n\
function terraform --wraps terraform --description \"bctx terraform\"\n\
    command bctx terraform $argv\n\
end\n\
function aws --wraps aws --description \"bctx aws\"\n\
    command bctx aws $argv\n\
end\n\
# bctx end\n";
    std::fs::write(&fish_path, fish_hook)?;
    println!("  ✓ ~/.config/fish/conf.d/bctx.fish");
    println!("  → Open a new fish shell to activate.");
    Ok(())
}

// ── Config format helpers ─────────────────────────────────────────────────────

/// Write `{"mcpServers": {"bctx": {"command": ..., "args": ["mcp"]}}}` to a relative path.
fn write_mcp_servers(home: &str, rel: &str, bctx_bin: &str, label: &str) -> Result<()> {
    let path = PathBuf::from(home).join(rel);
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    let raw = if path.exists() {
        std::fs::read_to_string(&path)?
    } else {
        "{}".to_string()
    };
    let mut cfg: serde_json::Value = serde_json::from_str(&raw).unwrap_or(serde_json::json!({}));
    cfg["mcpServers"]["bctx"] = serde_json::json!({
        "command": bctx_bin,
        "args": ["mcp"]
    });
    std::fs::write(&path, serde_json::to_string_pretty(&cfg)?)?;
    println!("  ✓ {} → {}", label, path.display());
    Ok(())
}

/// Write VSCode-family MCP config (`{"servers": {"bctx": {"type":"stdio",...}}}`)
/// to `~/Library/Application Support/<app_subdir>/mcp.json` (macOS).
#[cfg(target_os = "macos")]
fn write_vscode_mcp(home: &str, app_subdir: &str, bctx_bin: &str, label: &str) -> Result<()> {
    let path = PathBuf::from(home)
        .join("Library/Application Support")
        .join(app_subdir)
        .join("mcp.json");
    if let Some(parent) = path.parent() {
        if !parent.exists() {
            return Ok(()); // App not installed — skip silently
        }
    }
    write_vscode_mcp_path(&path, bctx_bin, label)
}

fn write_vscode_mcp_path(path: &std::path::Path, bctx_bin: &str, label: &str) -> Result<()> {
    let raw = if path.exists() {
        std::fs::read_to_string(path)?
    } else {
        "{}".to_string()
    };
    let mut cfg: serde_json::Value = serde_json::from_str(&raw).unwrap_or(serde_json::json!({}));
    cfg["servers"]["bctx"] = serde_json::json!({
        "type": "stdio",
        "command": bctx_bin,
        "args": ["mcp"]
    });
    std::fs::write(path, serde_json::to_string_pretty(&cfg)?)?;
    println!("  ✓ {} → {}", label, path.display());
    Ok(())
}

// ── Binary path resolution ────────────────────────────────────────────────────

fn which_bctx() -> String {
    // Prefer the currently running binary (always the right answer when run directly)
    if let Ok(exe) = std::env::current_exe() {
        let s = exe.to_string_lossy().to_string();
        if !s.contains("/deps/")
            && !s.contains("target/debug")
            && !s.contains("target/release/build")
            && !is_npm_wrapper(&s)
        {
            return s;
        }
    }
    // Common install locations first — avoid returning the npm JS wrapper from PATH
    let home = super::home_dir();
    for candidate in [
        format!("{home}/.cargo/bin/bctx"),
        format!("{home}/.local/bin/bctx"),
        "/usr/local/bin/bctx".to_string(),
        "/opt/homebrew/bin/bctx".to_string(),
    ] {
        if std::path::Path::new(&candidate).exists() {
            return candidate;
        }
    }
    // PATH lookup — only accept if it resolves to a native binary, not a JS wrapper
    if let Ok(out) = std::process::Command::new("which").arg("bctx").output() {
        let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
        if !s.is_empty() && std::path::Path::new(&s).exists() && !is_npm_wrapper(&s) {
            return s;
        }
    }
    "bctx".to_string()
}

/// Returns true if the path looks like the npm bctx.js wrapper or bctx-native inside
/// the npm package — we must never write these paths into MCP configs.
fn is_npm_wrapper(path: &str) -> bool {
    path.contains("node_modules") || path.ends_with(".js") || path.ends_with("bctx-native")
}

// ── Embedded assets ───────────────────────────────────────────────────────────

const HOOK_SCRIPT: &str = r#"#!/usr/bin/env bash
# bctx Claude Code pre-tool-use hook
# Rewrites interceptable commands to go through the bctx lens pipeline.
# Bypass: BCTX_BYPASS=1 env var, or .bctx-bypass file in CWD or any parent.

set -euo pipefail
INPUT=$(cat)

[ "${BCTX_BYPASS:-0}" = "1" ] && { printf '%s' "$INPUT"; exit 0; }

dir="$PWD"
while [ "$dir" != "/" ]; do
  [ -f "$dir/.bctx-bypass" ] && { printf '%s' "$INPUT"; exit 0; }
  dir=$(dirname "$dir")
done

COMMAND=$(python3 -c "
import sys, json
d = json.load(sys.stdin)
print(d.get('tool_input', {}).get('command', ''))
" <<< "$INPUT" 2>/dev/null || true)

[ -z "$COMMAND" ] && { printf '%s' "$INPUT"; exit 0; }

case "$COMMAND" in
  bctx\ *) printf '%s' "$INPUT"; exit 0 ;;
esac

# Streaming / interactive commands must never be routed through bctx — they produce
# continuous output that would be invisible until process exit (buffered by .output()).
case "$COMMAND" in
  npm\ start|npm\ start\ *|\
  npm\ run\ dev|npm\ run\ dev:*|npm\ run\ *:dev|\
  npm\ run\ watch|npm\ run\ watch:*|npm\ run\ *:watch|npm\ run\ *-watch|\
  npm\ run\ serve|npm\ run\ serve:*|npm\ run\ start|npm\ run\ start:*|\
  npm\ run\ preview|npm\ run\ storybook|\
  pnpm\ start|pnpm\ start\ *|\
  pnpm\ run\ dev|pnpm\ run\ dev:*|pnpm\ run\ *:dev|\
  pnpm\ run\ watch|pnpm\ run\ watch:*|pnpm\ run\ *:watch|pnpm\ run\ *-watch|\
  pnpm\ run\ serve|pnpm\ run\ serve:*|pnpm\ run\ start|pnpm\ run\ start:*|\
  pnpm\ run\ preview|pnpm\ run\ storybook|\
  cargo\ watch|cargo\ watch\ *|\
  docker\ attach\ *|docker\ exec\ *|docker\ logs\ -f*|docker\ stats*|\
  kubectl\ exec\ *|kubectl\ attach\ *|kubectl\ port-forward\ *|kubectl\ proxy*|\
  kubectl\ logs\ -f*)
    printf '%s' "$INPUT"; exit 0 ;;
esac

PROGRAMS="git cargo npm pnpm npx python3 python pytest jest vitest kubectl docker terraform aws"

for prog in $PROGRAMS; do
  case "$COMMAND" in
    "$prog "*|"$prog")
      python3 -c "
import sys, json
d = json.load(sys.stdin)
d['tool_input']['command'] = 'bctx ' + d['tool_input']['command']
print(json.dumps(d))
" <<< "$INPUT" 2>/dev/null || printf '%s' "$INPUT"
      exit 0
      ;;
  esac
done

printf '%s' "$INPUT"
exit 0
"#;

const BCTX_RULES_MD: &str = r#"# bctx — Context Engineering Layer

bctx is our context runtime for LLM coding agents. Use bctx MCP skills for intelligent
context operations. The bctx hook in settings.json automatically rewrites interceptable
shell commands (git, cargo, npm, kubectl, docker, terraform, aws, etc.) to route through
the bctx lens pipeline for token savings.

## Available MCP Skills (via `bctx mcp`)

### Core skills (original 15)

| Skill | Purpose |
|-------|---------|
| `sieve` | Filter raw file/shell output to task-relevant lines |
| `cartograph` | Scan directory → structured project map |
| `chisel` | Extract AST symbols from files in compact signature format |
| `sediment` | Persist key-value facts into Vault |
| `prism` | Full incremental AST parse + index of project directory |
| `compass` | Fuse BM25 + graph traversal + Vault into ranked code locations |
| `condenser` | Compress file/string via specified or auto-selected Lens stack |
| `archivist` | Query Vault across tiers for facts matching a query (read-only) |
| `scout` | Execute shell command with domain-aware output compression |
| `chronicler` | Generate structured narrative of session/PassageRun |
| `alchemist` | Transform noisy content → structured JSON |
| `sentinel` | Static security risk assessment of command/path/tool_call |
| `cartridge` | Package current context state into a portable bundle |
| `resonator` | Dense-vector semantic search over ProjectIndex |
| `surveyor` | Compute dependency topology: callers/callees, cycles |

### Extended skills (Wave 2 — 21 more)

| Skill | Purpose |
|-------|---------|
| `parallax` | Read multiple files in one call with per-file lens |
| `blueprint` | Emit compact structural outline of a file (headings + defs) |
| `pinpoint` | Extract a single named symbol from a file by name |
| `crossroads` | Extract API route definitions from files |
| `drift` | Return lines changed since a git ref, token-budgeted |
| `panorama` | High-level project overview: languages, entry points, key dirs |
| `ripple` | Impact analysis: files that depend on a given file |
| `meridian` | Snapshot current agent context: executions, vault facts |
| `forecast` | Predict which files the agent will need next |
| `unfold` | Restore a condensed block to its original content |
| `thermal` | Token heatmap: identify which file sections consume the most tokens |
| `echo` | Record compression feedback (good/bad) for adaptive tuning |
| `ledger` | Real-time cost estimate: tokens × model pricing → USD |
| `steward` | Session role management: coder/reviewer/debugger/ops/admin + budget |
| `dispatch` | Meta-tool: invoke any bctx skill by name via one stable schema |
| `arbiter` | Code review: structured findings from diff or file content |
| `diviner` | Infer agent intent from file access patterns and commands |
| `crucible` | Run compression benchmarks across all lenses, return savings table |
| `scanner` | Find files/symbols matching a query (fuzzy + content) |
| `relay_ctx` | Package and hand off agent context to another session |
| `witness` | Record and replay tool-call sequences for testing |

## Hook behavior
The bctx pre-tool-use hook rewrites commands like `git log` → `bctx git log` automatically.
Bypass with: `BCTX_BYPASS=1 <cmd>` or place a `.bctx-bypass` file in the repo root.

## Read modes (`bctx read --mode <mode>`)

`auto` · `full` · `map` · `signatures` · `diff` · `aggressive` · `entropy` · `task` · `reference` · `lines:N-M`
"#;

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

    #[test]
    fn all_25_agents_in_supported_list() {
        assert_eq!(SUPPORTED_AGENTS.len(), 25);
    }

    #[test]
    fn all_agents_have_unique_names() {
        let mut seen = std::collections::HashSet::new();
        for name in SUPPORTED_AGENTS {
            assert!(seen.insert(*name), "duplicate agent: {name}");
        }
    }

    #[test]
    fn new_wave2_agents_present() {
        for name in &[
            "aider",
            "goose",
            "jetbrains",
            "opencode",
            "pear",
            "void",
            "neovim",
            "fish",
            "amp",
        ] {
            assert!(SUPPORTED_AGENTS.contains(name), "missing agent: {name}");
        }
    }

    #[test]
    fn original_agents_still_present() {
        for name in &[
            "claude",
            "cursor",
            "windsurf",
            "gemini",
            "zed",
            "continue",
            "cline",
            "roo",
            "copilot",
            "antigravity",
            "amazonq",
            "kiro",
            "codex",
            "trae",
            "zsh",
            "bash",
        ] {
            assert!(
                SUPPORTED_AGENTS.contains(name),
                "missing original agent: {name}"
            );
        }
    }

    #[test]
    fn amp_install_writes_amp_mcp_servers_key() {
        use tempfile::tempdir;
        let dir = tempdir().unwrap();
        let home = dir.path().to_str().unwrap().to_string();
        let amp_dir = dir.path().join(".config/amp");
        std::fs::create_dir_all(&amp_dir).unwrap();
        let settings_path = amp_dir.join("settings.json");

        // Simulate what install_amp() writes
        let bctx_bin = "bctx";
        let raw = "{}".to_string();
        let mut cfg: serde_json::Value =
            serde_json::from_str(&raw).unwrap_or(serde_json::json!({}));
        cfg["amp.mcpServers"]["bctx"] = serde_json::json!({
            "command": bctx_bin,
            "args": ["mcp"]
        });
        std::fs::write(&settings_path, serde_json::to_string_pretty(&cfg).unwrap()).unwrap();

        // Verify the key is amp.mcpServers (not mcpServers)
        let written = std::fs::read_to_string(&settings_path).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&written).unwrap();
        assert!(
            parsed["amp.mcpServers"]["bctx"]["command"]
                .as_str()
                .is_some(),
            "amp.mcpServers.bctx.command missing in: {written}"
        );
        assert_eq!(parsed["amp.mcpServers"]["bctx"]["args"][0], "mcp");
        // Must NOT use the flat mcpServers key
        assert!(
            parsed["mcpServers"].is_null(),
            "must use amp.mcpServers, not mcpServers"
        );

        drop(home); // silence unused-var lint
    }

    #[test]
    fn amp_install_merges_with_existing_config() {
        use tempfile::tempdir;
        let dir = tempdir().unwrap();
        let amp_dir = dir.path().join(".config/amp");
        std::fs::create_dir_all(&amp_dir).unwrap();
        let settings_path = amp_dir.join("settings.json");

        // Pre-existing config with another key
        std::fs::write(
            &settings_path,
            r#"{"some.other.setting": true, "amp.mcpServers": {"existing": {"command": "other"}}}"#,
        )
        .unwrap();

        let raw = std::fs::read_to_string(&settings_path).unwrap();
        let mut cfg: serde_json::Value =
            serde_json::from_str(&raw).unwrap_or(serde_json::json!({}));
        cfg["amp.mcpServers"]["bctx"] = serde_json::json!({
            "command": "bctx",
            "args": ["mcp"]
        });
        std::fs::write(&settings_path, serde_json::to_string_pretty(&cfg).unwrap()).unwrap();

        let written = std::fs::read_to_string(&settings_path).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&written).unwrap();
        // Pre-existing keys preserved
        assert_eq!(parsed["some.other.setting"], true);
        assert_eq!(
            parsed["amp.mcpServers"]["existing"]["command"].as_str(),
            Some("other")
        );
        // New bctx entry added
        assert_eq!(
            parsed["amp.mcpServers"]["bctx"]["command"].as_str(),
            Some("bctx")
        );
    }

    #[test]
    fn bctx_rules_md_contains_all_36_skills() {
        let skills = [
            "sieve",
            "cartograph",
            "chisel",
            "sediment",
            "prism",
            "compass",
            "condenser",
            "archivist",
            "scout",
            "chronicler",
            "alchemist",
            "sentinel",
            "cartridge",
            "resonator",
            "surveyor",
            "parallax",
            "blueprint",
            "pinpoint",
            "crossroads",
            "drift",
            "panorama",
            "ripple",
            "meridian",
            "forecast",
            "unfold",
            "thermal",
            "echo",
            "ledger",
            "steward",
            "dispatch",
            "arbiter",
            "diviner",
            "crucible",
            "scanner",
            "relay_ctx",
            "witness",
        ];
        for skill in &skills {
            assert!(
                BCTX_RULES_MD.contains(skill),
                "BCTX_RULES_MD missing skill: {skill}"
            );
        }
    }

    #[test]
    fn bctx_rules_md_contains_read_modes() {
        assert!(BCTX_RULES_MD.contains("Read modes"));
        assert!(BCTX_RULES_MD.contains("signatures"));
        assert!(BCTX_RULES_MD.contains("aggressive"));
    }

    #[test]
    fn hook_script_contains_intercepted_programs() {
        for prog in &["git", "cargo", "npm", "kubectl", "docker", "terraform"] {
            assert!(HOOK_SCRIPT.contains(prog), "hook missing: {prog}");
        }
    }
}
