mod metrics;
mod update;

use std::io::{self, Read, Write};
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use std::process::Command;

use anyhow::{Context, Result, anyhow, bail};
use clap::{Args, CommandFactory, Parser, Subcommand, ValueEnum};
use clap_complete::{Shell, generate};
use codewhale_agent::ModelRegistry;
use codewhale_app_server::{
    AppServerOptions, run as run_app_server, run_stdio as run_app_server_stdio,
};
use codewhale_config::{
    CliRuntimeOverrides, ConfigStore, ProviderKind, ProviderSource, ResolvedRuntimeOptions,
    RuntimeApiKeySource,
};
use codewhale_execpolicy::{AskForApproval, ExecPolicyContext, ExecPolicyEngine};
use codewhale_mcp::{McpServerDefinition, run_stdio_server};
use codewhale_secrets::Secrets;
use codewhale_state::{StateStore, ThreadListFilters};

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
enum ProviderArg {
    Deepseek,
    NvidiaNim,
    Openai,
    Atlascloud,
    WanjieArk,
    Volcengine,
    Openrouter,
    XiaomiMimo,
    Novita,
    Fireworks,
    Siliconflow,
    Arcee,
    Moonshot,
    Sglang,
    Vllm,
    Ollama,
    Huggingface,
    Together,
    OpenaiCodex,
    Anthropic,
    Zai,
    Stepfun,
    Minimax,
}

impl From<ProviderArg> for ProviderKind {
    fn from(value: ProviderArg) -> Self {
        match value {
            ProviderArg::Deepseek => ProviderKind::Deepseek,
            ProviderArg::NvidiaNim => ProviderKind::NvidiaNim,
            ProviderArg::Openai => ProviderKind::Openai,
            ProviderArg::Atlascloud => ProviderKind::Atlascloud,
            ProviderArg::WanjieArk => ProviderKind::WanjieArk,
            ProviderArg::Volcengine => ProviderKind::Volcengine,
            ProviderArg::Openrouter => ProviderKind::Openrouter,
            ProviderArg::XiaomiMimo => ProviderKind::XiaomiMimo,
            ProviderArg::Novita => ProviderKind::Novita,
            ProviderArg::Fireworks => ProviderKind::Fireworks,
            ProviderArg::Siliconflow => ProviderKind::Siliconflow,
            ProviderArg::Arcee => ProviderKind::Arcee,
            ProviderArg::Moonshot => ProviderKind::Moonshot,
            ProviderArg::Sglang => ProviderKind::Sglang,
            ProviderArg::Vllm => ProviderKind::Vllm,
            ProviderArg::Ollama => ProviderKind::Ollama,
            ProviderArg::Huggingface => ProviderKind::Huggingface,
            ProviderArg::Together => ProviderKind::Together,
            ProviderArg::OpenaiCodex => ProviderKind::OpenaiCodex,
            ProviderArg::Anthropic => ProviderKind::Anthropic,
            ProviderArg::Zai => ProviderKind::Zai,
            ProviderArg::Stepfun => ProviderKind::Stepfun,
            ProviderArg::Minimax => ProviderKind::Minimax,
        }
    }
}

#[derive(Debug, Parser)]
#[command(
    name = "codewhale",
    version = env!("DEEPSEEK_BUILD_VERSION"),
    bin_name = "codewhale",
    override_usage = "codewhale [OPTIONS] [PROMPT]\n       codewhale [OPTIONS] <COMMAND> [ARGS]"
)]
struct Cli {
    #[arg(long)]
    config: Option<PathBuf>,
    #[arg(long)]
    profile: Option<String>,
    #[arg(
        long,
        value_enum,
        help = "Advanced provider selector for non-TUI registry/config commands"
    )]
    provider: Option<ProviderArg>,
    #[arg(long)]
    model: Option<String>,
    #[arg(long = "output-mode")]
    output_mode: Option<String>,
    #[arg(
        long = "verbosity",
        value_name = "LEVEL",
        help = "Controls transcript and output verbosity (normal, concise)"
    )]
    verbosity: Option<String>,
    #[arg(long = "log-level")]
    log_level: Option<String>,
    #[arg(long)]
    telemetry: Option<bool>,
    #[arg(long)]
    approval_policy: Option<String>,
    #[arg(long)]
    sandbox_mode: Option<String>,
    #[arg(long)]
    api_key: Option<String>,
    #[arg(long)]
    base_url: Option<String>,
    /// Workspace directory for TUI file tools
    #[arg(short = 'C', long = "workspace", alias = "cd", value_name = "DIR")]
    workspace: Option<PathBuf>,
    #[arg(long = "no-alt-screen", hide = true)]
    no_alt_screen: bool,
    #[arg(long = "mouse-capture", conflicts_with = "no_mouse_capture")]
    mouse_capture: bool,
    #[arg(long = "no-mouse-capture", conflicts_with = "mouse_capture")]
    no_mouse_capture: bool,
    #[arg(long = "skip-onboarding")]
    skip_onboarding: bool,
    /// YOLO mode: auto-approve all tools
    #[arg(long)]
    yolo: bool,
    /// Continue the most recent interactive session for this workspace.
    #[arg(short = 'c', long = "continue")]
    continue_session: bool,
    #[arg(short = 'p', long = "prompt", value_name = "PROMPT")]
    prompt_flag: Option<String>,
    #[arg(
        value_name = "PROMPT",
        trailing_var_arg = true,
        allow_hyphen_values = true
    )]
    prompt: Vec<String>,
    #[command(subcommand)]
    command: Option<Commands>,
}

#[derive(Debug, Subcommand)]
enum Commands {
    /// Run interactive/non-interactive flows via the TUI binary.
    Run(RunArgs),
    /// Run CodeWhale diagnostics.
    Doctor(TuiPassthroughArgs),
    /// List live provider API models via the TUI binary.
    Models(TuiPassthroughArgs),
    /// Generate speech audio with Xiaomi MiMo TTS models via the TUI binary.
    #[command(visible_alias = "tts")]
    Speech(TuiPassthroughArgs),
    /// List saved TUI sessions.
    Sessions(TuiPassthroughArgs),
    /// Resume a saved TUI session.
    Resume(TuiPassthroughArgs),
    /// Fork a saved TUI session.
    Fork(TuiPassthroughArgs),
    /// Create a default AGENTS.md in the current directory.
    Init(TuiPassthroughArgs),
    /// Bootstrap MCP config and/or skills directories.
    Setup(TuiPassthroughArgs),
    /// Run a non-interactive prompt through the TUI runtime.
    #[command(after_help = "\
Examples:
  codewhale exec \"explain this function\"
  codewhale exec --auto \"list crates/ with ls\"
  codewhale exec --auto --output-format stream-json \"fix the failing test\"

Common forwarded flags:
  --auto                           Enable tool-backed agent mode with auto-approvals
  --json                           Emit summary JSON
  --resume <SESSION_ID>            Resume a previous session by ID or prefix
  --session-id <SESSION_ID>        Resume a previous session by ID or prefix
  --continue                       Continue the most recent session for this workspace
  --output-format <FORMAT>         Output format: text or stream-json

Plain `codewhale exec` is a one-shot model response. Use `--auto` for
non-interactive filesystem/shell tool use, matching the supported automation
path used by stream-json wrappers.
")]
    Exec(TuiPassthroughArgs),
    /// Generate SWE-bench prediction rows from CodeWhale runs.
    #[command(after_help = "\
Examples:
  codewhale swebench run --instance-id django__django-12345 --issue-file issue.md
  codewhale swebench export --instance-id django__django-12345 --predictions-path all_preds.jsonl

This command forwards to the TUI runtime. `run` invokes tool-backed agent mode
and writes a SWE-bench-compatible JSONL prediction row from the resulting
working-tree diff. `export` only writes the current diff.
")]
    Swebench(TuiPassthroughArgs),
    /// Manage durable Agent Fleet runs via the TUI runtime.
    Fleet(TuiPassthroughArgs),
    /// Run a CodeWhale-powered code review over a git diff.
    Review(TuiPassthroughArgs),
    /// Apply a patch file or stdin to the working tree.
    Apply(TuiPassthroughArgs),
    /// Run the offline TUI evaluation harness.
    Eval(TuiPassthroughArgs),
    /// Manage TUI MCP servers.
    Mcp(TuiPassthroughArgs),
    /// Inspect TUI feature flags.
    Features(TuiPassthroughArgs),
    /// Run a local TUI server.
    Serve(TuiPassthroughArgs),
    /// Generate shell completions for the TUI binary.
    Completions(TuiPassthroughArgs),
    /// Configure provider credentials.
    Login(LoginArgs),
    /// Remove saved authentication state.
    Logout,
    /// Manage authentication credentials and provider mode.
    Auth(AuthArgs),
    /// Run MCP server mode over stdio.
    McpServer,
    /// Read/write/list config values.
    Config(ConfigArgs),
    /// Resolve or list available models across providers.
    Model(ModelArgs),
    /// Manage thread/session metadata and resume/fork flows.
    Thread(ThreadArgs),
    /// Evaluate sandbox/approval policy decisions.
    Sandbox(SandboxArgs),
    /// Run the app-server transport.
    AppServer(AppServerArgs),
    /// Generate shell completions.
    #[command(after_help = r#"Examples:
  Bash (current shell only):
    source <(codewhale completion bash)

  Bash (persistent, Linux/bash-completion):
    mkdir -p ~/.local/share/bash-completion/completions
    codewhale completion bash > ~/.local/share/bash-completion/completions/codewhale
    # Requires bash-completion to be installed and loaded by your shell.

  Zsh:
    mkdir -p ~/.zfunc
    codewhale completion zsh > ~/.zfunc/_codewhale
    # Add to ~/.zshrc if needed:
    #   fpath=(~/.zfunc $fpath)
    #   autoload -Uz compinit && compinit

  Fish:
    mkdir -p ~/.config/fish/completions
    codewhale completion fish > ~/.config/fish/completions/codewhale.fish

  PowerShell (current shell only):
    codewhale completion powershell | Out-String | Invoke-Expression

The command prints the completion script to stdout; redirect it to a path your shell loads automatically."#)]
    Completion {
        #[arg(value_enum)]
        shell: Shell,
    },
    /// Print a usage rollup from the audit log and session store.
    Metrics(MetricsArgs),
    /// Check for and apply updates to the `codewhale` binary.
    Update(UpdateArgs),
}

#[derive(Debug, Args)]
struct UpdateArgs {
    /// Update to the latest beta release instead of the latest stable release.
    #[arg(long)]
    beta: bool,
    /// Only check the latest release; do not download or replace binaries.
    #[arg(long)]
    check: bool,
    /// Proxy URL to use for update HTTP requests.
    #[arg(long, value_name = "URL")]
    proxy: Option<String>,
}

#[derive(Debug, Args)]
struct MetricsArgs {
    /// Emit machine-readable JSON.
    #[arg(long)]
    json: bool,
    /// Restrict to events newer than this duration (e.g. 7d, 24h, 30m, now-2h).
    #[arg(long, value_name = "DURATION")]
    since: Option<String>,
}

#[derive(Debug, Args)]
struct RunArgs {
    #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
    args: Vec<String>,
}

#[derive(Debug, Args, Clone)]
struct TuiPassthroughArgs {
    #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
    args: Vec<String>,
}

#[derive(Debug, Args)]
struct LoginArgs {
    #[arg(long, value_enum, hide = true)]
    provider: Option<ProviderArg>,
    #[arg(long)]
    api_key: Option<String>,
}

#[derive(Debug, Args)]
struct AuthArgs {
    #[command(subcommand)]
    command: AuthCommand,
}

#[derive(Debug, Subcommand)]
enum AuthCommand {
    /// Show current provider and credential source state.
    /// Without `--provider`, shows all known providers.
    /// With `--provider`, shows detailed status for that provider.
    Status {
        /// Show status for a specific provider only.
        #[arg(long, value_enum)]
        provider: Option<ProviderArg>,
    },
    /// Save an API key to the shared user config file. Reads from
    /// `--api-key`, `--api-key-stdin`, or prompts on stdin when
    /// neither is given. Does not echo the key.
    Set {
        #[arg(long, value_enum)]
        provider: ProviderArg,
        /// Inline value (discouraged — appears in shell history).
        #[arg(long)]
        api_key: Option<String>,
        /// Read the key from stdin instead of prompting.
        #[arg(long = "api-key-stdin", default_value_t = false)]
        api_key_stdin: bool,
    },
    /// Report whether a provider has a key configured. Never prints
    /// the value; just `set` / `not set` plus the source layer.
    Get {
        #[arg(long, value_enum)]
        provider: ProviderArg,
    },
    /// Delete a provider's key from config and secret-store storage.
    Clear {
        #[arg(long, value_enum)]
        provider: ProviderArg,
    },
    /// List all known providers with their auth state, without
    /// revealing keys.
    List,
    /// Advanced: migrate config-file keys into a platform credential store.
    #[command(hide = true)]
    Migrate {
        /// Don't actually write anything; print what would change.
        #[arg(long, default_value_t = false)]
        dry_run: bool,
    },
}

#[derive(Debug, Args)]
struct ConfigArgs {
    #[command(subcommand)]
    command: ConfigCommand,
}

#[derive(Debug, Subcommand)]
enum ConfigCommand {
    Get { key: String },
    Set { key: String, value: String },
    Unset { key: String },
    List,
    Path,
}

#[derive(Debug, Args)]
struct ModelArgs {
    #[command(subcommand)]
    command: ModelCommand,
}

#[derive(Debug, Subcommand)]
enum ModelCommand {
    List {
        #[arg(long, value_enum)]
        provider: Option<ProviderArg>,
    },
    Resolve {
        model: Option<String>,
        #[arg(long, value_enum)]
        provider: Option<ProviderArg>,
    },
}

#[derive(Debug, Args)]
struct ThreadArgs {
    #[command(subcommand)]
    command: ThreadCommand,
}

#[derive(Debug, Subcommand)]
enum ThreadCommand {
    List {
        #[arg(long, default_value_t = false)]
        all: bool,
        #[arg(long)]
        limit: Option<usize>,
    },
    Read {
        thread_id: String,
    },
    Resume {
        thread_id: String,
    },
    Fork {
        thread_id: String,
    },
    Archive {
        thread_id: String,
    },
    Unarchive {
        thread_id: String,
    },
    SetName {
        thread_id: String,
        name: String,
    },
    /// Remove the custom name from a thread, restoring the default
    /// `(unnamed)` rendering in `thread list`.
    ClearName {
        thread_id: String,
    },
}

#[derive(Debug, Args)]
struct SandboxArgs {
    #[command(subcommand)]
    command: SandboxCommand,
}

#[derive(Debug, Subcommand)]
enum SandboxCommand {
    Check {
        command: String,
        #[arg(long, value_enum, default_value_t = ApprovalModeArg::OnRequest)]
        ask: ApprovalModeArg,
    },
}

#[derive(Debug, Clone, Copy, ValueEnum)]
enum ApprovalModeArg {
    UnlessTrusted,
    OnFailure,
    OnRequest,
    Never,
}

impl From<ApprovalModeArg> for AskForApproval {
    fn from(value: ApprovalModeArg) -> Self {
        match value {
            ApprovalModeArg::UnlessTrusted => AskForApproval::UnlessTrusted,
            ApprovalModeArg::OnFailure => AskForApproval::OnFailure,
            ApprovalModeArg::OnRequest => AskForApproval::OnRequest,
            ApprovalModeArg::Never => AskForApproval::Never,
        }
    }
}

#[derive(Debug, Args)]
struct AppServerArgs {
    #[arg(long, default_value = "127.0.0.1")]
    host: String,
    #[arg(long, default_value_t = 8787)]
    port: u16,
    #[arg(long)]
    config: Option<PathBuf>,
    #[arg(long = "auth-token")]
    auth_token: Option<String>,
    #[arg(long, default_value_t = false)]
    insecure_no_auth: bool,
    #[arg(long = "cors-origin")]
    cors_origin: Vec<String>,
    #[arg(long, default_value_t = false)]
    stdio: bool,
}

const MCP_SERVER_DEFINITIONS_KEY: &str = "mcp.server_definitions";

fn install_rustls_crypto_provider() {
    let _ = rustls::crypto::ring::default_provider().install_default();
}

pub fn run_cli() -> std::process::ExitCode {
    install_rustls_crypto_provider();

    match run() {
        Ok(()) => std::process::ExitCode::SUCCESS,
        Err(err) => {
            // Use the full anyhow chain so callers see the underlying
            // cause (e.g. the actual TOML parse error with line/column)
            // instead of just the top-level context message. The bare
            // `{err}` Display impl drops the chain — see #767, where
            // users hit "failed to parse config at <path>" with no
            // hint that the real error was a stray BOM or unbalanced
            // quote a few lines down.
            eprintln!("error: {err}");
            for cause in err.chain().skip(1) {
                eprintln!("  caused by: {cause}");
            }
            std::process::ExitCode::FAILURE
        }
    }
}

fn run() -> Result<()> {
    let mut cli = Cli::parse();

    let mut store = ConfigStore::load(cli.config.clone())?;
    let runtime_overrides = CliRuntimeOverrides {
        provider: cli.provider.map(Into::into),
        model: cli.model.clone(),
        api_key: cli.api_key.clone(),
        base_url: cli.base_url.clone(),
        auth_mode: None,
        output_mode: cli.output_mode.clone(),
        log_level: cli.log_level.clone(),
        telemetry: cli.telemetry,
        approval_policy: cli.approval_policy.clone(),
        sandbox_mode: cli.sandbox_mode.clone(),
        yolo: Some(cli.yolo),
        verbosity: cli.verbosity.clone(),
    };
    let command = cli.command.take();

    match command {
        Some(Commands::Run(args)) => {
            let resolved_runtime = resolve_runtime_for_dispatch(&mut store, &runtime_overrides);
            delegate_to_tui(&cli, &resolved_runtime, args.args)
        }
        Some(Commands::Doctor(args)) => {
            let resolved_runtime = resolve_runtime_for_dispatch(&mut store, &runtime_overrides);
            delegate_to_tui(&cli, &resolved_runtime, tui_args("doctor", args))
        }
        Some(Commands::Models(args)) => {
            let resolved_runtime = resolve_runtime_for_dispatch(&mut store, &runtime_overrides);
            delegate_to_tui(&cli, &resolved_runtime, tui_args("models", args))
        }
        Some(Commands::Speech(args)) => {
            let resolved_runtime = resolve_runtime_for_dispatch(&mut store, &runtime_overrides);
            delegate_to_tui(&cli, &resolved_runtime, tui_args("speech", args))
        }
        Some(Commands::Sessions(args)) => {
            let resolved_runtime = resolve_runtime_for_dispatch(&mut store, &runtime_overrides);
            delegate_to_tui(&cli, &resolved_runtime, tui_args("sessions", args))
        }
        Some(Commands::Resume(args)) => {
            let resolved_runtime = resolve_runtime_for_dispatch(&mut store, &runtime_overrides);
            run_resume_command(&cli, &resolved_runtime, args)
        }
        Some(Commands::Fork(args)) => {
            let resolved_runtime = resolve_runtime_for_dispatch(&mut store, &runtime_overrides);
            delegate_to_tui(&cli, &resolved_runtime, tui_args("fork", args))
        }
        Some(Commands::Init(args)) => {
            let resolved_runtime = resolve_runtime_for_dispatch(&mut store, &runtime_overrides);
            delegate_to_tui(&cli, &resolved_runtime, tui_args("init", args))
        }
        Some(Commands::Setup(args)) => {
            let resolved_runtime = resolve_runtime_for_dispatch(&mut store, &runtime_overrides);
            delegate_to_tui(&cli, &resolved_runtime, tui_args("setup", args))
        }
        Some(Commands::Exec(args)) => {
            let resolved_runtime = resolve_runtime_for_dispatch(&mut store, &runtime_overrides);
            delegate_to_tui(&cli, &resolved_runtime, tui_args("exec", args))
        }
        Some(Commands::Swebench(args)) => {
            let resolved_runtime = resolve_runtime_for_dispatch(&mut store, &runtime_overrides);
            delegate_to_tui(&cli, &resolved_runtime, tui_args("swebench", args))
        }
        Some(Commands::Fleet(args)) => {
            let resolved_runtime = resolve_runtime_for_dispatch(&mut store, &runtime_overrides);
            delegate_to_tui(&cli, &resolved_runtime, tui_args("fleet", args))
        }
        Some(Commands::Review(args)) => {
            let resolved_runtime = resolve_runtime_for_dispatch(&mut store, &runtime_overrides);
            delegate_to_tui(&cli, &resolved_runtime, tui_args("review", args))
        }
        Some(Commands::Apply(args)) => {
            let resolved_runtime = resolve_runtime_for_dispatch(&mut store, &runtime_overrides);
            delegate_to_tui(&cli, &resolved_runtime, tui_args("apply", args))
        }
        Some(Commands::Eval(args)) => {
            let resolved_runtime = resolve_runtime_for_dispatch(&mut store, &runtime_overrides);
            delegate_to_tui(&cli, &resolved_runtime, tui_args("eval", args))
        }
        Some(Commands::Mcp(args)) => {
            let resolved_runtime = resolve_runtime_for_dispatch(&mut store, &runtime_overrides);
            delegate_to_tui(&cli, &resolved_runtime, tui_args("mcp", args))
        }
        Some(Commands::Features(args)) => {
            let resolved_runtime = resolve_runtime_for_dispatch(&mut store, &runtime_overrides);
            delegate_to_tui(&cli, &resolved_runtime, tui_args("features", args))
        }
        Some(Commands::Serve(args)) => {
            let resolved_runtime = resolve_runtime_for_dispatch(&mut store, &runtime_overrides);
            delegate_to_tui(&cli, &resolved_runtime, tui_args("serve", args))
        }
        Some(Commands::Completions(args)) => {
            let resolved_runtime = resolve_runtime_for_dispatch(&mut store, &runtime_overrides);
            delegate_to_tui(&cli, &resolved_runtime, tui_args("completions", args))
        }
        Some(Commands::Login(args)) => run_login_command(&mut store, args),
        Some(Commands::Logout) => run_logout_command(&mut store),
        Some(Commands::Auth(args)) => run_auth_command(&mut store, args.command),
        Some(Commands::McpServer) => run_mcp_server_command(&mut store),
        Some(Commands::Config(args)) => run_config_command(&mut store, args.command),
        Some(Commands::Model(args)) => run_model_command(args.command, runtime_overrides.provider),
        Some(Commands::Thread(args)) => run_thread_command(args.command),
        Some(Commands::Sandbox(args)) => run_sandbox_command(args.command),
        Some(Commands::AppServer(args)) => run_app_server_command(args),
        Some(Commands::Completion { shell }) => {
            let mut cmd = Cli::command();
            generate(shell, &mut cmd, "codewhale", &mut io::stdout());
            Ok(())
        }
        Some(Commands::Metrics(args)) => run_metrics_command(args),
        Some(Commands::Update(args)) => update::run_update(args.beta, args.check, args.proxy),
        None => {
            let resolved_runtime = resolve_runtime_for_dispatch(&mut store, &runtime_overrides);
            let forwarded = root_tui_passthrough(&cli)?;
            delegate_to_tui(&cli, &resolved_runtime, forwarded)
        }
    }
}

fn root_tui_passthrough(cli: &Cli) -> Result<Vec<String>> {
    let mut forwarded = Vec::new();
    if cli.continue_session {
        forwarded.push("--continue".to_string());
    }

    let prompt =
        cli.prompt_flag
            .iter()
            .chain(cli.prompt.iter())
            .fold(String::new(), |mut acc, part| {
                if !acc.is_empty() {
                    acc.push(' ');
                }
                acc.push_str(part);
                acc
            });
    if !prompt.is_empty() {
        if cli.continue_session {
            bail!(
                "`codewhale --continue` resumes the interactive TUI. Use `codewhale exec --continue <PROMPT>` to continue a session non-interactively."
            );
        }
        forwarded.push("--prompt".to_string());
        forwarded.push(prompt);
    }

    Ok(forwarded)
}

fn resolve_runtime_for_dispatch(
    store: &mut ConfigStore,
    runtime_overrides: &CliRuntimeOverrides,
) -> ResolvedRuntimeOptions {
    let runtime_secrets = Secrets::auto_detect();
    resolve_runtime_for_dispatch_with_secrets(store, runtime_overrides, &runtime_secrets)
}

fn resolve_runtime_for_dispatch_with_secrets(
    store: &mut ConfigStore,
    runtime_overrides: &CliRuntimeOverrides,
    secrets: &Secrets,
) -> ResolvedRuntimeOptions {
    let mut resolved = store
        .config
        .resolve_runtime_options_with_secrets(runtime_overrides, secrets);

    if resolved.api_key_source == Some(RuntimeApiKeySource::Keyring)
        && !provider_config_set(store, resolved.provider)
        && let Some(api_key) = resolved.api_key.clone()
    {
        write_provider_api_key_to_config(store, resolved.provider, &api_key);
        match store.save() {
            Ok(()) => {
                eprintln!(
                    "info: recovered API key from secret store and saved it to {}",
                    store.path().display()
                );
                resolved.api_key_source = Some(RuntimeApiKeySource::ConfigFile);
            }
            Err(err) => {
                eprintln!(
                    "warning: recovered API key from secret store but failed to save {}: {err}",
                    store.path().display()
                );
            }
        }
    }

    resolved
}

fn tui_args(command: &str, args: TuiPassthroughArgs) -> Vec<String> {
    let mut forwarded = Vec::with_capacity(args.args.len() + 1);
    forwarded.push(command.to_string());
    forwarded.extend(args.args);
    forwarded
}

fn run_login_command(store: &mut ConfigStore, args: LoginArgs) -> Result<()> {
    run_login_command_with_secrets(store, args, &Secrets::auto_detect())
}

fn run_login_command_with_secrets(
    store: &mut ConfigStore,
    args: LoginArgs,
    secrets: &Secrets,
) -> Result<()> {
    let provider: ProviderKind = args.provider.unwrap_or(ProviderArg::Deepseek).into();
    store.config.provider = provider;

    let api_key = match args.api_key {
        Some(v) => v,
        None => read_api_key_from_stdin()?,
    };
    write_provider_api_key_to_config(store, provider, &api_key);
    let keyring_saved = write_provider_api_key_to_keyring(secrets, provider, &api_key);
    store.save()?;
    let destination = if keyring_saved {
        format!("{} and {}", store.path().display(), secrets.backend_name())
    } else {
        store.path().display().to_string()
    };
    if provider == ProviderKind::Deepseek {
        println!("logged in using API key mode (deepseek); saved key to {destination}");
    } else {
        println!(
            "logged in using API key mode ({}); saved key to {destination}",
            provider.as_str(),
        );
    }
    Ok(())
}

fn run_logout_command(store: &mut ConfigStore) -> Result<()> {
    run_logout_command_with_secrets(store, &Secrets::auto_detect())
}

fn run_logout_command_with_secrets(store: &mut ConfigStore, secrets: &Secrets) -> Result<()> {
    let active_provider = store.config.provider;
    store.config.api_key = None;
    for provider in PROVIDER_LIST {
        clear_provider_api_key_from_config(store, provider);
    }
    clear_provider_api_key_from_keyring(secrets, active_provider);
    store.config.auth_mode = None;
    store.save()?;
    println!("logged out");
    Ok(())
}

/// Map [`ProviderKind`] to the canonical provider credential slot.
fn provider_slot(provider: ProviderKind) -> &'static str {
    match provider {
        ProviderKind::Deepseek => "deepseek",
        ProviderKind::NvidiaNim => "nvidia-nim",
        ProviderKind::Openai => "openai",
        ProviderKind::Atlascloud => "atlascloud",
        ProviderKind::WanjieArk => "wanjie-ark",
        ProviderKind::Volcengine => "volcengine",
        ProviderKind::Openrouter => "openrouter",
        ProviderKind::XiaomiMimo => "xiaomi-mimo",
        ProviderKind::Novita => "novita",
        ProviderKind::Fireworks => "fireworks",
        ProviderKind::Siliconflow => "siliconflow",
        ProviderKind::SiliconflowCN => "siliconflow",
        ProviderKind::Arcee => "arcee",
        ProviderKind::Moonshot => "moonshot",
        ProviderKind::Sglang => "sglang",
        ProviderKind::Vllm => "vllm",
        ProviderKind::Ollama => "ollama",
        ProviderKind::Huggingface => "huggingface",
        ProviderKind::Together => "together",
        ProviderKind::OpenaiCodex => "openai-codex",
        ProviderKind::Anthropic => "anthropic",
        ProviderKind::Zai => "zai",
        ProviderKind::Stepfun => "stepfun",
        ProviderKind::Minimax => "minimax",
    }
}

/// Provider order used by the `auth list` and `auth status` outputs.
const PROVIDER_LIST: [ProviderKind; 24] = [
    ProviderKind::Deepseek,
    ProviderKind::NvidiaNim,
    ProviderKind::Openai,
    ProviderKind::Atlascloud,
    ProviderKind::WanjieArk,
    ProviderKind::Volcengine,
    ProviderKind::Openrouter,
    ProviderKind::XiaomiMimo,
    ProviderKind::Novita,
    ProviderKind::Fireworks,
    ProviderKind::Siliconflow,
    ProviderKind::SiliconflowCN,
    ProviderKind::Arcee,
    ProviderKind::Moonshot,
    ProviderKind::Sglang,
    ProviderKind::Vllm,
    ProviderKind::Ollama,
    ProviderKind::Huggingface,
    ProviderKind::Together,
    ProviderKind::OpenaiCodex,
    ProviderKind::Anthropic,
    ProviderKind::Zai,
    ProviderKind::Stepfun,
    ProviderKind::Minimax,
];

fn provider_is_supported_by_tui(provider: ProviderKind) -> bool {
    matches!(
        provider,
        ProviderKind::Deepseek
            | ProviderKind::NvidiaNim
            | ProviderKind::Openai
            | ProviderKind::Atlascloud
            | ProviderKind::WanjieArk
            | ProviderKind::Volcengine
            | ProviderKind::Openrouter
            | ProviderKind::XiaomiMimo
            | ProviderKind::Novita
            | ProviderKind::Fireworks
            | ProviderKind::Siliconflow
            | ProviderKind::SiliconflowCN
            | ProviderKind::Arcee
            | ProviderKind::Moonshot
            | ProviderKind::Sglang
            | ProviderKind::Vllm
            | ProviderKind::Ollama
            | ProviderKind::Huggingface
            | ProviderKind::Together
            | ProviderKind::OpenaiCodex
            | ProviderKind::Zai
            | ProviderKind::Stepfun
            | ProviderKind::Minimax
    )
    // NOTE: Anthropic is intentionally exec-only in the interactive TUI: it
    // speaks the native Messages API rather than the OpenAI-compatible shape the
    // interactive loop expects, so `codewhale --provider anthropic` is rejected
    // with a hint to use `codewhale exec --provider anthropic`. Zai (GLM/Z.AI),
    // Stepfun, and Minimax are OpenAI-compatible and supported interactively.
    // (Re-evaluate if/when the interactive loop gains a native Anthropic client.)
}

fn supported_tui_providers_csv() -> String {
    ProviderKind::ALL
        .iter()
        .copied()
        .filter(|provider| provider_is_supported_by_tui(*provider))
        .map(ProviderKind::as_str)
        .collect::<Vec<_>>()
        .join(", ")
}

#[cfg(test)]
fn no_keyring_secrets() -> Secrets {
    Secrets::new(std::sync::Arc::new(
        codewhale_secrets::InMemoryKeyringStore::new(),
    ))
}

fn write_provider_api_key_to_config(
    store: &mut ConfigStore,
    provider: ProviderKind,
    api_key: &str,
) {
    store.config.auth_mode = Some("api_key".to_string());
    store.config.providers.for_provider_mut(provider).api_key = Some(api_key.to_string());
    if provider == ProviderKind::Deepseek {
        store.config.api_key = Some(api_key.to_string());
        if store.config.default_text_model.is_none() {
            store.config.default_text_model = Some(
                store
                    .config
                    .providers
                    .deepseek
                    .model
                    .clone()
                    .unwrap_or_else(|| "deepseek-v4-pro".to_string()),
            );
        }
    }
}

fn clear_provider_api_key_from_config(store: &mut ConfigStore, provider: ProviderKind) {
    store.config.providers.for_provider_mut(provider).api_key = None;
    if provider == ProviderKind::Deepseek {
        store.config.api_key = None;
    }
}

fn provider_env_set(provider: ProviderKind) -> bool {
    provider_env_value(provider).is_some()
}

fn provider_env_vars(provider: ProviderKind) -> &'static [&'static str] {
    match provider {
        ProviderKind::Deepseek => &["DEEPSEEK_API_KEY"],
        ProviderKind::Openrouter => &["OPENROUTER_API_KEY"],
        ProviderKind::XiaomiMimo => &["XIAOMI_MIMO_API_KEY", "XIAOMI_API_KEY", "MIMO_API_KEY"],
        ProviderKind::Novita => &["NOVITA_API_KEY"],
        ProviderKind::NvidiaNim => &["NVIDIA_API_KEY", "NVIDIA_NIM_API_KEY", "DEEPSEEK_API_KEY"],
        ProviderKind::Fireworks => &["FIREWORKS_API_KEY"],
        ProviderKind::Siliconflow => &["SILICONFLOW_API_KEY"],
        ProviderKind::SiliconflowCN => &["SILICONFLOW_API_KEY"],
        ProviderKind::Arcee => &["ARCEE_API_KEY"],
        ProviderKind::Moonshot => &["MOONSHOT_API_KEY", "KIMI_API_KEY"],
        ProviderKind::Sglang => &["SGLANG_API_KEY"],
        ProviderKind::Vllm => &["VLLM_API_KEY"],
        ProviderKind::Ollama => &["OLLAMA_API_KEY"],
        ProviderKind::Huggingface => &["HUGGINGFACE_API_KEY", "HF_TOKEN"],
        ProviderKind::Openai => &["OPENAI_API_KEY"],
        ProviderKind::Atlascloud => &["ATLASCLOUD_API_KEY"],
        ProviderKind::Volcengine => &[
            "VOLCENGINE_API_KEY",
            "VOLCENGINE_ARK_API_KEY",
            "ARK_API_KEY",
        ],
        ProviderKind::WanjieArk => &[
            "WANJIE_ARK_API_KEY",
            "WANJIE_API_KEY",
            "WANJIE_MAAS_API_KEY",
        ],
        ProviderKind::Together => &["TOGETHER_API_KEY"],
        ProviderKind::OpenaiCodex => &["OPENAI_CODEX_ACCESS_TOKEN", "CODEX_ACCESS_TOKEN"],
        ProviderKind::Anthropic => &["ANTHROPIC_API_KEY"],
        ProviderKind::Zai => &["ZAI_API_KEY", "Z_AI_API_KEY"],
        ProviderKind::Stepfun => &["STEPFUN_API_KEY", "STEP_API_KEY"],
        ProviderKind::Minimax => &["MINIMAX_API_KEY"],
    }
}

fn provider_env_value(provider: ProviderKind) -> Option<(&'static str, String)> {
    provider_env_vars(provider).iter().find_map(|var| {
        std::env::var(var)
            .ok()
            .filter(|value| !value.trim().is_empty())
            .map(|value| (*var, value))
    })
}

fn openai_codex_auth_file_path() -> PathBuf {
    if let Ok(path) = std::env::var("OPENAI_CODEX_AUTH_FILE") {
        let path = PathBuf::from(path);
        if !path.as_os_str().is_empty() {
            return path;
        }
    }

    let codex_home = std::env::var("CODEX_HOME")
        .map(PathBuf::from)
        .unwrap_or_else(|_| {
            dirs::home_dir()
                .unwrap_or_else(|| PathBuf::from("."))
                .join(".codex")
        });
    codex_home.join("auth.json")
}

fn provider_oauth_file_path(provider: ProviderKind) -> Option<PathBuf> {
    (provider == ProviderKind::OpenaiCodex).then(openai_codex_auth_file_path)
}

fn provider_config_api_key(store: &ConfigStore, provider: ProviderKind) -> Option<&str> {
    let slot = store
        .config
        .providers
        .for_provider(provider)
        .api_key
        .as_deref();
    let root = (provider == ProviderKind::Deepseek)
        .then_some(store.config.api_key.as_deref())
        .flatten();
    slot.or(root).filter(|v| !v.trim().is_empty())
}

fn provider_config_set(store: &ConfigStore, provider: ProviderKind) -> bool {
    provider_config_api_key(store, provider).is_some()
}

fn provider_keyring_api_key(secrets: &Secrets, provider: ProviderKind) -> Option<String> {
    secrets
        .get(provider_slot(provider))
        .ok()
        .flatten()
        .filter(|v| !v.trim().is_empty())
}

fn provider_keyring_set(secrets: &Secrets, provider: ProviderKind) -> bool {
    provider_keyring_api_key(secrets, provider).is_some()
}

fn write_provider_api_key_to_keyring(
    secrets: &Secrets,
    provider: ProviderKind,
    api_key: &str,
) -> bool {
    secrets.set(provider_slot(provider), api_key).is_ok()
}

fn clear_provider_api_key_from_keyring(secrets: &Secrets, provider: ProviderKind) {
    let _ = secrets.delete(provider_slot(provider));
}

fn auth_status_all_providers(store: &ConfigStore, secrets: &Secrets) -> Vec<String> {
    let active_provider = store.config.provider;
    let mut lines = Vec::new();
    lines.push(format!(
        "active provider: {} (set via config or CODEWHALE_PROVIDER)",
        active_provider.as_str()
    ));
    lines.push(String::new());
    lines.push(format!(
        "{:<14} {:<8} {:<10} {:<8} {}",
        "provider", "config", "keyring", "env", "status"
    ));
    lines.push("-".repeat(70));

    for provider in PROVIDER_LIST {
        let config_key = provider_config_api_key(store, provider);
        let keyring_key = provider_keyring_api_key(secrets, provider);
        let env_key = provider_env_value(provider);
        let oauth_file_present = provider_oauth_file_path(provider).is_some_and(|p| p.exists());

        let config_status = config_key.map(|_| "set").unwrap_or("-");
        let keyring_status = keyring_key.as_ref().map(|_| "set").unwrap_or("-");
        let env_status = env_key.as_ref().map(|_| "set").unwrap_or("-");

        let source = if provider == ProviderKind::OpenaiCodex {
            // Keep the summary consistent with `auth status`: Codex auth is
            // OAuth-file (or env token) based — config/keyring keys are not
            // consulted for it.
            if env_key.is_some() {
                "env"
            } else if oauth_file_present {
                "oauth file"
            } else {
                "unset"
            }
        } else if config_key.is_some() {
            "config"
        } else if keyring_key.is_some() {
            "keyring"
        } else if env_key.is_some() {
            "env"
        } else if oauth_file_present {
            "oauth file"
        } else {
            "unset"
        };

        let active_marker = if provider == active_provider {
            " *"
        } else {
            ""
        };

        lines.push(format!(
            "{:<14} {:<8} {:<10} {:<8} {}{}",
            provider.as_str(),
            config_status,
            keyring_status,
            env_status,
            source,
            active_marker
        ));
    }

    lines.push(String::new());
    lines.push("* = active provider (from config or CODEWHALE_PROVIDER)".to_string());
    lines.push("Run `codewhale auth status --provider <id>` for detailed info.".to_string());
    lines
}

fn auth_status_lines_for_provider(
    store: &ConfigStore,
    secrets: &Secrets,
    provider: ProviderKind,
) -> Vec<String> {
    let config_key = provider_config_api_key(store, provider);
    let keyring_key = provider_keyring_api_key(secrets, provider);
    let env_key = provider_env_value(provider);
    let oauth_file = provider_oauth_file_path(provider);
    let oauth_file_present = oauth_file.as_ref().is_some_and(|path| path.exists());

    let active_source = if provider == ProviderKind::OpenaiCodex {
        if env_key.is_some() {
            "env"
        } else if oauth_file_present {
            "Codex OAuth file"
        } else {
            "missing"
        }
    } else if config_key.is_some() {
        "config"
    } else if keyring_key.is_some() {
        "secret store"
    } else if env_key.is_some() {
        "env"
    } else {
        "missing"
    };
    let active_last4 = if provider == ProviderKind::OpenaiCodex {
        env_key.as_ref().map(|(_, value)| last4_label(value))
    } else {
        config_key
            .map(last4_label)
            .or_else(|| keyring_key.as_deref().map(last4_label))
            .or_else(|| env_key.as_ref().map(|(_, value)| last4_label(value)))
    };
    let active_label = active_last4
        .map(|last4| format!("{active_source} (last4: {last4})"))
        .unwrap_or_else(|| active_source.to_string());

    let env_var_label = env_key
        .as_ref()
        .map(|(name, _)| (*name).to_string())
        .unwrap_or_else(|| provider_env_vars(provider).join("/"));
    let env_status = env_key
        .as_ref()
        .map(|(_, value)| format!("set, last4: {}", last4_label(value)))
        .unwrap_or_else(|| "unset".to_string());

    let is_active = provider == store.config.provider;
    let active_marker = if is_active { " (active provider)" } else { "" };

    let provider_cfg = store.config.providers.for_provider(provider);
    let base_url = provider_cfg.base_url.as_deref().unwrap_or("(default)");
    let model = provider_cfg.model.as_deref().unwrap_or("(default)");

    let lookup_order = if provider == ProviderKind::OpenaiCodex {
        "lookup order: env -> Codex OAuth file".to_string()
    } else {
        "lookup order: config -> secret store -> env".to_string()
    };
    let auth_mode = if provider == ProviderKind::OpenaiCodex {
        "codex_oauth"
    } else {
        store.config.auth_mode.as_deref().unwrap_or("api_key")
    };

    let mut lines = vec![
        format!("provider: {}{}", provider.as_str(), active_marker),
        format!("route: {}", base_url),
        format!("model: {}", model),
        format!("auth mode: {auth_mode}"),
        format!("active source: {active_label}"),
        lookup_order,
        format!(
            "config file: {} ({})",
            store.path().display(),
            source_status(config_key, "missing")
        ),
        format!(
            "secret store: {} ({})",
            secrets.backend_name(),
            source_status(keyring_key.as_deref(), "missing")
        ),
        format!("env var: {env_var_label} ({env_status})"),
    ];
    if let Some(path) = oauth_file {
        let status = if path.exists() { "present" } else { "missing" };
        lines.push(format!("Codex OAuth file: {} ({status})", path.display()));
    }
    lines
}

fn source_status(value: Option<&str>, missing_label: &str) -> String {
    value
        .map(|v| format!("set, last4: {}", last4_label(v)))
        .unwrap_or_else(|| missing_label.to_string())
}

fn last4_label(value: &str) -> String {
    let trimmed = value.trim();
    let chars: Vec<char> = trimmed.chars().collect();
    if chars.len() <= 4 {
        return "<redacted>".to_string();
    }
    let last4: String = chars[chars.len() - 4..].iter().collect();
    format!("...{last4}")
}

fn run_auth_command(store: &mut ConfigStore, command: AuthCommand) -> Result<()> {
    run_auth_command_with_secrets(store, command, &Secrets::auto_detect())
}

fn run_auth_command_with_secrets(
    store: &mut ConfigStore,
    command: AuthCommand,
    secrets: &Secrets,
) -> Result<()> {
    match command {
        AuthCommand::Status { provider } => {
            match provider {
                Some(p) => {
                    let provider: ProviderKind = p.into();
                    for line in auth_status_lines_for_provider(store, secrets, provider) {
                        println!("{line}");
                    }
                }
                None => {
                    for line in auth_status_all_providers(store, secrets) {
                        println!("{line}");
                    }
                }
            }
            Ok(())
        }
        AuthCommand::Set {
            provider,
            api_key,
            api_key_stdin,
        } => {
            let provider: ProviderKind = provider.into();
            let slot = provider_slot(provider);
            if provider == ProviderKind::Ollama && api_key.is_none() && !api_key_stdin {
                let provider_cfg = store.config.providers.for_provider_mut(provider);
                if provider_cfg.base_url.is_none() {
                    provider_cfg.base_url = Some("http://localhost:11434/v1".to_string());
                }
                store.save()?;
                println!(
                    "configured {slot} provider in {} (API key optional)",
                    store.path().display()
                );
                return Ok(());
            }
            let api_key = match (api_key, api_key_stdin) {
                (Some(v), _) => v,
                (None, true) => read_api_key_from_stdin()?,
                (None, false) => prompt_api_key(slot)?,
            };
            write_provider_api_key_to_config(store, provider, &api_key);
            let keyring_saved = write_provider_api_key_to_keyring(secrets, provider, &api_key);
            store.save()?;
            // Don't print the key. Don't echo length.
            if keyring_saved {
                println!(
                    "saved API key for {slot} to {} and {}",
                    store.path().display(),
                    secrets.backend_name()
                );
            } else {
                println!("saved API key for {slot} to {}", store.path().display());
            }
            Ok(())
        }
        AuthCommand::Get { provider } => {
            let provider: ProviderKind = provider.into();
            let slot = provider_slot(provider);
            let in_file = provider_config_set(store, provider);
            let in_keyring = !in_file && provider_keyring_set(secrets, provider);
            let in_env = provider_env_set(provider);
            // Report the highest-priority source that has it.
            let source = if in_file {
                Some("config-file")
            } else if in_keyring {
                Some("secret-store")
            } else if in_env {
                Some("env")
            } else {
                None
            };
            match source {
                Some(source) => println!("{slot}: set (source: {source})"),
                None => println!("{slot}: not set"),
            }
            Ok(())
        }
        AuthCommand::Clear { provider } => {
            let provider: ProviderKind = provider.into();
            let slot = provider_slot(provider);
            clear_provider_api_key_from_config(store, provider);
            clear_provider_api_key_from_keyring(secrets, provider);
            store.save()?;
            println!("cleared API key for {slot} from config and secret store");
            Ok(())
        }
        AuthCommand::List => {
            println!("provider     config store env  active");
            for provider in PROVIDER_LIST {
                let slot = provider_slot(provider);
                let file = provider_config_set(store, provider);
                let keyring = (!file).then(|| provider_keyring_set(secrets, provider));
                let env = provider_env_set(provider);
                let active = if file {
                    "config"
                } else if keyring == Some(true) {
                    "store"
                } else if env {
                    "env"
                } else {
                    "missing"
                };
                println!(
                    "{slot:<12}  {}     {}      {}   {active}",
                    yes_no(file),
                    keyring_status_short(keyring),
                    yes_no(env)
                );
            }
            Ok(())
        }
        AuthCommand::Migrate { dry_run } => run_auth_migrate(store, secrets, dry_run),
    }
}

fn yes_no(b: bool) -> &'static str {
    if b { "yes" } else { "no " }
}

fn keyring_status_short(state: Option<bool>) -> &'static str {
    match state {
        Some(true) => "yes",
        Some(false) => "no ",
        None => "n/a",
    }
}

fn prompt_api_key(slot: &str) -> Result<String> {
    use std::io::{IsTerminal, Write};
    eprint!("Enter API key for {slot}: ");
    io::stderr().flush().ok();
    if !io::stdin().is_terminal() {
        // Non-interactive: read directly without prompting twice.
        return read_api_key_from_stdin();
    }
    let mut buf = String::new();
    io::stdin()
        .read_line(&mut buf)
        .context("failed to read API key from stdin")?;
    let key = buf.trim().to_string();
    if key.is_empty() {
        bail!("empty API key provided");
    }
    Ok(key)
}

/// Move plaintext keys from config.toml into the configured secret store.
/// Hidden in v0.8.8 because the normal setup path is config/env only.
fn run_auth_migrate(store: &mut ConfigStore, secrets: &Secrets, dry_run: bool) -> Result<()> {
    let mut migrated: Vec<(ProviderKind, &'static str)> = Vec::new();
    let mut warnings: Vec<String> = Vec::new();

    for provider in PROVIDER_LIST {
        let slot = provider_slot(provider);
        let from_provider_block = store
            .config
            .providers
            .for_provider(provider)
            .api_key
            .clone()
            .filter(|v| !v.trim().is_empty());
        let from_root = (provider == ProviderKind::Deepseek)
            .then(|| store.config.api_key.clone())
            .flatten()
            .filter(|v| !v.trim().is_empty());
        let value = from_provider_block.or(from_root);
        let Some(value) = value else { continue };

        if let Ok(Some(existing)) = secrets.get(slot)
            && existing == value
        {
            // Already migrated; safe to strip the file slot.
        } else if dry_run {
            migrated.push((provider, slot));
            continue;
        } else if let Err(err) = secrets.set(slot, &value) {
            warnings.push(format!(
                "skipped {slot}: failed to write to secret store: {err}"
            ));
            continue;
        }
        if !dry_run {
            store.config.providers.for_provider_mut(provider).api_key = None;
            if provider == ProviderKind::Deepseek {
                store.config.api_key = None;
            }
        }
        migrated.push((provider, slot));
    }

    if !dry_run && !migrated.is_empty() {
        store
            .save()
            .context("failed to write updated config.toml")?;
    }

    println!("secret store backend: {}", secrets.backend_name());
    if migrated.is_empty() {
        println!("nothing to migrate (config.toml has no plaintext api_key entries)");
    } else {
        println!(
            "{} {} provider key(s):",
            if dry_run { "would migrate" } else { "migrated" },
            migrated.len()
        );
        for (_, slot) in &migrated {
            println!("  - {slot}");
        }
        if !dry_run {
            println!(
                "config.toml at {} no longer contains api_key entries for migrated providers.",
                store.path().display()
            );
        }
    }
    for w in warnings {
        eprintln!("warning: {w}");
    }
    Ok(())
}

fn run_config_command(store: &mut ConfigStore, command: ConfigCommand) -> Result<()> {
    match command {
        ConfigCommand::Get { key } => {
            if let Some(value) = store.config.get_display_value(&key) {
                println!("{value}");
                return Ok(());
            }
            bail!("key not found: {key}");
        }
        ConfigCommand::Set { key, value } => {
            store.config.set_value(&key, &value)?;
            store.save()?;
            println!("set {key}");
            Ok(())
        }
        ConfigCommand::Unset { key } => {
            store.config.unset_value(&key)?;
            store.save()?;
            println!("unset {key}");
            Ok(())
        }
        ConfigCommand::List => {
            for (key, value) in store.config.list_values() {
                println!("{key} = {value}");
            }
            Ok(())
        }
        ConfigCommand::Path => {
            println!("{}", store.path().display());
            Ok(())
        }
    }
}

fn model_command_provider_hint(
    command_provider: Option<ProviderArg>,
    top_level_provider: Option<ProviderKind>,
) -> Option<ProviderKind> {
    command_provider
        .map(ProviderKind::from)
        .or(top_level_provider)
}

fn run_model_command(
    command: ModelCommand,
    top_level_provider: Option<ProviderKind>,
) -> Result<()> {
    let registry = ModelRegistry::default();
    match command {
        ModelCommand::List { provider } => {
            let filter = model_command_provider_hint(provider, top_level_provider);
            for model in registry.list().into_iter().filter(|m| match filter {
                Some(p) => m.provider == p,
                None => true,
            }) {
                println!("{} ({})", model.id, model.provider.as_str());
            }
            Ok(())
        }
        ModelCommand::Resolve { model, provider } => {
            let provider = model_command_provider_hint(provider, top_level_provider);
            let resolved = registry.resolve(model.as_deref(), provider);
            println!("requested: {}", resolved.requested.unwrap_or_default());
            println!("resolved: {}", resolved.resolved.id);
            println!("provider: {}", resolved.resolved.provider.as_str());
            println!("used_fallback: {}", resolved.used_fallback);
            Ok(())
        }
    }
}

fn run_thread_command(command: ThreadCommand) -> Result<()> {
    let state = StateStore::open(None)?;
    match command {
        ThreadCommand::List { all, limit } => {
            let threads = state.list_threads(ThreadListFilters {
                include_archived: all,
                limit,
            })?;
            for thread in threads {
                println!(
                    "{} | {} | {} | {}",
                    thread.id,
                    thread
                        .name
                        .clone()
                        .unwrap_or_else(|| "(unnamed)".to_string()),
                    thread.model_provider,
                    thread.cwd.display()
                );
            }
            Ok(())
        }
        ThreadCommand::Read { thread_id } => {
            let thread = state.get_thread(&thread_id)?;
            println!("{}", serde_json::to_string_pretty(&thread)?);
            Ok(())
        }
        ThreadCommand::Resume { thread_id } => {
            let args = vec!["resume".to_string(), thread_id];
            delegate_simple_tui(args)
        }
        ThreadCommand::Fork { thread_id } => {
            let args = vec!["fork".to_string(), thread_id];
            delegate_simple_tui(args)
        }
        ThreadCommand::Archive { thread_id } => {
            state.mark_archived(&thread_id)?;
            println!("archived {thread_id}");
            Ok(())
        }
        ThreadCommand::Unarchive { thread_id } => {
            state.mark_unarchived(&thread_id)?;
            println!("unarchived {thread_id}");
            Ok(())
        }
        ThreadCommand::SetName { thread_id, name } => {
            let mut thread = state
                .get_thread(&thread_id)?
                .with_context(|| format!("thread not found: {thread_id}"))?;
            thread.name = Some(name);
            thread.updated_at = chrono::Utc::now().timestamp();
            state.upsert_thread(&thread)?;
            println!("renamed {thread_id}");
            Ok(())
        }
        ThreadCommand::ClearName { thread_id } => {
            let mut thread = state
                .get_thread(&thread_id)?
                .with_context(|| format!("thread not found: {thread_id}"))?;
            thread.name = None;
            thread.updated_at = chrono::Utc::now().timestamp();
            state.upsert_thread(&thread)?;
            println!("cleared name for {thread_id}");
            Ok(())
        }
    }
}

fn run_sandbox_command(command: SandboxCommand) -> Result<()> {
    match command {
        SandboxCommand::Check { command, ask } => {
            let engine = ExecPolicyEngine::new(Vec::new(), vec!["rm -rf".to_string()]);
            let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
            let decision = engine.check(ExecPolicyContext {
                command: &command,
                cwd: &cwd.display().to_string(),
                tool: Some("exec_shell"),
                path: None,
                ask_for_approval: ask.into(),
                sandbox_mode: Some("workspace-write"),
            })?;
            println!("{}", serde_json::to_string_pretty(&decision)?);
            Ok(())
        }
    }
}

fn run_app_server_command(args: AppServerArgs) -> Result<()> {
    let runtime = tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()
        .context("failed to create tokio runtime")?;
    if args.stdio {
        return runtime.block_on(run_app_server_stdio(args.config));
    }
    let listen: SocketAddr = format!("{}:{}", args.host, args.port)
        .parse()
        .with_context(|| {
            format!(
                "invalid app-server listen address {}:{}",
                args.host, args.port
            )
        })?;
    runtime.block_on(run_app_server(AppServerOptions {
        listen,
        config_path: args.config,
        auth_token: args.auth_token.or_else(app_server_token_from_env),
        insecure_no_auth: args.insecure_no_auth,
        cors_origins: args.cors_origin,
    }))
}

fn app_server_token_from_env() -> Option<String> {
    std::env::var("CODEWHALE_APP_SERVER_TOKEN")
        .ok()
        .or_else(|| std::env::var("DEEPSEEK_APP_SERVER_TOKEN").ok())
}

fn run_mcp_server_command(store: &mut ConfigStore) -> Result<()> {
    let persisted = load_mcp_server_definitions(store);
    let updated = run_stdio_server(persisted)?;
    persist_mcp_server_definitions(store, &updated)
}

fn load_mcp_server_definitions(store: &ConfigStore) -> Vec<McpServerDefinition> {
    let Some(raw) = store.config.get_value(MCP_SERVER_DEFINITIONS_KEY) else {
        return Vec::new();
    };

    match parse_mcp_server_definitions(&raw) {
        Ok(definitions) => definitions,
        Err(err) => {
            eprintln!(
                "warning: failed to parse persisted MCP server definitions ({MCP_SERVER_DEFINITIONS_KEY}): {err}"
            );
            Vec::new()
        }
    }
}

fn parse_mcp_server_definitions(raw: &str) -> Result<Vec<McpServerDefinition>> {
    if let Ok(parsed) = serde_json::from_str::<Vec<McpServerDefinition>>(raw) {
        return Ok(parsed);
    }

    let unwrapped: String = serde_json::from_str(raw)
        .with_context(|| format!("invalid JSON payload at key {MCP_SERVER_DEFINITIONS_KEY}"))?;
    serde_json::from_str::<Vec<McpServerDefinition>>(&unwrapped).with_context(|| {
        format!("invalid MCP server definition list in key {MCP_SERVER_DEFINITIONS_KEY}")
    })
}

fn persist_mcp_server_definitions(
    store: &mut ConfigStore,
    definitions: &[McpServerDefinition],
) -> Result<()> {
    let encoded =
        serde_json::to_string(definitions).context("failed to encode MCP server definitions")?;
    store
        .config
        .set_value(MCP_SERVER_DEFINITIONS_KEY, &encoded)?;
    store.save()
}

fn delegate_to_tui(
    cli: &Cli,
    resolved_runtime: &ResolvedRuntimeOptions,
    passthrough: Vec<String>,
) -> Result<()> {
    let mut cmd = build_tui_command(cli, resolved_runtime, passthrough)?;
    let tui = PathBuf::from(cmd.get_program());
    let status = cmd
        .status()
        .map_err(|err| anyhow!("{}", tui_spawn_error(&tui, &err)))?;
    exit_with_tui_status(status)
}

fn run_resume_command(
    cli: &Cli,
    resolved_runtime: &ResolvedRuntimeOptions,
    args: TuiPassthroughArgs,
) -> Result<()> {
    let passthrough = tui_args("resume", args);
    if should_pick_resume_in_dispatcher(&passthrough, cfg!(windows)) {
        return run_dispatcher_resume_picker(cli, resolved_runtime);
    }
    delegate_to_tui(cli, resolved_runtime, passthrough)
}

fn run_dispatcher_resume_picker(
    cli: &Cli,
    resolved_runtime: &ResolvedRuntimeOptions,
) -> Result<()> {
    let mut sessions_cmd = build_tui_command(cli, resolved_runtime, vec!["sessions".to_string()])?;
    let tui = PathBuf::from(sessions_cmd.get_program());
    let status = sessions_cmd
        .status()
        .map_err(|err| anyhow!("{}", tui_spawn_error(&tui, &err)))?;
    if !status.success() {
        return exit_with_tui_status(status);
    }

    println!();
    println!("Windows note: enter a session id or prefix from the list above.");
    println!("You can also run `codewhale resume --last` to skip this prompt.");
    print!("Session id/prefix (Enter to cancel): ");
    io::stdout().flush()?;

    let mut input = String::new();
    io::stdin()
        .read_line(&mut input)
        .context("failed to read session selection")?;
    let session_id = input.trim();
    if session_id.is_empty() {
        bail!("No session selected.");
    }

    delegate_to_tui(
        cli,
        resolved_runtime,
        vec!["resume".to_string(), session_id.to_string()],
    )
}

fn should_pick_resume_in_dispatcher(passthrough: &[String], is_windows: bool) -> bool {
    is_windows && passthrough == ["resume"]
}

fn build_tui_command(
    cli: &Cli,
    resolved_runtime: &ResolvedRuntimeOptions,
    passthrough: Vec<String>,
) -> Result<Command> {
    let tui = locate_sibling_tui_binary()?;
    let mut verbosity = resolved_runtime.verbosity.clone();
    if verbosity.is_none()
        && passthrough
            .iter()
            .any(|arg| matches!(arg.as_str(), "exec" | "swebench" | "eval"))
    {
        verbosity = Some("concise".to_string());
    }

    let mut cmd = Command::new(&tui);
    if let Some(config) = cli.config.as_ref() {
        cmd.arg("--config").arg(config);
    }
    if let Some(profile) = cli.profile.as_ref() {
        cmd.arg("--profile").arg(profile);
    }
    if let Some(workspace) = cli.workspace.as_ref() {
        cmd.arg("--workspace").arg(workspace);
    }
    // Accepted for older scripts, but no longer forwarded: the interactive TUI
    // always owns the alternate screen to avoid host scrollback hijacking.
    let _ = cli.no_alt_screen;
    if cli.mouse_capture {
        cmd.arg("--mouse-capture");
    }
    if cli.no_mouse_capture {
        cmd.arg("--no-mouse-capture");
    }
    if cli.skip_onboarding {
        cmd.arg("--skip-onboarding");
    }
    cmd.args(passthrough);

    let mut launch_provider_override = cli.provider.map(ProviderKind::from);
    let mut keyring_bridge_provider = resolved_runtime.provider;
    let mut keyring_bridge_api_key = resolved_runtime.api_key.as_ref();
    let mut keyring_bridge_source = resolved_runtime.api_key_source;

    if !provider_is_supported_by_tui(resolved_runtime.provider) {
        let supported = supported_tui_providers_csv();
        match resolved_runtime.provider_source {
            ProviderSource::Cli => {
                bail!(
                    "The interactive TUI does not support provider '{}' from --provider.\n\
                     \n\
                     Supported TUI providers: {supported}.\n\
                     \n\
                     To fix: remove `--provider {}` or pass a supported provider. \
                     For this provider, use `codewhale exec --provider {} \"your prompt\"`.",
                    resolved_runtime.provider.as_str(),
                    resolved_runtime.provider.as_str(),
                    resolved_runtime.provider.as_str(),
                );
            }
            ProviderSource::Env(var) => {
                bail!(
                    "The interactive TUI does not support provider '{}' from {var}.\n\
                     \n\
                     Supported TUI providers: {supported}.\n\
                     \n\
                     To fix: unset {var} or set it to a supported provider. \
                     For this provider, use `codewhale exec --provider {} \"your prompt\"`.",
                    resolved_runtime.provider.as_str(),
                    resolved_runtime.provider.as_str(),
                );
            }
            ProviderSource::Config => {
                let config_hint = cli
                    .config
                    .as_ref()
                    .map(|path| path.display().to_string())
                    .unwrap_or_else(|| "~/.codewhale/config.toml".to_string());
                eprintln!(
                    "Warning: provider '{}' from config is not supported by the interactive TUI; \
                     launching with deepseek instead. Edit {config_hint} and set \
                     provider = \"deepseek\", or pass --provider <supported-id>.",
                    resolved_runtime.provider.as_str(),
                );
                launch_provider_override = Some(ProviderKind::Deepseek);
                keyring_bridge_provider = ProviderKind::Deepseek;
                keyring_bridge_api_key = None;
                keyring_bridge_source = None;
            }
        }
    }

    if let Some(provider) = launch_provider_override {
        cmd.env("DEEPSEEK_PROVIDER", provider.as_str());
    }
    if matches!(keyring_bridge_source, Some(RuntimeApiKeySource::Keyring))
        && let Some(api_key) = keyring_bridge_api_key
    {
        // TUI reloads auth_mode from config/profile, but it does not re-query the
        // platform keyring on normal startup. Bridge only the recovered secret;
        // replaying auth_mode here would turn it back into a profile override.
        cmd.env("DEEPSEEK_API_KEY", api_key);
        for var in provider_env_vars(keyring_bridge_provider) {
            if *var != "DEEPSEEK_API_KEY" {
                cmd.env(var, api_key);
            }
        }
        cmd.env(
            "DEEPSEEK_API_KEY_SOURCE",
            RuntimeApiKeySource::Keyring.as_env_value(),
        );
    }

    if let Some(model) = cli.model.as_ref() {
        cmd.env("DEEPSEEK_MODEL", model);
    }
    if let Some(output_mode) = cli.output_mode.as_ref() {
        cmd.env("DEEPSEEK_OUTPUT_MODE", output_mode);
    }
    if let Some(v) = verbosity.as_ref() {
        cmd.env("CODEWHALE_VERBOSITY", v);
        cmd.env("DEEPSEEK_VERBOSITY", v);
    }
    if let Some(log_level) = cli.log_level.as_ref() {
        cmd.env("DEEPSEEK_LOG_LEVEL", log_level);
    }
    if let Some(telemetry) = cli.telemetry {
        cmd.env("DEEPSEEK_TELEMETRY", telemetry.to_string());
    }
    if let Some(policy) = cli.approval_policy.as_ref() {
        cmd.env("DEEPSEEK_APPROVAL_POLICY", policy);
    }
    if let Some(mode) = cli.sandbox_mode.as_ref() {
        cmd.env("DEEPSEEK_SANDBOX_MODE", mode);
    }
    if cli.yolo {
        cmd.env("DEEPSEEK_YOLO", "true");
    }
    if let Some(api_key) = cli.api_key.as_ref() {
        cmd.env("DEEPSEEK_API_KEY", api_key);
        if resolved_runtime.provider == ProviderKind::Openai {
            cmd.env("OPENAI_API_KEY", api_key);
        }
        if resolved_runtime.provider == ProviderKind::Atlascloud {
            cmd.env("ATLASCLOUD_API_KEY", api_key);
        }
        if resolved_runtime.provider == ProviderKind::WanjieArk {
            cmd.env("WANJIE_ARK_API_KEY", api_key);
        }
        if resolved_runtime.provider == ProviderKind::Volcengine {
            cmd.env("VOLCENGINE_API_KEY", api_key);
        }
        if resolved_runtime.provider == ProviderKind::Siliconflow {
            cmd.env("SILICONFLOW_API_KEY", api_key);
        }
        cmd.env("DEEPSEEK_API_KEY_SOURCE", "cli");
    }
    if let Some(base_url) = cli.base_url.as_ref() {
        cmd.env("DEEPSEEK_BASE_URL", base_url);
    }

    Ok(cmd)
}

fn exit_with_tui_status(status: std::process::ExitStatus) -> Result<()> {
    match status.code() {
        Some(code) => std::process::exit(code),
        None => bail!("codewhale-tui terminated by signal"),
    }
}

fn delegate_simple_tui(args: Vec<String>) -> Result<()> {
    let tui = locate_sibling_tui_binary()?;
    let status = Command::new(&tui)
        .args(args)
        .status()
        .map_err(|err| anyhow!("{}", tui_spawn_error(&tui, &err)))?;
    match status.code() {
        Some(code) => std::process::exit(code),
        None => bail!("codewhale-tui terminated by signal"),
    }
}

fn tui_spawn_error(tui: &Path, err: &io::Error) -> String {
    format!(
        "failed to spawn companion TUI binary at {}: {err}\n\
\n\
The `codewhale` dispatcher found a `codewhale-tui` file, but the OS refused \
to execute it. Common fixes:\n\
  - Reinstall with `npm install -g codewhale`, or run `codewhale update`.\n\
  - On Windows, run `where codewhale` and `where codewhale-tui`; both should \
come from the same install directory.\n\
  - If you downloaded release assets manually, keep both `codewhale` and \
`codewhale-tui` binaries together and make sure the TUI binary is executable.\n\
  - Set DEEPSEEK_TUI_BIN to the absolute path of a working `codewhale-tui` \
binary.",
        tui.display()
    )
}

/// Resolve the sibling `codewhale-tui` executable next to the running
/// dispatcher. Honours platform executable suffix (`.exe` on Windows) so
/// the npm-distributed Windows package — which ships
/// `bin/downloads/codewhale-tui.exe` — is found by `Path::exists` (#247).
///
/// `DEEPSEEK_TUI_BIN` is consulted first as an explicit override for
/// custom installs and CI test layouts. On Windows we additionally try
/// the suffix-less name as a fallback for users who already manually
/// renamed the file before this fix landed.
fn locate_sibling_tui_binary() -> Result<PathBuf> {
    if let Ok(override_path) = std::env::var("DEEPSEEK_TUI_BIN") {
        let candidate = PathBuf::from(override_path);
        if candidate.is_file() {
            return Ok(candidate);
        }
        bail!(
            "DEEPSEEK_TUI_BIN points at {}, which is not a regular file.",
            candidate.display()
        );
    }

    let current = std::env::current_exe().context("failed to locate current executable path")?;
    if let Some(found) = sibling_tui_candidate(&current) {
        return Ok(found);
    }

    // Build a stable error path so the user sees the platform-correct
    // expected name, not "codewhale-tui" on Windows.
    let expected = current.with_file_name(format!("codewhale-tui{}", std::env::consts::EXE_SUFFIX));
    bail!(
        "Companion `codewhale-tui` binary not found at {}.\n\
\n\
The `codewhale` dispatcher delegates interactive sessions to a sibling \
`codewhale-tui` binary. To fix this, install one of:\n\
  • npm:    npm install -g codewhale                (downloads both binaries)\n\
  • cargo:  cargo install codewhale-cli codewhale-tui --locked\n\
  • GitHub Releases: download BOTH `codewhale-<platform>` AND \
`codewhale-tui-<platform>` from https://github.com/Hmbown/CodeWhale/releases/latest \
and place them in the same directory.\n\
\n\
Or set DEEPSEEK_TUI_BIN to the absolute path of an existing `codewhale-tui` binary.",
        expected.display()
    );
}

/// Return the first existing sibling-binary path under any of the names
/// `codewhale-tui` might use on this platform. Pure function to keep
/// `locate_sibling_tui_binary` testable.
fn sibling_tui_candidate(dispatcher: &Path) -> Option<PathBuf> {
    // Primary: platform-correct name. EXE_SUFFIX is "" on Unix and ".exe"
    // on Windows.
    let primary =
        dispatcher.with_file_name(format!("codewhale-tui{}", std::env::consts::EXE_SUFFIX));
    if primary.is_file() {
        return Some(primary);
    }
    // Windows fallback: a user who manually renamed `.exe` away (per the
    // workaround in #247) still launches successfully under the new code.
    if cfg!(windows) {
        let suffixless = dispatcher.with_file_name("codewhale-tui");
        if suffixless.is_file() {
            return Some(suffixless);
        }
    }
    None
}

fn run_metrics_command(args: MetricsArgs) -> Result<()> {
    let since = match args.since.as_deref() {
        Some(s) => {
            Some(metrics::parse_since(s).with_context(|| format!("invalid --since value: {s:?}"))?)
        }
        None => None,
    };
    metrics::run(metrics::MetricsArgs {
        json: args.json,
        since,
    })
}

fn read_api_key_from_stdin() -> Result<String> {
    let mut input = String::new();
    io::stdin()
        .read_to_string(&mut input)
        .context("failed to read api key from stdin")?;
    let key = input.trim().to_string();
    if key.is_empty() {
        bail!("empty API key provided");
    }
    Ok(key)
}

#[cfg(test)]
mod tests {
    use super::*;
    use clap::error::ErrorKind;
    use std::ffi::OsString;
    use std::sync::{Mutex, OnceLock};

    fn parse_ok(argv: &[&str]) -> Cli {
        Cli::try_parse_from(argv).unwrap_or_else(|err| panic!("parse failed for {argv:?}: {err}"))
    }

    fn help_for(argv: &[&str]) -> String {
        let err = Cli::try_parse_from(argv).expect_err("expected --help to short-circuit parsing");
        assert_eq!(err.kind(), ErrorKind::DisplayHelp);
        err.to_string()
    }

    fn command_env(cmd: &Command, name: &str) -> Option<String> {
        let name = std::ffi::OsStr::new(name);
        cmd.get_envs().find_map(|(key, value)| {
            if key == name {
                value.map(|v| v.to_string_lossy().into_owned())
            } else {
                None
            }
        })
    }

    fn env_lock() -> std::sync::MutexGuard<'static, ()> {
        static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
        LOCK.get_or_init(|| Mutex::new(()))
            .lock()
            .unwrap_or_else(|p| p.into_inner())
    }

    struct ScopedEnvVar {
        name: &'static str,
        previous: Option<OsString>,
    }

    impl ScopedEnvVar {
        fn set(name: &'static str, value: &str) -> Self {
            let previous = std::env::var_os(name);
            // Safety: tests using this helper serialize with env_lock() and
            // restore the original value in Drop.
            unsafe { std::env::set_var(name, value) };
            Self { name, previous }
        }
    }

    impl Drop for ScopedEnvVar {
        fn drop(&mut self) {
            // Safety: tests using this helper serialize with env_lock().
            unsafe {
                if let Some(previous) = self.previous.take() {
                    std::env::set_var(self.name, previous);
                } else {
                    std::env::remove_var(self.name);
                }
            }
        }
    }

    fn install_fake_tui_binary() -> (tempfile::TempDir, ScopedEnvVar) {
        let dir = tempfile::TempDir::new().expect("tempdir");
        let custom = dir
            .path()
            .join(format!("custom-tui{}", std::env::consts::EXE_SUFFIX));
        std::fs::write(&custom, b"").unwrap();
        let custom_str = custom.to_string_lossy().into_owned();
        let bin = ScopedEnvVar::set("DEEPSEEK_TUI_BIN", &custom_str);
        (dir, bin)
    }

    fn resolved_runtime_for_test(
        provider: ProviderKind,
        provider_source: ProviderSource,
    ) -> ResolvedRuntimeOptions {
        ResolvedRuntimeOptions {
            provider,
            provider_source,
            model: "test-model".to_string(),
            api_key: None,
            api_key_source: None,
            base_url: "http://localhost:8000/v1".to_string(),
            auth_mode: None,
            insecure_skip_tls_verify: false,
            output_mode: None,
            log_level: None,
            telemetry: false,
            approval_policy: None,
            sandbox_mode: None,
            yolo: None,
            verbosity: None,
            http_headers: std::collections::BTreeMap::new(),
        }
    }

    #[test]
    fn clap_command_definition_is_consistent() {
        Cli::command().debug_assert();
    }

    // Regression for #767: `run_cli` prints the full anyhow chain so users
    // see the underlying TOML parser error (line/column, expected token)
    // instead of just the top-level "failed to parse config at <path>"
    // wrapper. anyhow's bare `Display` impl drops the chain — pin both
    // pieces here so a future refactor of the printing path doesn't
    // silently regress.
    #[test]
    fn anyhow_chain_surfaces_toml_parse_cause() {
        use anyhow::Context;
        let inner = anyhow::anyhow!("TOML parse error at line 1, column 20");
        let err = Err::<(), _>(inner)
            .context("failed to parse config at C:\\Users\\test\\.deepseek\\config.toml")
            .unwrap_err();

        // What `eprintln!("error: {err}")` prints (top context only).
        assert_eq!(
            err.to_string(),
            "failed to parse config at C:\\Users\\test\\.deepseek\\config.toml",
        );

        // What the `for cause in err.chain().skip(1)` loop iterates over.
        let causes: Vec<String> = err.chain().skip(1).map(ToString::to_string).collect();
        assert_eq!(causes, vec!["TOML parse error at line 1, column 20"]);
    }

    #[test]
    fn parses_config_command_matrix() {
        let cli = parse_ok(&["deepseek", "config", "get", "provider"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Config(ConfigArgs {
                command: ConfigCommand::Get { ref key }
            })) if key == "provider"
        ));

        let cli = parse_ok(&["deepseek", "config", "set", "model", "deepseek-v4-flash"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Config(ConfigArgs {
                command: ConfigCommand::Set { ref key, ref value }
            })) if key == "model" && value == "deepseek-v4-flash"
        ));

        let cli = parse_ok(&["deepseek", "config", "unset", "model"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Config(ConfigArgs {
                command: ConfigCommand::Unset { ref key }
            })) if key == "model"
        ));

        assert!(matches!(
            parse_ok(&["deepseek", "config", "list"]).command,
            Some(Commands::Config(ConfigArgs {
                command: ConfigCommand::List
            }))
        ));
        assert!(matches!(
            parse_ok(&["deepseek", "config", "path"]).command,
            Some(Commands::Config(ConfigArgs {
                command: ConfigCommand::Path
            }))
        ));
    }

    #[test]
    fn parses_update_beta_flag() {
        let cli = parse_ok(&["codewhale", "update"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Update(UpdateArgs {
                beta: false,
                check: false,
                proxy: None
            }))
        ));

        let cli = parse_ok(&["codewhale", "update", "--beta"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Update(UpdateArgs {
                beta: true,
                check: false,
                proxy: None
            }))
        ));

        let cli = parse_ok(&["codewhale", "update", "--check"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Update(UpdateArgs {
                beta: false,
                check: true,
                proxy: None
            }))
        ));

        let cli = parse_ok(&["codewhale", "update", "--proxy", "socks5://127.0.0.1:1080"]);
        let Some(Commands::Update(args)) = cli.command else {
            panic!("expected update command");
        };
        assert!(!args.beta);
        assert!(!args.check);
        assert_eq!(args.proxy.as_deref(), Some("socks5://127.0.0.1:1080"));
    }

    #[test]
    fn parses_model_command_matrix() {
        let cli = parse_ok(&["deepseek", "model", "list"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Model(ModelArgs {
                command: ModelCommand::List { provider: None }
            }))
        ));

        let cli = parse_ok(&["deepseek", "model", "list", "--provider", "openai"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Model(ModelArgs {
                command: ModelCommand::List {
                    provider: Some(ProviderArg::Openai)
                }
            }))
        ));

        let cli = parse_ok(&["deepseek", "model", "resolve", "deepseek-v4-flash"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Model(ModelArgs {
                command: ModelCommand::Resolve {
                    model: Some(ref model),
                    provider: None
                }
            })) if model == "deepseek-v4-flash"
        ));

        let cli = parse_ok(&[
            "deepseek",
            "model",
            "resolve",
            "--provider",
            "deepseek",
            "deepseek-v4-pro",
        ]);
        assert!(matches!(
            cli.command,
            Some(Commands::Model(ModelArgs {
                command: ModelCommand::Resolve {
                    model: Some(ref model),
                    provider: Some(ProviderArg::Deepseek)
                }
            })) if model == "deepseek-v4-pro"
        ));
    }

    #[test]
    fn model_command_provider_hint_uses_subcommand_then_top_level_provider() {
        assert_eq!(
            model_command_provider_hint(None, Some(ProviderKind::Zai)),
            Some(ProviderKind::Zai)
        );
        assert_eq!(
            model_command_provider_hint(Some(ProviderArg::Minimax), Some(ProviderKind::Zai)),
            Some(ProviderKind::Minimax)
        );
        assert_eq!(model_command_provider_hint(None, None), None);

        let cli = parse_ok(&["codewhale", "--provider", "zai", "model", "list"]);
        assert_eq!(cli.provider, Some(ProviderArg::Zai));
        assert!(matches!(
            cli.command,
            Some(Commands::Model(ModelArgs {
                command: ModelCommand::List { provider: None }
            }))
        ));
    }

    #[test]
    fn parses_thread_command_matrix() {
        let cli = parse_ok(&["deepseek", "thread", "list", "--all", "--limit", "50"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Thread(ThreadArgs {
                command: ThreadCommand::List {
                    all: true,
                    limit: Some(50)
                }
            }))
        ));

        let cli = parse_ok(&["deepseek", "thread", "read", "thread-1"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Thread(ThreadArgs {
                command: ThreadCommand::Read { ref thread_id }
            })) if thread_id == "thread-1"
        ));

        let cli = parse_ok(&["deepseek", "thread", "resume", "thread-2"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Thread(ThreadArgs {
                command: ThreadCommand::Resume { ref thread_id }
            })) if thread_id == "thread-2"
        ));

        let cli = parse_ok(&["deepseek", "thread", "fork", "thread-3"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Thread(ThreadArgs {
                command: ThreadCommand::Fork { ref thread_id }
            })) if thread_id == "thread-3"
        ));

        let cli = parse_ok(&["deepseek", "thread", "archive", "thread-4"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Thread(ThreadArgs {
                command: ThreadCommand::Archive { ref thread_id }
            })) if thread_id == "thread-4"
        ));

        let cli = parse_ok(&["deepseek", "thread", "unarchive", "thread-5"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Thread(ThreadArgs {
                command: ThreadCommand::Unarchive { ref thread_id }
            })) if thread_id == "thread-5"
        ));

        let cli = parse_ok(&["deepseek", "thread", "set-name", "thread-6", "My Thread"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Thread(ThreadArgs {
                command: ThreadCommand::SetName {
                    ref thread_id,
                    ref name
                }
            })) if thread_id == "thread-6" && name == "My Thread"
        ));

        let cli = parse_ok(&["deepseek", "thread", "clear-name", "thread-7"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Thread(ThreadArgs {
                command: ThreadCommand::ClearName { ref thread_id }
            })) if thread_id == "thread-7"
        ));
    }

    #[test]
    fn parses_sandbox_app_server_and_completion_matrix() {
        let cli = parse_ok(&[
            "deepseek",
            "sandbox",
            "check",
            "echo hello",
            "--ask",
            "on-failure",
        ]);
        assert!(matches!(
            cli.command,
            Some(Commands::Sandbox(SandboxArgs {
                command: SandboxCommand::Check {
                    ref command,
                    ask: ApprovalModeArg::OnFailure
                }
            })) if command == "echo hello"
        ));

        let cli = parse_ok(&[
            "deepseek",
            "app-server",
            "--host",
            "0.0.0.0",
            "--port",
            "9999",
        ]);
        assert!(matches!(
            cli.command,
            Some(Commands::AppServer(AppServerArgs {
                ref host,
                port: 9999,
                stdio: false,
                ..
            })) if host == "0.0.0.0"
        ));

        let cli = parse_ok(&["deepseek", "app-server", "--stdio"]);
        assert!(matches!(
            cli.command,
            Some(Commands::AppServer(AppServerArgs { stdio: true, .. }))
        ));

        let cli = parse_ok(&["deepseek", "completion", "bash"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Completion { shell: Shell::Bash })
        ));
    }

    #[test]
    fn parses_direct_tui_command_aliases() {
        let cli = parse_ok(&["deepseek", "doctor"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Doctor(TuiPassthroughArgs { ref args })) if args.is_empty()
        ));

        let cli = parse_ok(&["deepseek", "models", "--json"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Models(TuiPassthroughArgs { ref args })) if args == &["--json"]
        ));

        let cli = parse_ok(&["deepseek", "resume", "abc123"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Resume(TuiPassthroughArgs { ref args })) if args == &["abc123"]
        ));

        let cli = parse_ok(&["deepseek", "setup", "--skills", "--local"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Setup(TuiPassthroughArgs { ref args }))
                if args == &["--skills", "--local"]
        ));

        let cli = parse_ok(&["codewhale", "fleet", "init"]);
        assert!(cli.prompt.is_empty());
        assert!(matches!(
            cli.command,
            Some(Commands::Fleet(TuiPassthroughArgs { ref args })) if args == &["init"]
        ));

        let cli = parse_ok(&[
            "codewhale",
            "fleet",
            "run",
            "tasks.json",
            "--max-workers",
            "2",
        ]);
        assert!(cli.prompt.is_empty());
        assert!(matches!(
            cli.command,
            Some(Commands::Fleet(TuiPassthroughArgs { ref args }))
                if args == &["run", "tasks.json", "--max-workers", "2"]
        ));
    }

    #[test]
    fn dispatcher_resume_picker_only_handles_bare_windows_resume() {
        assert!(should_pick_resume_in_dispatcher(
            &["resume".to_string()],
            true
        ));
        assert!(!should_pick_resume_in_dispatcher(
            &["resume".to_string(), "--last".to_string()],
            true
        ));
        assert!(!should_pick_resume_in_dispatcher(
            &["resume".to_string(), "abc123".to_string()],
            true
        ));
        assert!(!should_pick_resume_in_dispatcher(
            &["resume".to_string()],
            false
        ));
    }

    #[test]
    fn deepseek_login_writes_shared_config_and_preserves_tui_defaults() {
        let nanos = chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default();
        let path = std::env::temp_dir().join(format!(
            "deepseek-cli-login-test-{}-{nanos}.toml",
            std::process::id()
        ));
        let mut store = ConfigStore::load(Some(path.clone())).expect("store should load");
        let secrets = no_keyring_secrets();

        run_login_command_with_secrets(
            &mut store,
            LoginArgs {
                provider: Some(ProviderArg::Deepseek),
                api_key: Some("sk-test".to_string()),
            },
            &secrets,
        )
        .expect("login should write config");

        assert_eq!(store.config.api_key.as_deref(), Some("sk-test"));
        assert_eq!(
            store.config.providers.deepseek.api_key.as_deref(),
            Some("sk-test")
        );
        assert_eq!(
            store.config.default_text_model.as_deref(),
            Some("deepseek-v4-pro")
        );
        let saved = std::fs::read_to_string(&path).expect("config should be written");
        assert!(saved.contains("api_key = \"sk-test\""));
        assert!(saved.contains("default_text_model = \"deepseek-v4-pro\""));

        let _ = std::fs::remove_file(path);
    }

    #[test]
    fn parses_auth_subcommand_matrix() {
        let cli = parse_ok(&["deepseek", "auth", "set", "--provider", "deepseek"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Auth(AuthArgs {
                command: AuthCommand::Set {
                    provider: ProviderArg::Deepseek,
                    api_key: None,
                    api_key_stdin: false,
                }
            }))
        ));

        let cli = parse_ok(&[
            "deepseek",
            "auth",
            "set",
            "--provider",
            "openrouter",
            "--api-key-stdin",
        ]);
        assert!(matches!(
            cli.command,
            Some(Commands::Auth(AuthArgs {
                command: AuthCommand::Set {
                    provider: ProviderArg::Openrouter,
                    api_key: None,
                    api_key_stdin: true,
                }
            }))
        ));

        let cli = parse_ok(&["deepseek", "auth", "get", "--provider", "novita"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Auth(AuthArgs {
                command: AuthCommand::Get {
                    provider: ProviderArg::Novita
                }
            }))
        ));

        let cli = parse_ok(&["deepseek", "auth", "clear", "--provider", "nvidia-nim"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Auth(AuthArgs {
                command: AuthCommand::Clear {
                    provider: ProviderArg::NvidiaNim
                }
            }))
        ));

        let cli = parse_ok(&["deepseek", "auth", "set", "--provider", "fireworks"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Auth(AuthArgs {
                command: AuthCommand::Set {
                    provider: ProviderArg::Fireworks,
                    api_key: None,
                    api_key_stdin: false,
                }
            }))
        ));

        let cli = parse_ok(&["deepseek", "auth", "set", "--provider", "siliconflow"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Auth(AuthArgs {
                command: AuthCommand::Set {
                    provider: ProviderArg::Siliconflow,
                    api_key: None,
                    api_key_stdin: false,
                }
            }))
        ));

        let cli = parse_ok(&["deepseek", "auth", "set", "--provider", "arcee"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Auth(AuthArgs {
                command: AuthCommand::Set {
                    provider: ProviderArg::Arcee,
                    api_key: None,
                    api_key_stdin: false,
                }
            }))
        ));

        let cli = parse_ok(&["deepseek", "auth", "set", "--provider", "moonshot"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Auth(AuthArgs {
                command: AuthCommand::Set {
                    provider: ProviderArg::Moonshot,
                    api_key: None,
                    api_key_stdin: false,
                }
            }))
        ));

        let cli = parse_ok(&["deepseek", "auth", "set", "--provider", "wanjie-ark"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Auth(AuthArgs {
                command: AuthCommand::Set {
                    provider: ProviderArg::WanjieArk,
                    api_key: None,
                    api_key_stdin: false,
                }
            }))
        ));

        let cli = parse_ok(&["deepseek", "auth", "get", "--provider", "sglang"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Auth(AuthArgs {
                command: AuthCommand::Get {
                    provider: ProviderArg::Sglang
                }
            }))
        ));

        let cli = parse_ok(&["deepseek", "auth", "get", "--provider", "vllm"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Auth(AuthArgs {
                command: AuthCommand::Get {
                    provider: ProviderArg::Vllm
                }
            }))
        ));

        let cli = parse_ok(&["deepseek", "auth", "set", "--provider", "ollama"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Auth(AuthArgs {
                command: AuthCommand::Set {
                    provider: ProviderArg::Ollama,
                    api_key: None,
                    api_key_stdin: false,
                }
            }))
        ));

        let cli = parse_ok(&["deepseek", "auth", "status", "--provider", "openai-codex"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Auth(AuthArgs {
                command: AuthCommand::Status {
                    provider: Some(ProviderArg::OpenaiCodex)
                }
            }))
        ));

        for (provider, expected) in [
            ("anthropic", ProviderArg::Anthropic),
            ("zai", ProviderArg::Zai),
            ("stepfun", ProviderArg::Stepfun),
            ("minimax", ProviderArg::Minimax),
        ] {
            let cli = parse_ok(&[
                "deepseek",
                "auth",
                "set",
                "--provider",
                provider,
                "--api-key-stdin",
            ]);
            assert!(matches!(
                cli.command,
                Some(Commands::Auth(AuthArgs {
                    command: AuthCommand::Set {
                        provider,
                        api_key: None,
                        api_key_stdin: true,
                    }
                })) if provider == expected
            ));
        }

        let cli = parse_ok(&["deepseek", "auth", "list"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Auth(AuthArgs {
                command: AuthCommand::List
            }))
        ));

        let cli = parse_ok(&["deepseek", "auth", "migrate"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Auth(AuthArgs {
                command: AuthCommand::Migrate { dry_run: false }
            }))
        ));

        let cli = parse_ok(&["deepseek", "auth", "migrate", "--dry-run"]);
        assert!(matches!(
            cli.command,
            Some(Commands::Auth(AuthArgs {
                command: AuthCommand::Migrate { dry_run: true }
            }))
        ));
    }

    #[test]
    fn auth_set_writes_to_shared_config_file() {
        use codewhale_secrets::{InMemoryKeyringStore, KeyringStore};
        use std::sync::Arc;

        let nanos = chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default();
        let path = std::env::temp_dir().join(format!(
            "deepseek-cli-auth-set-test-{}-{nanos}.toml",
            std::process::id()
        ));
        let mut store = ConfigStore::load(Some(path.clone())).expect("store should load");
        let inner = Arc::new(InMemoryKeyringStore::new());
        let secrets = Secrets::new(inner.clone());

        run_auth_command_with_secrets(
            &mut store,
            AuthCommand::Set {
                provider: ProviderArg::Deepseek,
                api_key: Some("sk-keyring".to_string()),
                api_key_stdin: false,
            },
            &secrets,
        )
        .expect("set should succeed");

        assert_eq!(store.config.api_key.as_deref(), Some("sk-keyring"));
        assert_eq!(
            store.config.providers.deepseek.api_key.as_deref(),
            Some("sk-keyring")
        );
        let saved = std::fs::read_to_string(&path).unwrap_or_default();
        assert!(saved.contains("api_key = \"sk-keyring\""));
        assert_eq!(
            inner.get("deepseek").unwrap().as_deref(),
            Some("sk-keyring")
        );

        let _ = std::fs::remove_file(path);
    }

    #[test]
    fn auth_set_provider_key_does_not_switch_active_provider() {
        let nanos = chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default();
        let path = std::env::temp_dir().join(format!(
            "deepseek-cli-auth-set-preserve-provider-test-{}-{nanos}.toml",
            std::process::id()
        ));
        let mut store = ConfigStore::load(Some(path.clone())).expect("store should load");
        store.config.provider = ProviderKind::Deepseek;
        let secrets = no_keyring_secrets();

        run_auth_command_with_secrets(
            &mut store,
            AuthCommand::Set {
                provider: ProviderArg::Arcee,
                api_key: Some("arcee-key".to_string()),
                api_key_stdin: false,
            },
            &secrets,
        )
        .expect("set should succeed");

        assert_eq!(store.config.provider, ProviderKind::Deepseek);
        assert_eq!(
            store.config.providers.arcee.api_key.as_deref(),
            Some("arcee-key")
        );

        let reloaded = ConfigStore::load(Some(path.clone())).expect("store should reload");
        assert_eq!(reloaded.config.provider, ProviderKind::Deepseek);
        assert_eq!(
            reloaded.config.providers.arcee.api_key.as_deref(),
            Some("arcee-key")
        );

        let _ = std::fs::remove_file(path);
    }

    #[test]
    fn auth_set_ollama_accepts_empty_key_and_records_base_url() {
        let nanos = chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default();
        let path = std::env::temp_dir().join(format!(
            "deepseek-cli-auth-ollama-test-{}-{nanos}.toml",
            std::process::id()
        ));
        let mut store = ConfigStore::load(Some(path.clone())).expect("store should load");
        store.config.provider = ProviderKind::Deepseek;
        let secrets = no_keyring_secrets();

        run_auth_command_with_secrets(
            &mut store,
            AuthCommand::Set {
                provider: ProviderArg::Ollama,
                api_key: None,
                api_key_stdin: false,
            },
            &secrets,
        )
        .expect("ollama auth set should not require a key");

        assert_eq!(store.config.provider, ProviderKind::Deepseek);
        assert_eq!(
            store.config.providers.ollama.base_url.as_deref(),
            Some("http://localhost:11434/v1")
        );
        assert_eq!(store.config.providers.ollama.api_key, None);

        let _ = std::fs::remove_file(path);
    }

    #[test]
    fn auth_clear_removes_from_config() {
        use codewhale_secrets::{InMemoryKeyringStore, KeyringStore};
        use std::sync::Arc;

        let nanos = chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default();
        let path = std::env::temp_dir().join(format!(
            "deepseek-cli-auth-clear-test-{}-{nanos}.toml",
            std::process::id()
        ));
        let mut store = ConfigStore::load(Some(path.clone())).expect("store should load");
        store.config.api_key = Some("sk-stale".to_string());
        store.config.providers.deepseek.api_key = Some("sk-stale".to_string());
        store.save().unwrap();

        let inner = Arc::new(InMemoryKeyringStore::new());
        inner.set("deepseek", "sk-stale").unwrap();
        let secrets = Secrets::new(inner.clone());

        run_auth_command_with_secrets(
            &mut store,
            AuthCommand::Clear {
                provider: ProviderArg::Deepseek,
            },
            &secrets,
        )
        .expect("clear should succeed");

        assert!(store.config.api_key.is_none());
        assert!(store.config.providers.deepseek.api_key.is_none());
        assert_eq!(inner.get("deepseek").unwrap(), None);

        let _ = std::fs::remove_file(path);
    }

    #[test]
    fn auth_status_scoped_probe_and_list_all_provider_keyrings() {
        use codewhale_secrets::{KeyringStore, SecretsError};
        use std::sync::{Arc, Mutex};

        #[derive(Default)]
        struct RecordingStore {
            gets: Mutex<Vec<String>>,
        }

        impl KeyringStore for RecordingStore {
            fn get(&self, key: &str) -> Result<Option<String>, SecretsError> {
                self.gets.lock().unwrap().push(key.to_string());
                Ok(None)
            }

            fn set(&self, _key: &str, _value: &str) -> Result<(), SecretsError> {
                Ok(())
            }

            fn delete(&self, _key: &str) -> Result<(), SecretsError> {
                Ok(())
            }

            fn backend_name(&self) -> &'static str {
                "recording"
            }
        }

        let nanos = chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default();
        let path = std::env::temp_dir().join(format!(
            "deepseek-cli-auth-active-keyring-test-{}-{nanos}.toml",
            std::process::id()
        ));
        let mut store = ConfigStore::load(Some(path.clone())).expect("store should load");
        store.config.provider = ProviderKind::Deepseek;
        let inner = Arc::new(RecordingStore::default());
        let secrets = Secrets::new(inner.clone());

        run_auth_command_with_secrets(
            &mut store,
            AuthCommand::Status {
                provider: Some(ProviderArg::Deepseek),
            },
            &secrets,
        )
        .expect("status should succeed");
        run_auth_command_with_secrets(&mut store, AuthCommand::List, &secrets)
            .expect("list should succeed");

        let probed = inner.gets.lock().unwrap();
        // Scoped status probes only the requested provider.
        assert_eq!(probed[0], "deepseek");
        // List now probes all providers (not just active) to fix the
        // stale keyring-only-for-active-provider bug.
        assert!(probed.len() > 1, "list should probe all providers");
        assert!(
            PROVIDER_LIST
                .iter()
                .all(|p| probed.contains(&provider_slot(*p).to_string())),
            "every known provider should be probed by auth list: {:?}",
            *probed
        );

        let _ = std::fs::remove_file(path);
    }

    #[test]
    fn auth_status_reports_all_active_provider_sources_with_last4() {
        use codewhale_secrets::{InMemoryKeyringStore, KeyringStore};
        use std::sync::Arc;

        let _lock = env_lock();
        let _env = ScopedEnvVar::set("DEEPSEEK_API_KEY", "sk-env-1111");

        let nanos = chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default();
        let path = std::env::temp_dir().join(format!(
            "deepseek-cli-auth-status-table-test-{}-{nanos}.toml",
            std::process::id()
        ));
        let mut store = ConfigStore::load(Some(path.clone())).expect("store should load");
        store.config.provider = ProviderKind::Deepseek;
        store.config.api_key = Some("sk-config-3333".to_string());
        store.config.providers.deepseek.api_key = Some("sk-config-3333".to_string());

        let inner = Arc::new(InMemoryKeyringStore::new());
        inner.set("deepseek", "sk-keyring-2222").unwrap();
        let secrets = Secrets::new(inner);

        let output =
            auth_status_lines_for_provider(&store, &secrets, ProviderKind::Deepseek).join("\n");

        assert!(output.contains("provider: deepseek"));
        assert!(output.contains("active source: config (last4: ...3333)"));
        assert!(output.contains("lookup order: config -> secret store -> env"));
        assert!(output.contains("config file: "));
        assert!(output.contains("set, last4: ...3333"));
        assert!(output.contains("secret store: in-memory (test) (set, last4: ...2222)"));
        assert!(output.contains("env var: DEEPSEEK_API_KEY (set, last4: ...1111)"));
        assert!(!output.contains("sk-config-3333"));
        assert!(!output.contains("sk-keyring-2222"));
        assert!(!output.contains("sk-env-1111"));

        let _ = std::fs::remove_file(path);
    }

    #[test]
    fn auth_status_all_providers_lists_every_known_provider() {
        use codewhale_secrets::{InMemoryKeyringStore, KeyringStore};
        use std::sync::Arc;

        let nanos = chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default();
        let path = std::env::temp_dir().join(format!(
            "deepseek-cli-auth-all-status-test-{}-{nanos}.toml",
            std::process::id()
        ));
        let mut store = ConfigStore::load(Some(path.clone())).expect("store should load");
        store.config.provider = ProviderKind::Deepseek;
        store.config.providers.arcee.api_key = Some("sk-arcee-test1234".to_string());

        let inner = Arc::new(InMemoryKeyringStore::new());
        inner.set("openrouter", "sk-or-test5678").unwrap();
        let secrets = Secrets::new(inner);

        let output = auth_status_all_providers(&store, &secrets).join("\n");

        // Should list all known providers
        assert!(output.contains("deepseek"));
        assert!(output.contains("arcee"));
        assert!(output.contains("openrouter"));
        assert!(output.contains("huggingface"));
        assert!(output.contains("ollama"));

        // Active provider should be marked
        assert!(output.contains("deepseek") && output.contains("*"));

        // Arcee should show config source
        assert!(output.contains("config"));

        // Should NOT leak raw keys
        assert!(!output.contains("sk-arcee-test1234"));
        assert!(!output.contains("sk-or-test5678"));

        let _ = std::fs::remove_file(path);
    }

    #[test]
    fn auth_status_openai_codex_reports_codex_oauth_file() {
        use codewhale_secrets::InMemoryKeyringStore;
        use std::sync::Arc;

        let _lock = env_lock();
        let _access_token = ScopedEnvVar::set("OPENAI_CODEX_ACCESS_TOKEN", "");
        let _codex_token = ScopedEnvVar::set("CODEX_ACCESS_TOKEN", "");

        let dir = tempfile::TempDir::new().expect("tempdir");
        let config_path = dir.path().join("config.toml");
        let auth_path = dir.path().join("auth.json");
        std::fs::write(&auth_path, r#"{"tokens":{"access_token":"secret-token"}}"#)
            .expect("write auth file");
        let auth_path_str = auth_path.to_string_lossy().into_owned();
        let _auth_file = ScopedEnvVar::set("OPENAI_CODEX_AUTH_FILE", &auth_path_str);

        let mut store = ConfigStore::load(Some(config_path)).expect("store should load");
        store.config.provider = ProviderKind::OpenaiCodex;
        let secrets = Secrets::new(Arc::new(InMemoryKeyringStore::new()));

        let output =
            auth_status_lines_for_provider(&store, &secrets, ProviderKind::OpenaiCodex).join("\n");

        assert!(output.contains("provider: openai-codex"));
        assert!(output.contains("auth mode: codex_oauth"));
        assert!(output.contains("active source: Codex OAuth file"));
        assert!(output.contains("lookup order: env -> Codex OAuth file"));
        assert!(output.contains(&format!(
            "Codex OAuth file: {} (present)",
            auth_path.display()
        )));
        assert!(!output.contains("secret-token"));
    }

    #[test]
    fn auth_status_scoped_provider_shows_detailed_info() {
        use codewhale_secrets::InMemoryKeyringStore;
        use std::sync::Arc;

        let nanos = chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default();
        let path = std::env::temp_dir().join(format!(
            "deepseek-cli-auth-scoped-test-{}-{nanos}.toml",
            std::process::id()
        ));
        let mut store = ConfigStore::load(Some(path.clone())).expect("store should load");
        store.config.provider = ProviderKind::Deepseek;
        store.config.providers.arcee.api_key = Some("sk-arcee-9999".to_string());

        let secrets = Secrets::new(Arc::new(InMemoryKeyringStore::new()));

        let output =
            auth_status_lines_for_provider(&store, &secrets, ProviderKind::Arcee).join("\n");

        assert!(output.contains("provider: arcee"));
        assert!(output.contains("active source: config (last4: ...9999)"));
        assert!(output.contains("route:"));
        assert!(output.contains("model:"));
        assert!(!output.contains("sk-arcee-9999"));

        let _ = std::fs::remove_file(path);
    }

    #[test]
    fn dispatch_keyring_recovery_self_heals_into_config_file() {
        use codewhale_secrets::{InMemoryKeyringStore, KeyringStore};
        use std::sync::Arc;

        let nanos = chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default();
        let path = std::env::temp_dir().join(format!(
            "deepseek-cli-dispatch-keyring-heal-test-{}-{nanos}.toml",
            std::process::id()
        ));
        let mut store = ConfigStore::load(Some(path.clone())).expect("store should load");
        let inner = Arc::new(InMemoryKeyringStore::new());
        inner.set("deepseek", "ring-key").unwrap();
        let secrets = Secrets::new(inner);

        let resolved = resolve_runtime_for_dispatch_with_secrets(
            &mut store,
            &CliRuntimeOverrides::default(),
            &secrets,
        );

        assert_eq!(resolved.api_key.as_deref(), Some("ring-key"));
        assert_eq!(
            resolved.api_key_source,
            Some(RuntimeApiKeySource::ConfigFile)
        );
        assert_eq!(store.config.api_key.as_deref(), Some("ring-key"));
        assert_eq!(
            store.config.providers.deepseek.api_key.as_deref(),
            Some("ring-key")
        );

        let saved = std::fs::read_to_string(&path).expect("config should be written");
        assert!(saved.contains("api_key = \"ring-key\""));

        let resolved_again = resolve_runtime_for_dispatch_with_secrets(
            &mut store,
            &CliRuntimeOverrides::default(),
            &no_keyring_secrets(),
        );
        assert_eq!(resolved_again.api_key.as_deref(), Some("ring-key"));
        assert_eq!(
            resolved_again.api_key_source,
            Some(RuntimeApiKeySource::ConfigFile)
        );

        let _ = std::fs::remove_file(path);
    }

    #[test]
    fn logout_removes_plaintext_provider_keys() {
        let nanos = chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default();
        let path = std::env::temp_dir().join(format!(
            "deepseek-cli-logout-test-{}-{nanos}.toml",
            std::process::id()
        ));
        let mut store = ConfigStore::load(Some(path.clone())).expect("store should load");
        store.config.api_key = Some("sk-stale".to_string());
        store.config.providers.deepseek.api_key = Some("sk-stale".to_string());
        store.config.providers.fireworks.api_key = Some("fw-stale".to_string());
        store.save().unwrap();

        let secrets = no_keyring_secrets();

        run_logout_command_with_secrets(&mut store, &secrets).expect("logout should succeed");

        assert!(store.config.api_key.is_none());
        assert!(store.config.providers.deepseek.api_key.is_none());
        assert!(store.config.providers.fireworks.api_key.is_none());

        let _ = std::fs::remove_file(path);
    }

    #[test]
    fn auth_migrate_moves_plaintext_keys_into_keyring_and_strips_file() {
        use codewhale_secrets::{InMemoryKeyringStore, KeyringStore};
        use std::sync::Arc;

        let nanos = chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default();
        let path = std::env::temp_dir().join(format!(
            "deepseek-cli-auth-migrate-test-{}-{nanos}.toml",
            std::process::id()
        ));
        let mut store = ConfigStore::load(Some(path.clone())).expect("store should load");
        store.config.api_key = Some("sk-deep".to_string());
        store.config.providers.deepseek.api_key = Some("sk-deep".to_string());
        store.config.providers.openrouter.api_key = Some("or-key".to_string());
        store.config.providers.novita.api_key = Some("nv-key".to_string());
        store.save().unwrap();

        let inner = Arc::new(InMemoryKeyringStore::new());
        let secrets = Secrets::new(inner.clone());

        run_auth_command_with_secrets(
            &mut store,
            AuthCommand::Migrate { dry_run: false },
            &secrets,
        )
        .expect("migrate should succeed");

        assert_eq!(inner.get("deepseek").unwrap(), Some("sk-deep".to_string()));
        assert_eq!(inner.get("openrouter").unwrap(), Some("or-key".to_string()));
        assert_eq!(inner.get("novita").unwrap(), Some("nv-key".to_string()));

        // Config file must no longer contain the api keys.
        assert!(store.config.api_key.is_none());
        assert!(store.config.providers.deepseek.api_key.is_none());
        assert!(store.config.providers.openrouter.api_key.is_none());
        assert!(store.config.providers.novita.api_key.is_none());

        let saved = std::fs::read_to_string(&path).expect("config exists post-migrate");
        assert!(!saved.contains("sk-deep"), "plaintext leaked: {saved}");
        assert!(!saved.contains("or-key"), "plaintext leaked: {saved}");
        assert!(!saved.contains("nv-key"), "plaintext leaked: {saved}");

        let _ = std::fs::remove_file(path);
    }

    #[test]
    fn auth_migrate_dry_run_does_not_modify_anything() {
        use codewhale_secrets::{InMemoryKeyringStore, KeyringStore};
        use std::sync::Arc;

        let nanos = chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default();
        let path = std::env::temp_dir().join(format!(
            "deepseek-cli-auth-migrate-dry-{}-{nanos}.toml",
            std::process::id()
        ));
        let mut store = ConfigStore::load(Some(path.clone())).expect("store should load");
        store.config.providers.openrouter.api_key = Some("or-stay".to_string());
        store.save().unwrap();

        let inner = Arc::new(InMemoryKeyringStore::new());
        let secrets = Secrets::new(inner.clone());

        run_auth_command_with_secrets(&mut store, AuthCommand::Migrate { dry_run: true }, &secrets)
            .expect("dry-run should succeed");

        assert_eq!(inner.get("openrouter").unwrap(), None);
        assert_eq!(
            store.config.providers.openrouter.api_key.as_deref(),
            Some("or-stay")
        );

        let _ = std::fs::remove_file(path);
    }

    #[test]
    fn parses_global_override_flags() {
        let cli = parse_ok(&[
            "deepseek",
            "--provider",
            "openai",
            "--config",
            "/tmp/deepseek.toml",
            "--profile",
            "work",
            "--model",
            "deepseek-v4-pro",
            "--output-mode",
            "json",
            "--verbosity",
            "concise",
            "--log-level",
            "debug",
            "--telemetry",
            "true",
            "--approval-policy",
            "on-request",
            "--sandbox-mode",
            "workspace-write",
            "--base-url",
            "https://openai-compatible.example/v1",
            "--api-key",
            "sk-test",
            "--workspace",
            "/tmp/workspace",
            "--no-alt-screen",
            "--no-mouse-capture",
            "--skip-onboarding",
            "model",
            "resolve",
            "deepseek-v4-pro",
        ]);

        assert!(matches!(cli.provider, Some(ProviderArg::Openai)));
        assert_eq!(cli.config, Some(PathBuf::from("/tmp/deepseek.toml")));
        assert_eq!(cli.profile.as_deref(), Some("work"));
        assert_eq!(cli.model.as_deref(), Some("deepseek-v4-pro"));
        assert_eq!(cli.output_mode.as_deref(), Some("json"));
        assert_eq!(cli.verbosity.as_deref(), Some("concise"));
        assert_eq!(cli.log_level.as_deref(), Some("debug"));
        assert_eq!(cli.telemetry, Some(true));
        assert_eq!(cli.approval_policy.as_deref(), Some("on-request"));
        assert_eq!(cli.sandbox_mode.as_deref(), Some("workspace-write"));
        assert_eq!(
            cli.base_url.as_deref(),
            Some("https://openai-compatible.example/v1")
        );
        assert_eq!(cli.api_key.as_deref(), Some("sk-test"));
        assert_eq!(cli.workspace, Some(PathBuf::from("/tmp/workspace")));
        assert!(cli.no_alt_screen);
        assert!(cli.no_mouse_capture);
        assert!(!cli.mouse_capture);
        assert!(cli.skip_onboarding);
    }

    #[test]
    fn build_tui_command_allows_openai_and_forwards_provider_key() {
        let _lock = env_lock();
        let dir = tempfile::TempDir::new().expect("tempdir");
        let custom = dir
            .path()
            .join(format!("custom-tui{}", std::env::consts::EXE_SUFFIX));
        std::fs::write(&custom, b"").unwrap();
        let custom_str = custom.to_string_lossy().into_owned();
        let _bin = ScopedEnvVar::set("DEEPSEEK_TUI_BIN", &custom_str);

        let cli = parse_ok(&[
            "deepseek",
            "--provider",
            "openai",
            "--workspace",
            "/tmp/codewhale-workspace",
        ]);
        let resolved = ResolvedRuntimeOptions {
            provider: ProviderKind::Openai,
            provider_source: ProviderSource::Cli,
            model: "glm-5".to_string(),
            api_key: Some("resolved-openai-key".to_string()),
            api_key_source: Some(RuntimeApiKeySource::Keyring),
            base_url: "https://openai-compatible.example/v4".to_string(),
            auth_mode: Some("api_key".to_string()),
            insecure_skip_tls_verify: false,
            output_mode: None,
            log_level: None,
            telemetry: false,
            approval_policy: None,
            sandbox_mode: None,
            yolo: None,
            verbosity: None,
            http_headers: std::collections::BTreeMap::new(),
        };

        let cmd = build_tui_command(&cli, &resolved, Vec::new()).expect("command");
        assert_eq!(
            command_env(&cmd, "DEEPSEEK_PROVIDER").as_deref(),
            Some("openai")
        );
        assert_eq!(
            command_env(&cmd, "DEEPSEEK_API_KEY").as_deref(),
            Some("resolved-openai-key")
        );
        assert_eq!(
            command_env(&cmd, "OPENAI_API_KEY").as_deref(),
            Some("resolved-openai-key")
        );
        assert_eq!(
            command_env(&cmd, "DEEPSEEK_API_KEY_SOURCE").as_deref(),
            Some("keyring")
        );
        assert_eq!(command_env(&cmd, "DEEPSEEK_AUTH_MODE"), None);
        let args: Vec<String> = cmd
            .get_args()
            .map(|arg| arg.to_string_lossy().into_owned())
            .collect();
        assert!(
            args.windows(2)
                .any(|pair| pair == ["--workspace", "/tmp/codewhale-workspace"]),
            "expected workspace forwarding in args: {args:?}"
        );
    }

    #[test]
    fn build_tui_command_allows_openai_codex_from_resolved_runtime() {
        let _lock = env_lock();
        let dir = tempfile::TempDir::new().expect("tempdir");
        let custom = dir
            .path()
            .join(format!("custom-tui{}", std::env::consts::EXE_SUFFIX));
        std::fs::write(&custom, b"").unwrap();
        let custom_str = custom.to_string_lossy().into_owned();
        let _bin = ScopedEnvVar::set("DEEPSEEK_TUI_BIN", &custom_str);

        let cli = parse_ok(&["codewhale", "doctor"]);
        let resolved = ResolvedRuntimeOptions {
            provider: ProviderKind::OpenaiCodex,
            provider_source: ProviderSource::Config,
            model: "gpt-5.5".to_string(),
            api_key: None,
            api_key_source: None,
            base_url: "https://chatgpt.com/backend-api".to_string(),
            auth_mode: Some("oauth".to_string()),
            insecure_skip_tls_verify: false,
            output_mode: None,
            log_level: None,
            telemetry: false,
            approval_policy: None,
            sandbox_mode: None,
            yolo: None,
            verbosity: None,
            http_headers: std::collections::BTreeMap::new(),
        };

        let cmd = build_tui_command(&cli, &resolved, vec!["doctor".to_string()])
            .expect("openai-codex should be accepted by the facade");
        assert_eq!(command_env(&cmd, "DEEPSEEK_PROVIDER"), None);
        let args: Vec<String> = cmd
            .get_args()
            .map(|arg| arg.to_string_lossy().into_owned())
            .collect();
        assert_eq!(args, vec!["doctor"]);
    }

    #[test]
    fn build_tui_command_forwards_explicit_openai_codex_provider() {
        let _lock = env_lock();
        let dir = tempfile::TempDir::new().expect("tempdir");
        let custom = dir
            .path()
            .join(format!("custom-tui{}", std::env::consts::EXE_SUFFIX));
        std::fs::write(&custom, b"").unwrap();
        let custom_str = custom.to_string_lossy().into_owned();
        let _bin = ScopedEnvVar::set("DEEPSEEK_TUI_BIN", &custom_str);

        let cli = parse_ok(&["codewhale", "--provider", "openai-codex", "doctor"]);
        let resolved = ResolvedRuntimeOptions {
            provider: ProviderKind::OpenaiCodex,
            provider_source: ProviderSource::Cli,
            model: "gpt-5.5".to_string(),
            api_key: None,
            api_key_source: None,
            base_url: "https://chatgpt.com/backend-api".to_string(),
            auth_mode: Some("oauth".to_string()),
            insecure_skip_tls_verify: false,
            output_mode: None,
            log_level: None,
            telemetry: false,
            approval_policy: None,
            sandbox_mode: None,
            yolo: None,
            verbosity: None,
            http_headers: std::collections::BTreeMap::new(),
        };

        let cmd = build_tui_command(&cli, &resolved, vec!["doctor".to_string()])
            .expect("openai-codex should be accepted by the facade");
        assert_eq!(
            command_env(&cmd, "DEEPSEEK_PROVIDER").as_deref(),
            Some("openai-codex")
        );
    }

    #[test]
    fn build_tui_command_rejects_unsupported_cli_provider_with_flag_hint() {
        let _lock = env_lock();
        let (_dir, _bin) = install_fake_tui_binary();

        let cli = parse_ok(&["codewhale", "doctor"]);
        let resolved = resolved_runtime_for_test(ProviderKind::Anthropic, ProviderSource::Cli);

        let err = build_tui_command(&cli, &resolved, vec!["doctor".to_string()])
            .expect_err("unsupported provider should fail");
        let msg = err.to_string();

        assert!(msg.contains("from --provider"), "{msg}");
        assert!(msg.contains("remove `--provider anthropic`"), "{msg}");
        assert!(msg.contains("Supported TUI providers:"), "{msg}");
    }

    #[test]
    fn build_tui_command_rejects_unsupported_env_provider_with_env_hint() {
        let _lock = env_lock();
        let (_dir, _bin) = install_fake_tui_binary();

        let cli = parse_ok(&["codewhale", "doctor"]);
        let resolved = resolved_runtime_for_test(
            ProviderKind::Anthropic,
            ProviderSource::Env("DEEPSEEK_PROVIDER"),
        );

        let err = build_tui_command(&cli, &resolved, vec!["doctor".to_string()])
            .expect_err("unsupported provider should fail");
        let msg = err.to_string();

        assert!(msg.contains("from DEEPSEEK_PROVIDER"), "{msg}");
        assert!(msg.contains("unset DEEPSEEK_PROVIDER"), "{msg}");
        assert!(msg.contains("Supported TUI providers:"), "{msg}");
    }

    #[test]
    fn build_tui_command_config_fallback_does_not_forward_stale_keyring_secret() {
        let _lock = env_lock();
        let (_dir, _bin) = install_fake_tui_binary();

        let cli = parse_ok(&["codewhale", "doctor"]);
        let mut resolved =
            resolved_runtime_for_test(ProviderKind::Anthropic, ProviderSource::Config);
        resolved.api_key = Some("anthropic-keyring-secret".to_string());
        resolved.api_key_source = Some(RuntimeApiKeySource::Keyring);

        let cmd = build_tui_command(&cli, &resolved, vec!["doctor".to_string()])
            .expect("config-sourced unsupported provider should fall back");

        assert_eq!(
            command_env(&cmd, "DEEPSEEK_PROVIDER").as_deref(),
            Some("deepseek")
        );
        assert_eq!(command_env(&cmd, "DEEPSEEK_API_KEY"), None);
        assert_eq!(command_env(&cmd, "ANTHROPIC_API_KEY"), None);
        assert_eq!(command_env(&cmd, "DEEPSEEK_API_KEY_SOURCE"), None);
    }

    #[test]
    fn build_tui_command_does_not_export_default_runtime_overrides_for_profiles() {
        let _lock = env_lock();
        let dir = tempfile::TempDir::new().expect("tempdir");
        let custom = dir
            .path()
            .join(format!("custom-tui{}", std::env::consts::EXE_SUFFIX));
        std::fs::write(&custom, b"").unwrap();
        let custom_str = custom.to_string_lossy().into_owned();
        let _bin = ScopedEnvVar::set("DEEPSEEK_TUI_BIN", &custom_str);

        let cli = parse_ok(&["deepseek", "--profile", "google"]);
        let mut resolved_headers = std::collections::BTreeMap::new();
        resolved_headers.insert("X-From-Base".to_string(), "base".to_string());
        let resolved = ResolvedRuntimeOptions {
            provider: ProviderKind::Deepseek,
            provider_source: ProviderSource::Config,
            model: "deepseek-v4-pro".to_string(),
            api_key: Some("config-file-key".to_string()),
            api_key_source: Some(RuntimeApiKeySource::ConfigFile),
            base_url: "https://api.deepseek.com/beta".to_string(),
            auth_mode: Some("api_key".to_string()),
            insecure_skip_tls_verify: false,
            output_mode: None,
            log_level: None,
            telemetry: false,
            approval_policy: None,
            sandbox_mode: None,
            yolo: None,
            verbosity: None,
            http_headers: resolved_headers,
        };

        let cmd = build_tui_command(&cli, &resolved, Vec::new()).expect("command");

        assert_eq!(command_env(&cmd, "DEEPSEEK_PROVIDER"), None);
        assert_eq!(command_env(&cmd, "DEEPSEEK_MODEL"), None);
        assert_eq!(command_env(&cmd, "DEEPSEEK_BASE_URL"), None);
        assert_eq!(command_env(&cmd, "DEEPSEEK_API_KEY"), None);
        assert_eq!(command_env(&cmd, "DEEPSEEK_API_KEY_SOURCE"), None);
        assert_eq!(command_env(&cmd, "DEEPSEEK_AUTH_MODE"), None);
        assert_eq!(command_env(&cmd, "DEEPSEEK_HTTP_HEADERS"), None);
        let args: Vec<String> = cmd
            .get_args()
            .map(|arg| arg.to_string_lossy().into_owned())
            .collect();
        assert!(
            args.windows(2).any(|pair| pair == ["--profile", "google"]),
            "expected profile forwarding in args: {args:?}"
        );
    }

    #[test]
    fn build_tui_command_defaults_noninteractive_to_concise_verbosity() {
        let _lock = env_lock();
        let (_dir, _bin) = install_fake_tui_binary();

        let cli = parse_ok(&["codewhale"]);
        let resolved = resolved_runtime_for_test(ProviderKind::Deepseek, ProviderSource::Config);

        let cmd = build_tui_command(
            &cli,
            &resolved,
            vec!["exec".to_string(), "summarize".to_string()],
        )
        .expect("command");

        assert_eq!(
            command_env(&cmd, "CODEWHALE_VERBOSITY").as_deref(),
            Some("concise")
        );
        assert_eq!(
            command_env(&cmd, "DEEPSEEK_VERBOSITY").as_deref(),
            Some("concise")
        );
    }

    #[test]
    fn build_tui_command_respects_resolved_verbosity_override() {
        let _lock = env_lock();
        let (_dir, _bin) = install_fake_tui_binary();

        let cli = parse_ok(&["codewhale"]);
        let mut resolved =
            resolved_runtime_for_test(ProviderKind::Deepseek, ProviderSource::Config);
        resolved.verbosity = Some("normal".to_string());

        let cmd = build_tui_command(&cli, &resolved, vec!["exec".to_string()]).expect("command");

        assert_eq!(
            command_env(&cmd, "CODEWHALE_VERBOSITY").as_deref(),
            Some("normal")
        );
        assert_eq!(
            command_env(&cmd, "DEEPSEEK_VERBOSITY").as_deref(),
            Some("normal")
        );
    }

    #[test]
    fn build_tui_command_allows_moonshot_and_forwards_kimi_key() {
        let _lock = env_lock();
        let dir = tempfile::TempDir::new().expect("tempdir");
        let custom = dir
            .path()
            .join(format!("custom-tui{}", std::env::consts::EXE_SUFFIX));
        std::fs::write(&custom, b"").unwrap();
        let custom_str = custom.to_string_lossy().into_owned();
        let _bin = ScopedEnvVar::set("DEEPSEEK_TUI_BIN", &custom_str);

        let cli = parse_ok(&[
            "codewhale",
            "--provider",
            "moonshot",
            "--model",
            "kimi-k2.7-code",
            "--workspace",
            "/tmp/codewhale-workspace",
        ]);
        let resolved = ResolvedRuntimeOptions {
            provider: ProviderKind::Moonshot,
            provider_source: ProviderSource::Cli,
            model: "kimi-k2.7-code".to_string(),
            api_key: Some("resolved-kimi-key".to_string()),
            api_key_source: Some(RuntimeApiKeySource::Keyring),
            base_url: "https://api.moonshot.ai/v1".to_string(),
            auth_mode: Some("api_key".to_string()),
            insecure_skip_tls_verify: false,
            output_mode: None,
            log_level: None,
            telemetry: false,
            approval_policy: None,
            sandbox_mode: None,
            yolo: None,
            verbosity: None,
            http_headers: std::collections::BTreeMap::new(),
        };

        let cmd = build_tui_command(&cli, &resolved, Vec::new()).expect("command");
        assert_eq!(
            command_env(&cmd, "DEEPSEEK_PROVIDER").as_deref(),
            Some("moonshot")
        );
        assert_eq!(
            command_env(&cmd, "DEEPSEEK_MODEL").as_deref(),
            Some("kimi-k2.7-code")
        );
        assert_eq!(
            command_env(&cmd, "DEEPSEEK_API_KEY").as_deref(),
            Some("resolved-kimi-key")
        );
        assert_eq!(
            command_env(&cmd, "MOONSHOT_API_KEY").as_deref(),
            Some("resolved-kimi-key")
        );
        assert_eq!(
            command_env(&cmd, "KIMI_API_KEY").as_deref(),
            Some("resolved-kimi-key")
        );
        assert_eq!(
            command_env(&cmd, "DEEPSEEK_API_KEY_SOURCE").as_deref(),
            Some("keyring")
        );
        assert_eq!(command_env(&cmd, "DEEPSEEK_AUTH_MODE"), None);
    }

    #[test]
    fn build_tui_command_allows_volcengine_and_forwards_ark_keys() {
        let _lock = env_lock();
        let dir = tempfile::TempDir::new().expect("tempdir");
        let custom = dir
            .path()
            .join(format!("custom-tui{}", std::env::consts::EXE_SUFFIX));
        std::fs::write(&custom, b"").unwrap();
        let custom_str = custom.to_string_lossy().into_owned();
        let _bin = ScopedEnvVar::set("DEEPSEEK_TUI_BIN", &custom_str);

        let cli = parse_ok(&[
            "codewhale",
            "--provider",
            "volcengine",
            "--model",
            "DeepSeek-V4-Pro",
            "--workspace",
            "/tmp/codewhale-workspace",
        ]);
        let resolved = ResolvedRuntimeOptions {
            provider: ProviderKind::Volcengine,
            provider_source: ProviderSource::Cli,
            model: "DeepSeek-V4-Pro".to_string(),
            api_key: Some("resolved-ark-key".to_string()),
            api_key_source: Some(RuntimeApiKeySource::Keyring),
            base_url: "https://ark.cn-beijing.volces.com/api/coding/v3".to_string(),
            auth_mode: Some("api_key".to_string()),
            insecure_skip_tls_verify: false,
            output_mode: None,
            log_level: None,
            telemetry: false,
            approval_policy: None,
            sandbox_mode: None,
            yolo: None,
            verbosity: None,
            http_headers: std::collections::BTreeMap::new(),
        };

        let cmd = build_tui_command(&cli, &resolved, Vec::new()).expect("command");
        assert_eq!(
            command_env(&cmd, "DEEPSEEK_PROVIDER").as_deref(),
            Some("volcengine")
        );
        assert_eq!(
            command_env(&cmd, "DEEPSEEK_MODEL").as_deref(),
            Some("DeepSeek-V4-Pro")
        );
        assert_eq!(
            command_env(&cmd, "DEEPSEEK_API_KEY").as_deref(),
            Some("resolved-ark-key")
        );
        assert_eq!(
            command_env(&cmd, "VOLCENGINE_API_KEY").as_deref(),
            Some("resolved-ark-key")
        );
        assert_eq!(
            command_env(&cmd, "VOLCENGINE_ARK_API_KEY").as_deref(),
            Some("resolved-ark-key")
        );
        assert_eq!(
            command_env(&cmd, "ARK_API_KEY").as_deref(),
            Some("resolved-ark-key")
        );
    }

    #[test]
    fn build_tui_command_exports_explicit_provider_model_and_base_url() {
        let _lock = env_lock();
        let dir = tempfile::TempDir::new().expect("tempdir");
        let custom = dir
            .path()
            .join(format!("custom-tui{}", std::env::consts::EXE_SUFFIX));
        std::fs::write(&custom, b"").unwrap();
        let custom_str = custom.to_string_lossy().into_owned();
        let _bin = ScopedEnvVar::set("DEEPSEEK_TUI_BIN", &custom_str);

        let cli = parse_ok(&[
            "deepseek",
            "--profile",
            "google",
            "--provider",
            "openai",
            "--model",
            "glm-5",
            "--base-url",
            "https://openai-compatible.example/v4",
        ]);
        let resolved = ResolvedRuntimeOptions {
            provider: ProviderKind::Openai,
            provider_source: ProviderSource::Cli,
            model: "glm-5".to_string(),
            api_key: None,
            api_key_source: None,
            base_url: "https://openai-compatible.example/v4".to_string(),
            auth_mode: None,
            insecure_skip_tls_verify: false,
            output_mode: None,
            log_level: None,
            telemetry: false,
            approval_policy: None,
            sandbox_mode: None,
            yolo: None,
            verbosity: None,
            http_headers: std::collections::BTreeMap::new(),
        };

        let cmd = build_tui_command(&cli, &resolved, Vec::new()).expect("command");

        assert_eq!(
            command_env(&cmd, "DEEPSEEK_PROVIDER").as_deref(),
            Some("openai")
        );
        assert_eq!(
            command_env(&cmd, "DEEPSEEK_MODEL").as_deref(),
            Some("glm-5")
        );
        assert_eq!(
            command_env(&cmd, "DEEPSEEK_BASE_URL").as_deref(),
            Some("https://openai-compatible.example/v4")
        );
    }

    #[test]
    fn build_tui_command_forwards_provider_keyring_env_vars_for_all_providers() {
        let _lock = env_lock();
        let dir = tempfile::TempDir::new().expect("tempdir");
        let custom = dir
            .path()
            .join(format!("custom-tui{}", std::env::consts::EXE_SUFFIX));
        std::fs::write(&custom, b"").unwrap();
        let custom_str = custom.to_string_lossy().into_owned();
        let _bin = ScopedEnvVar::set("DEEPSEEK_TUI_BIN", &custom_str);

        // (provider, cli flag, extra env vars that must be forwarded besides DEEPSEEK_API_KEY)
        let cases: &[(ProviderKind, &str, &[&str])] = &[
            (
                ProviderKind::Openrouter,
                "openrouter",
                &["OPENROUTER_API_KEY"],
            ),
            (
                ProviderKind::XiaomiMimo,
                "xiaomi-mimo",
                &["XIAOMI_MIMO_API_KEY", "XIAOMI_API_KEY", "MIMO_API_KEY"],
            ),
            (ProviderKind::Novita, "novita", &["NOVITA_API_KEY"]),
            (
                ProviderKind::NvidiaNim,
                "nvidia-nim",
                &["NVIDIA_API_KEY", "NVIDIA_NIM_API_KEY"],
            ),
            (ProviderKind::Fireworks, "fireworks", &["FIREWORKS_API_KEY"]),
            (
                ProviderKind::Siliconflow,
                "siliconflow",
                &["SILICONFLOW_API_KEY"],
            ),
            (ProviderKind::Arcee, "arcee", &["ARCEE_API_KEY"]),
            (ProviderKind::Sglang, "sglang", &["SGLANG_API_KEY"]),
            (ProviderKind::Vllm, "vllm", &["VLLM_API_KEY"]),
            (ProviderKind::Ollama, "ollama", &["OLLAMA_API_KEY"]),
            (
                ProviderKind::Atlascloud,
                "atlascloud",
                &["ATLASCLOUD_API_KEY"],
            ),
            (
                ProviderKind::WanjieArk,
                "wanjie-ark",
                &[
                    "WANJIE_ARK_API_KEY",
                    "WANJIE_API_KEY",
                    "WANJIE_MAAS_API_KEY",
                ],
            ),
        ];

        for &(provider, flag, expected_vars) in cases {
            let cli = parse_ok(&[
                "codewhale",
                "--provider",
                flag,
                "--workspace",
                "/tmp/codewhale-workspace",
            ]);
            let resolved = ResolvedRuntimeOptions {
                provider,
                provider_source: ProviderSource::Cli,
                model: "test-model".to_string(),
                api_key: Some("test-key".to_string()),
                api_key_source: Some(RuntimeApiKeySource::Keyring),
                base_url: "http://localhost:8000/v1".to_string(),
                auth_mode: Some("api_key".to_string()),
                insecure_skip_tls_verify: false,
                output_mode: None,
                log_level: None,
                telemetry: false,
                approval_policy: None,
                sandbox_mode: None,
                yolo: None,
                verbosity: None,
                http_headers: std::collections::BTreeMap::new(),
            };

            let cmd = build_tui_command(&cli, &resolved, Vec::new())
                .unwrap_or_else(|e| panic!("{flag}: {e}"));

            assert_eq!(
                command_env(&cmd, "DEEPSEEK_API_KEY").as_deref(),
                Some("test-key"),
                "{flag}: DEEPSEEK_API_KEY not forwarded"
            );
            for var in expected_vars {
                assert_eq!(
                    command_env(&cmd, var).as_deref(),
                    Some("test-key"),
                    "{flag}: {var} not forwarded"
                );
            }
            assert_eq!(
                command_env(&cmd, "DEEPSEEK_API_KEY_SOURCE").as_deref(),
                Some("keyring"),
                "{flag}: expected keyring source bridge"
            );
            assert_eq!(
                command_env(&cmd, "DEEPSEEK_AUTH_MODE"),
                None,
                "{flag}: auth mode should come from config/profile, not env handoff"
            );
        }
    }

    #[test]
    fn parses_top_level_prompt_flag_for_interactive_startup_prompt() {
        let cli = parse_ok(&["deepseek", "-p", "Reply with exactly OK."]);

        assert_eq!(cli.prompt_flag.as_deref(), Some("Reply with exactly OK."));
        assert!(cli.prompt.is_empty());
        assert_eq!(
            root_tui_passthrough(&cli).unwrap(),
            vec!["--prompt".to_string(), "Reply with exactly OK.".to_string()]
        );
    }

    #[test]
    fn parses_top_level_continue_for_interactive_resume() {
        let cli = parse_ok(&["codewhale", "--continue"]);

        assert!(cli.continue_session);
        assert!(cli.prompt_flag.is_none());
        assert!(cli.prompt.is_empty());
        assert_eq!(root_tui_passthrough(&cli).unwrap(), vec!["--continue"]);
    }

    #[test]
    fn top_level_continue_rejects_startup_prompt() {
        let cli = parse_ok(&["codewhale", "--continue", "-p", "follow up"]);

        let err = root_tui_passthrough(&cli).expect_err("prompted continue should be rejected");
        assert!(
            err.to_string()
                .contains("codewhale exec --continue <PROMPT>")
        );
    }

    #[test]
    fn parses_split_top_level_prompt_words_for_windows_cmd_shims() {
        let cli = parse_ok(&["deepseek", "hello", "world"]);

        assert_eq!(cli.prompt, vec!["hello", "world"]);
        assert!(cli.command.is_none());
        assert_eq!(
            root_tui_passthrough(&cli).unwrap(),
            vec!["--prompt".to_string(), "hello world".to_string()]
        );
    }

    #[test]
    fn prompt_flag_keeps_split_tail_words_for_windows_cmd_shims() {
        let cli = parse_ok(&["deepseek", "-p", "hello", "world"]);

        assert_eq!(cli.prompt_flag.as_deref(), Some("hello"));
        assert_eq!(cli.prompt, vec!["world"]);
        assert_eq!(
            root_tui_passthrough(&cli).unwrap(),
            vec!["--prompt".to_string(), "hello world".to_string()]
        );
    }

    #[test]
    fn known_subcommands_still_parse_before_prompt_tail() {
        let cli = parse_ok(&["deepseek", "doctor"]);

        assert!(cli.prompt.is_empty());
        assert!(matches!(cli.command, Some(Commands::Doctor(_))));
    }

    #[test]
    fn root_help_surface_contains_expected_subcommands_and_globals() {
        let rendered = help_for(&["deepseek", "--help"]);

        for token in [
            "run",
            "doctor",
            "models",
            "sessions",
            "resume",
            "setup",
            "login",
            "logout",
            "auth",
            "mcp-server",
            "config",
            "model",
            "thread",
            "sandbox",
            "app-server",
            "completion",
            "metrics",
            "--provider",
            "--model",
            "--config",
            "--profile",
            "--output-mode",
            "--log-level",
            "--telemetry",
            "--base-url",
            "--api-key",
            "--approval-policy",
            "--sandbox-mode",
            "--mouse-capture",
            "--no-mouse-capture",
            "--skip-onboarding",
            "--continue",
            "--prompt",
        ] {
            assert!(
                rendered.contains(token),
                "expected help to contain token: {token}"
            );
        }
    }

    #[test]
    fn subcommand_help_surfaces_are_stable() {
        let cases = [
            ("config", vec!["get", "set", "unset", "list", "path"]),
            ("model", vec!["list", "resolve"]),
            (
                "thread",
                vec![
                    "list",
                    "read",
                    "resume",
                    "fork",
                    "archive",
                    "unarchive",
                    "set-name",
                    "clear-name",
                ],
            ),
            ("sandbox", vec!["check"]),
            (
                "exec",
                vec![
                    "--auto",
                    "--json",
                    "--resume",
                    "--session-id",
                    "--continue",
                    "--output-format",
                    "stream-json",
                ],
            ),
            (
                "app-server",
                vec!["--host", "--port", "--config", "--stdio"],
            ),
            (
                "completion",
                vec![
                    "<SHELL>",
                    "bash",
                    "source <(codewhale completion bash)",
                    "~/.local/share/bash-completion/completions/codewhale",
                    "fpath=(~/.zfunc $fpath)",
                    "codewhale completion fish > ~/.config/fish/completions/codewhale.fish",
                    "codewhale completion powershell | Out-String | Invoke-Expression",
                ],
            ),
            ("metrics", vec!["--json", "--since"]),
        ];

        for (subcommand, expected_tokens) in cases {
            let argv = ["deepseek", subcommand, "--help"];
            let rendered = help_for(&argv);
            for token in expected_tokens {
                assert!(
                    rendered.contains(token),
                    "expected help for `{subcommand}` to include `{token}`"
                );
            }
        }
    }

    /// Regression for issue #247: on Windows the dispatcher must find the
    /// sibling `codewhale-tui.exe`, not bail out looking for an
    /// extension-less `codewhale-tui`. The candidate resolver also accepts
    /// the suffix-less name on Windows so users who manually renamed the
    /// file as a workaround keep working after the upgrade.
    #[test]
    fn sibling_tui_candidate_picks_platform_correct_name() {
        let dir = tempfile::TempDir::new().expect("tempdir");
        let dispatcher = dir
            .path()
            .join("codewhale")
            .with_extension(std::env::consts::EXE_EXTENSION);
        // Touch the dispatcher so its parent dir is the lookup root.
        std::fs::write(&dispatcher, b"").unwrap();

        // No sibling yet — resolver returns None.
        assert!(sibling_tui_candidate(&dispatcher).is_none());

        let target =
            dispatcher.with_file_name(format!("codewhale-tui{}", std::env::consts::EXE_SUFFIX));
        std::fs::write(&target, b"").unwrap();

        let found = sibling_tui_candidate(&dispatcher).expect("must locate sibling");
        assert_eq!(found, target, "primary platform-correct name wins");
    }

    #[test]
    fn dispatcher_spawn_error_names_path_and_recovery_checks() {
        let err = io::Error::new(io::ErrorKind::PermissionDenied, "access is denied");
        let message = tui_spawn_error(Path::new("C:/tools/codewhale-tui.exe"), &err);

        assert!(message.contains("C:/tools/codewhale-tui.exe"));
        assert!(message.contains("access is denied"));
        assert!(message.contains("where codewhale"));
        assert!(message.contains("DEEPSEEK_TUI_BIN"));
    }

    /// Windows-only fallback: the user from #247 manually renamed the
    /// file to drop `.exe`. After the fix lands, that workaround must
    /// still resolve via the suffix-less fallback so they don't have to
    /// rename it back.
    #[cfg(windows)]
    #[test]
    fn sibling_tui_candidate_windows_falls_back_to_suffixless() {
        let dir = tempfile::TempDir::new().expect("tempdir");
        let dispatcher = dir.path().join("codewhale.exe");
        std::fs::write(&dispatcher, b"").unwrap();

        // Only the suffixless name exists — emulates the manual rename.
        let suffixless = dispatcher.with_file_name("codewhale-tui");
        std::fs::write(&suffixless, b"").unwrap();

        let found = sibling_tui_candidate(&dispatcher)
            .expect("Windows fallback must locate suffixless codewhale-tui");
        assert_eq!(found, suffixless);
    }

    /// `DEEPSEEK_TUI_BIN` overrides the discovery path. Useful for
    /// custom Windows install layouts and CI test rigs.
    #[test]
    fn locate_sibling_tui_binary_honours_env_override() {
        let _lock = env_lock();
        let dir = tempfile::TempDir::new().expect("tempdir");
        let custom = dir
            .path()
            .join(format!("custom-tui{}", std::env::consts::EXE_SUFFIX));
        std::fs::write(&custom, b"").unwrap();
        let custom_str = custom.to_string_lossy().into_owned();
        let _bin = ScopedEnvVar::set("DEEPSEEK_TUI_BIN", &custom_str);

        let resolved = locate_sibling_tui_binary().expect("override must resolve");
        assert_eq!(resolved, custom);
    }
}
