use std::path::PathBuf;

use clap::{Parser, Subcommand, ValueEnum};

#[derive(Parser, Debug)]
#[command(
    name = "batty",
    about = "Hierarchical agent team system for software development",
    version = concat!(env!("CARGO_PKG_VERSION"), "\nhttps://github.com/battysh/batty")
)]
pub struct Cli {
    #[command(subcommand)]
    pub command: Command,

    /// Verbosity level (-v, -vv, -vvv)
    #[arg(short, long, action = clap::ArgAction::Count, global = true)]
    pub verbose: u8,
}

#[derive(Subcommand, Debug)]
pub enum Command {
    /// Scaffold .batty/team_config/ with default team.yaml and prompt templates
    #[command(alias = "install")]
    Init {
        /// Template to use for scaffolding
        #[arg(long, value_enum, conflicts_with = "from")]
        template: Option<InitTemplate>,
        /// Copy team config from $HOME/.batty/templates/<name>/
        #[arg(long, conflicts_with = "template")]
        from: Option<String>,
        /// Overwrite existing team config files
        #[arg(long)]
        force: bool,
        /// Default agent backend for all roles (claude, codex, kiro)
        #[arg(long)]
        agent: Option<String>,
    },

    /// Export the current team config as a reusable template
    ExportTemplate {
        /// Template name
        name: String,
    },

    /// Export run state for debugging
    ExportRun,

    /// Generate a run retrospective
    Retro {
        /// Path to events.jsonl (default: .batty/team_config/events.jsonl)
        #[arg(long)]
        events: Option<PathBuf>,
    },

    /// Start the team daemon and tmux session
    Start {
        /// Auto-attach to the tmux session after startup
        #[arg(long, default_value_t = false)]
        attach: bool,
        /// Suppress informational output (errors still print)
        #[arg(short, long, default_value_t = false)]
        quiet: bool,
    },

    /// Stop the team daemon and kill the tmux session
    Stop,

    /// Attach to the running team tmux session
    Attach,

    /// Show all team members and their states
    Status {
        /// Emit machine-readable JSON output
        #[arg(long, default_value_t = false)]
        json: bool,
        /// Include detailed telemetry-backed engineer profiles
        #[arg(long, default_value_t = false)]
        detail: bool,
        /// Show optional subsystem health and error-budget state
        #[arg(long, default_value_t = false)]
        health: bool,
    },

    /// Prevent an engineer from receiving new auto-dispatch work
    Bench {
        /// Engineer instance (e.g., "eng-1-1")
        engineer: String,
        /// Optional reason recorded in bench.yaml and status output
        #[arg(long)]
        reason: Option<String>,
    },

    /// Remove an engineer from the durable bench list
    Unbench {
        /// Engineer instance (e.g., "eng-1-1")
        engineer: String,
    },

    /// OpenClaw supervisor integration helpers for Batty
    #[command(name = "openclaw")]
    OpenClaw {
        #[command(subcommand)]
        command: OpenClawCommand,
    },

    /// Send a message to an agent role (human → agent injection)
    Send {
        /// Explicit sender override (hidden; used by pane bridge and automation)
        #[arg(long, hide = true)]
        from: Option<String>,
        /// Target role name (e.g., "architect", "manager-1")
        role: String,
        /// Message to inject
        message: String,
    },

    /// Assign a task to an engineer (used by manager agent)
    Assign {
        /// Target engineer instance (e.g., "eng-1-1")
        engineer: String,
        /// Task description
        task: String,
    },

    /// Validate team config without launching
    Validate {
        /// Show all individual checks with pass/fail status
        #[arg(long, default_value_t = false)]
        show_checks: bool,
    },

    /// Show resolved team configuration
    Config {
        /// Emit machine-readable JSON output
        #[arg(long, default_value_t = false)]
        json: bool,
    },

    /// Show the kanban board
    Board {
        #[command(subcommand)]
        command: Option<BoardCommand>,
    },

    /// List inbox messages for a team member, or purge delivered inbox messages
    #[command(args_conflicts_with_subcommands = true)]
    Inbox {
        #[command(subcommand)]
        command: Option<InboxCommand>,
        /// Member name (e.g., "architect", "manager-1", "eng-1-1")
        member: Option<String>,
        /// Maximum number of recent messages to show
        #[arg(
            short = 'n',
            long = "limit",
            default_value_t = 20,
            conflicts_with = "all"
        )]
        limit: usize,
        /// Show all messages
        #[arg(long, default_value_t = false)]
        all: bool,
        /// Show all raw messages without digest collapsing
        #[arg(long, default_value_t = false)]
        raw: bool,
    },

    /// Read a specific message from a member's inbox
    Read {
        /// Member name
        member: String,
        /// Message REF, ID, or ID prefix from `batty inbox` output
        id: String,
    },

    /// Acknowledge (mark delivered) a message in a member's inbox
    Ack {
        /// Member name
        member: String,
        /// Message REF, ID, or ID prefix from `batty inbox` output
        id: String,
    },

    /// Merge an engineer's worktree branch into main
    Merge {
        /// Engineer instance name (e.g., "eng-1-1")
        engineer: String,
    },

    /// Manage workflow task state and metadata
    Task {
        #[command(subcommand)]
        command: TaskCommand,
    },

    /// Record a structured review disposition for a task
    Review {
        /// Task id
        task_id: u32,
        /// Review disposition
        #[arg(value_enum)]
        disposition: ReviewAction,
        /// Feedback text
        feedback: Option<String>,
        /// Reviewer name (default: human)
        #[arg(long, default_value = "human")]
        reviewer: String,
    },

    /// Generate shell completions
    Completions {
        /// Shell to generate completion script for
        #[arg(value_enum)]
        shell: CompletionShell,
    },

    /// Per-intervention runtime toggles
    Nudge {
        #[command(subcommand)]
        command: NudgeCommand,
    },

    /// Pause nudges and standups
    Pause,

    /// Resume nudges and standups
    Resume,

    /// Manage Grafana monitoring (setup, status, open)
    Grafana {
        #[command(subcommand)]
        command: GrafanaCommand,
    },

    /// Configure Discord human communication
    Discord {
        #[command(subcommand)]
        command: Option<DiscordCommand>,
    },

    /// Set up Telegram bot for human communication
    Telegram,

    /// Manage the global Batty project registry for multi-project supervision
    Project {
        #[command(subcommand)]
        command: ProjectCommand,
    },

    /// Estimate team load and show recent load history
    Load,

    /// Show clean-room parity summary from PARITY.md
    Parity {
        /// Show the full parity table
        #[arg(long, default_value_t = false, conflicts_with = "gaps")]
        detail: bool,
        /// Show only behaviors with missing tests or implementation
        #[arg(long, default_value_t = false)]
        gaps: bool,
    },

    /// Run clean-room equivalence verification from PARITY.md
    Verify,

    /// Validate clean main, assemble release notes, and create a release tag
    Release {
        /// Override the git tag to create (default: v<Cargo.toml version>)
        #[arg(long)]
        tag: Option<String>,
    },

    /// Show pending dispatch queue entries
    Queue,

    /// Explain which engineer would receive the next dispatch
    Dispatch {
        /// Print routing reasons for the chosen engineer
        #[arg(long, default_value_t = false)]
        explain: bool,
        /// Explain routing for a specific task id
        #[arg(long)]
        task: Option<u32>,
    },

    /// Estimate current run cost from agent session files
    Cost,

    /// Dynamically scale team topology (add/remove agents)
    Scale {
        #[command(subcommand)]
        command: ScaleCommand,
    },

    /// Trigger an explicit topology reload for the running daemon
    Reload,

    /// Run autonomous evaluator-driven research missions
    Research {
        #[command(subcommand)]
        command: ResearchCommand,
    },

    /// Dump diagnostic state from Batty state files
    Doctor {
        /// Remove orphan branches and worktrees after confirmation
        #[arg(long, default_value_t = false)]
        fix: bool,
        /// Skip the cleanup confirmation prompt
        #[arg(long, default_value_t = false, requires = "fix")]
        yes: bool,
    },

    /// Inspect engineer worktree health
    Worktree {
        /// Show the current worktree health report
        #[arg(long, default_value_t = false)]
        health: bool,
    },

    /// Show consolidated telemetry dashboard (tasks, cycle time, rates, agents)
    Metrics,

    /// Run a synthetic long-session stress harness with fault injection
    StressTest {
        /// Run the full fault matrix on an accelerated compact timeline for CI
        #[arg(long, default_value_t = false)]
        compact: bool,

        /// Virtual session duration in hours when not using compact mode
        #[arg(long, default_value_t = 8)]
        duration_hours: u64,

        /// Deterministic seed for fault scheduling and synthetic recovery timings
        #[arg(long, default_value_t = 1)]
        seed: u64,

        /// Override the JSON report output path
        #[arg(long)]
        json_out: Option<PathBuf>,

        /// Override the Markdown report output path
        #[arg(long)]
        markdown_out: Option<PathBuf>,
    },

    /// Query the telemetry database for agent and task metrics
    Telemetry {
        #[command(subcommand)]
        command: TelemetryCommand,
    },

    /// Interactive chat with an agent via the shim protocol
    Chat {
        /// Agent type: claude, codex, kiro, generic
        #[arg(long, default_value = "generic")]
        agent_type: String,

        /// Shell command to launch the agent CLI (auto-detected from agent type if omitted)
        #[arg(long)]
        cmd: Option<String>,

        /// Working directory for the agent
        #[arg(long, default_value = ".")]
        cwd: String,

        /// Use SDK mode (NDJSON stdin/stdout) instead of PTY screen-scraping
        #[arg(long, default_value_t = false)]
        sdk_mode: bool,
    },

    /// Internal: run a shim process (spawned by `batty chat` or orchestrator)
    #[command(hide = true)]
    Shim {
        /// Unique agent identifier
        #[arg(long)]
        id: String,

        /// Agent type: claude, codex, kiro, generic
        #[arg(long)]
        agent_type: String,

        /// Shell command to launch the agent CLI
        #[arg(long)]
        cmd: String,

        /// Working directory for the agent
        #[arg(long)]
        cwd: String,

        /// Terminal rows
        #[arg(long, default_value = "50")]
        rows: u16,

        /// Terminal columns
        #[arg(long, default_value = "220")]
        cols: u16,

        /// Path to write raw PTY output for tmux display panes
        #[arg(long)]
        pty_log_path: Option<String>,

        /// Seconds to wait for auto-commit before force-killing.
        #[arg(long, default_value_t = 5)]
        graceful_shutdown_timeout_secs: u64,

        /// Auto-commit dirty worktrees before restart/shutdown.
        #[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
        auto_commit_on_restart: bool,

        /// Use SDK mode (NDJSON stdin/stdout) instead of PTY screen-scraping
        #[arg(long, default_value_t = false)]
        sdk_mode: bool,
    },

    /// Internal: interactive shim pane bridge for tmux
    #[command(hide = true)]
    ConsolePane {
        /// Project root directory
        #[arg(long)]
        project_root: String,

        /// Member/agent id
        #[arg(long)]
        member: String,

        /// Path to the shim event log
        #[arg(long)]
        events_log_path: String,

        /// Path to the shim PTY log
        #[arg(long)]
        pty_log_path: String,
    },

    /// Internal: receive Grafana webhook alerts and append them to alerts.log
    #[command(hide = true)]
    GrafanaWebhook {
        /// Project root directory
        #[arg(long)]
        project_root: String,

        /// Local port to bind for webhook delivery
        #[arg(long, default_value_t = 8787)]
        port: u16,
    },

    /// Internal: run the daemon loop (spawned by `batty start`)
    #[command(hide = true)]
    Daemon {
        /// Project root directory
        #[arg(long)]
        project_root: String,
        /// Resume agent sessions from a previous run
        #[arg(long)]
        resume: bool,
    },

    /// Internal: run the daemon watchdog supervisor (spawned by `batty start`)
    #[command(hide = true)]
    Watchdog {
        /// Project root directory
        #[arg(long)]
        project_root: String,
        /// Resume agent sessions from a previous run
        #[arg(long)]
        resume: bool,
    },
}

#[derive(Subcommand, Debug)]
pub enum TelemetryCommand {
    /// Show session summaries
    Summary,
    /// Show per-agent performance metrics
    Agents,
    /// Show per-task lifecycle metrics
    Tasks,
    /// Show review pipeline metrics (auto-merge rate, rework, latency)
    Reviews,
    /// Show recent events from the telemetry database
    Events {
        /// Maximum number of events to show
        #[arg(short = 'n', long = "limit", default_value_t = 50)]
        limit: usize,
    },
}

#[derive(Subcommand, Debug)]
pub enum GrafanaCommand {
    /// Install Grafana and the SQLite datasource plugin, then start the service
    Setup,
    /// Check whether the Grafana server is reachable
    Status,
    /// Open the Grafana dashboard in the default browser
    Open,
}

#[derive(Subcommand, Debug)]
pub enum DiscordCommand {
    /// Run the interactive Discord setup wizard
    Setup,
    /// Show the current Discord connection health
    Status,
}

#[derive(Subcommand, Debug)]
pub enum ProjectCommand {
    /// Register a project in the global Batty/OpenClaw registry
    Register {
        /// Stable unique project identifier
        #[arg(long = "project-id")]
        project_id: String,
        /// Operator-facing project name
        #[arg(long)]
        name: String,
        /// Alternate operator-facing aliases used for routing
        #[arg(long = "alias")]
        aliases: Vec<String>,
        /// Repository root for the supervised project
        #[arg(long = "project-root")]
        project_root: PathBuf,
        /// Kanban board directory for the project
        #[arg(long = "board-dir")]
        board_dir: PathBuf,
        /// Batty team name from team.yaml
        #[arg(long = "team-name")]
        team_name: String,
        /// Explicit runtime session name
        #[arg(long = "session-name")]
        session_name: String,
        /// Optional owner or owning team
        #[arg(long)]
        owner: Option<String>,
        /// Tag metadata
        #[arg(long = "tag")]
        tags: Vec<String>,
        /// Channel binding in the form <channel>=<binding>
        #[arg(long = "channel-binding")]
        channel_bindings: Vec<String>,
        /// Thread binding in the form <channel>=<binding>#<thread-binding>
        #[arg(long = "thread-binding")]
        thread_bindings: Vec<String>,
        /// Allow OpenClaw supervision actions for this project
        #[arg(long, default_value_t = false)]
        allow_openclaw_supervision: bool,
        /// Allow cross-project routing for this project
        #[arg(long, default_value_t = false)]
        allow_cross_project_routing: bool,
        /// Allow shared-service routing for this project
        #[arg(long, default_value_t = false)]
        allow_shared_service_routing: bool,
        /// Mark the project archived at registration time
        #[arg(long, default_value_t = false)]
        archived: bool,
        /// Emit machine-readable JSON output
        #[arg(long, default_value_t = false)]
        json: bool,
    },
    /// Remove a project from the registry
    Unregister {
        /// Stable unique project identifier
        project_id: String,
        /// Emit machine-readable JSON output
        #[arg(long, default_value_t = false)]
        json: bool,
    },
    /// List all registered projects
    List {
        /// Emit machine-readable JSON output
        #[arg(long, default_value_t = false)]
        json: bool,
    },
    /// Show one registered project by projectId
    Get {
        /// Stable unique project identifier
        project_id: String,
        /// Emit machine-readable JSON output
        #[arg(long, default_value_t = false)]
        json: bool,
    },
    /// Start one registered project by projectId
    Start {
        /// Stable unique project identifier
        project_id: String,
        /// Emit machine-readable JSON output
        #[arg(long, default_value_t = false)]
        json: bool,
    },
    /// Stop one registered project by projectId
    Stop {
        /// Stable unique project identifier
        project_id: String,
        /// Emit machine-readable JSON output
        #[arg(long, default_value_t = false)]
        json: bool,
    },
    /// Restart one registered project by projectId
    Restart {
        /// Stable unique project identifier
        project_id: String,
        /// Emit machine-readable JSON output
        #[arg(long, default_value_t = false)]
        json: bool,
    },
    /// Show lifecycle and health status for one registered project
    Status {
        /// Stable unique project identifier
        project_id: String,
        /// Emit machine-readable JSON output
        #[arg(long, default_value_t = false)]
        json: bool,
    },
    /// Set the active project used for implicit routing
    SetActive {
        /// Stable unique project identifier
        project_id: String,
        /// Optional channel provider name for channel-scoped activation
        #[arg(long)]
        channel: Option<String>,
        /// Optional channel binding identifier for channel-scoped activation
        #[arg(long)]
        binding: Option<String>,
        /// Optional thread binding identifier for thread-scoped activation
        #[arg(long = "thread-binding")]
        thread_binding: Option<String>,
        /// Emit machine-readable JSON output
        #[arg(long, default_value_t = false)]
        json: bool,
    },
    /// Resolve which project a message should route to
    Resolve {
        /// Message text to resolve
        message: String,
        /// Optional channel provider name
        #[arg(long)]
        channel: Option<String>,
        /// Optional channel binding identifier
        #[arg(long)]
        binding: Option<String>,
        /// Optional thread binding identifier
        #[arg(long = "thread-binding")]
        thread_binding: Option<String>,
        /// Emit machine-readable JSON output
        #[arg(long, default_value_t = false)]
        json: bool,
    },
}

#[derive(Subcommand, Debug)]
pub enum InboxCommand {
    /// Purge delivered messages from inbox cur/ directories
    Purge {
        /// Role/member name to purge
        #[arg(required_unless_present = "all_roles")]
        role: Option<String>,
        /// Purge delivered messages for every inbox
        #[arg(long, default_value_t = false)]
        all_roles: bool,
        /// Purge delivered messages older than this unix timestamp
        #[arg(long, conflicts_with_all = ["all", "older_than"])]
        before: Option<u64>,
        /// Purge delivered messages older than this duration (e.g. 24h, 7d, 2w)
        #[arg(long, conflicts_with_all = ["all", "before"])]
        older_than: Option<String>,
        /// Purge all delivered messages
        #[arg(long, default_value_t = false, conflicts_with_all = ["before", "older_than"])]
        all: bool,
    },
}

#[derive(Subcommand, Debug)]
pub enum BoardCommand {
    /// List board tasks in a non-interactive table
    List {
        /// Filter tasks by status
        #[arg(long)]
        status: Option<String>,
    },
    /// Show per-status task counts
    Summary,
    /// Show dependency graph
    Deps {
        /// Output format: tree (default), flat, or dot
        #[arg(long, value_enum, default_value_t = DepsFormatArg::Tree)]
        format: DepsFormatArg,
    },
    /// Move done tasks to archive directory
    Archive {
        /// Only archive tasks older than this (e.g. "7d", "24h", "2w", or ISO date)
        #[arg(long, default_value = "0s")]
        older_than: String,

        /// Show what would be archived without moving files
        #[arg(long)]
        dry_run: bool,
    },
    /// Show board health dashboard
    Health,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum DepsFormatArg {
    Tree,
    Flat,
    Dot,
}

#[derive(Subcommand, Debug)]
pub enum TaskCommand {
    /// Transition a task to a new workflow state
    Transition {
        /// Task id
        task_id: u32,
        /// Target state
        #[arg(value_enum)]
        target_state: TaskStateArg,
    },

    /// Assign execution and/or review ownership
    Assign {
        /// Task id
        task_id: u32,
        /// Execution owner
        #[arg(long = "execution-owner")]
        execution_owner: Option<String>,
        /// Review owner
        #[arg(long = "review-owner")]
        review_owner: Option<String>,
    },

    /// Record a review disposition for a task
    Review {
        /// Task id
        task_id: u32,
        /// Review disposition
        #[arg(long, value_enum)]
        disposition: ReviewDispositionArg,
        /// Feedback text (stored and delivered for changes_requested)
        #[arg(long)]
        feedback: Option<String>,
    },

    /// Update workflow metadata fields
    Update {
        /// Task id
        task_id: u32,
        /// Worktree branch
        #[arg(long)]
        branch: Option<String>,
        /// Commit sha
        #[arg(long)]
        commit: Option<String>,
        /// Blocking reason
        #[arg(long = "blocked-on")]
        blocked_on: Option<String>,
        /// Clear blocking fields
        #[arg(long = "clear-blocked", default_value_t = false)]
        clear_blocked: bool,
    },

    /// Set per-task auto-merge override
    #[command(name = "auto-merge")]
    AutoMerge {
        /// Task id
        task_id: u32,
        /// Enable or disable auto-merge for this task
        #[arg(value_enum)]
        action: AutoMergeAction,
    },

    /// Set scheduled_for and/or cron_schedule on a task
    Schedule {
        /// Task id
        task_id: u32,
        /// Scheduled datetime in RFC3339 format (e.g. 2026-03-25T09:00:00-04:00)
        #[arg(long = "at")]
        at: Option<String>,
        /// Cron expression (e.g. '0 9 * * *')
        #[arg(long = "cron")]
        cron: Option<String>,
        /// Clear both scheduled_for and cron_schedule
        #[arg(long, default_value_t = false)]
        clear: bool,
    },
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum InitTemplate {
    /// Single agent, no hierarchy (1 pane)
    Solo,
    /// Architect + 1 engineer pair (2 panes)
    Pair,
    /// 1 architect + 1 manager + 3 engineers (5 panes)
    Simple,
    /// 1 architect + 1 manager + 5 engineers with layout (7 panes)
    Squad,
    /// Human + architect + 3 managers + 15 engineers with Telegram (19 panes)
    Large,
    /// PI + 3 sub-leads + 6 researchers — research lab style (10 panes)
    Research,
    /// Human + tech lead + 2 eng managers + 8 developers — full product team (11 panes)
    Software,
    /// Clean-room workflow: decompiler + spec-writer + test-writer + implementer (4 panes)
    Cleanroom,
    /// Batty self-development: human + architect + manager + 4 Rust engineers (6 panes)
    Batty,
    /// Python-focused team with pytest-aware engineer prompts (5 panes)
    Python,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum CompletionShell {
    Bash,
    Zsh,
    Fish,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum TaskStateArg {
    Backlog,
    Todo,
    #[value(name = "in-progress")]
    InProgress,
    Review,
    Blocked,
    Done,
    Archived,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum ReviewDispositionArg {
    Approved,
    #[value(name = "changes_requested")]
    ChangesRequested,
    Rejected,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum ReviewAction {
    Approve,
    #[value(name = "request-changes")]
    RequestChanges,
    Reject,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum AutoMergeAction {
    Enable,
    Disable,
}

#[derive(Subcommand, Debug)]
pub enum ScaleCommand {
    /// Set engineer instance count (scales up or down)
    Engineers {
        /// Target number of engineers per manager
        count: u32,
    },
    /// Add a new manager role
    AddManager {
        /// Name for the new manager role
        name: String,
    },
    /// Remove a manager role
    RemoveManager {
        /// Name of the manager role to remove
        name: String,
    },
    /// Show current topology (instance counts)
    Status,
}

#[derive(Subcommand, Debug)]
pub enum ResearchCommand {
    /// Start a new research mission
    Start {
        /// Short hypothesis or mission description
        hypothesis: String,
        /// Evaluator shell command to run inside the worktree
        #[arg(long)]
        evaluator: String,
        /// Evaluator output contract
        #[arg(long, value_enum, default_value_t = ResearchFormatArg::Json)]
        format: ResearchFormatArg,
        /// Rule for keeping or discarding candidate iterations
        #[arg(long, value_enum, default_value_t = ResearchKeepPolicyArg::PassOnly)]
        keep_policy: ResearchKeepPolicyArg,
        /// Stop after this many iterations
        #[arg(long, default_value_t = 10)]
        max_iterations: u32,
        /// Worktree to mutate during the mission
        #[arg(long, default_value = ".")]
        worktree: PathBuf,
    },
    /// Show the current mission status
    Status,
    /// Show the current mission ledger
    Ledger,
    /// Stop the current research mission
    Stop,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum ResearchFormatArg {
    Json,
    #[value(name = "exit-code")]
    ExitCode,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum ResearchKeepPolicyArg {
    #[value(name = "pass-only")]
    PassOnly,
    #[value(name = "score-improvement")]
    ScoreImprovement,
    #[value(name = "parity-improvement")]
    ParityImprovement,
}

#[derive(Subcommand, Debug)]
pub enum NudgeCommand {
    /// Disable an intervention at runtime
    Disable {
        /// Intervention name
        #[arg(value_enum)]
        name: NudgeIntervention,
    },
    /// Re-enable a disabled intervention
    Enable {
        /// Intervention name
        #[arg(value_enum)]
        name: NudgeIntervention,
    },
    /// Show status of all interventions
    Status,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum NudgeIntervention {
    Replenish,
    Triage,
    Review,
    Dispatch,
    Utilization,
    #[value(name = "owned-task")]
    OwnedTask,
}

#[derive(Subcommand, Debug)]
pub enum OpenClawCommand {
    /// Write an OpenClaw project registration/config skeleton for Batty
    Register {
        /// Overwrite an existing config file
        #[arg(long, default_value_t = false)]
        force: bool,
    },
    /// Show an operator-friendly Batty summary for OpenClaw supervision
    Status {
        /// Emit machine-readable JSON output
        #[arg(long, default_value_t = false)]
        json: bool,
    },
    /// Send a high-level instruction into an allowed Batty role
    Instruct {
        /// Target role, typically architect or manager
        role: String,
        /// High-level instruction text
        message: String,
    },
    /// Export stable OpenClaw event envelopes for one project or all registered projects
    Events {
        /// Registered project identifier; defaults to the current project if it is registered
        #[arg(long = "project-id", conflicts_with = "all_projects")]
        project_id: Option<String>,
        /// Read events across all registered projects that allow OpenClaw supervision
        #[arg(
            long = "all-projects",
            default_value_t = false,
            conflicts_with = "project_id"
        )]
        all_projects: bool,
        /// Emit machine-readable JSON output
        #[arg(long, default_value_t = false)]
        json: bool,
        /// Filter by stable public event topic
        #[arg(long = "topic", value_enum)]
        topics: Vec<OpenClawEventTopicArg>,
        /// Filter by Batty role identifier
        #[arg(long = "role")]
        roles: Vec<String>,
        /// Filter by Batty task identifier
        #[arg(long = "task-id")]
        task_ids: Vec<String>,
        /// Filter by stable public event type
        #[arg(long = "event-type")]
        event_types: Vec<String>,
        /// Filter by tmux/OpenClaw session name
        #[arg(long = "session-name")]
        session_names: Vec<String>,
        /// Include only events at or after this unix timestamp
        #[arg(long = "since-ts")]
        since_ts: Option<u64>,
        /// Trim the result set to the N most recent matching events
        #[arg(long)]
        limit: Option<usize>,
        /// Include archived projects when reading from the project registry
        #[arg(long, default_value_t = false)]
        include_archived: bool,
    },
    /// Run configured OpenClaw follow-up/reminder workflows
    #[command(name = "follow-up")]
    FollowUp {
        #[command(subcommand)]
        command: OpenClawFollowUpCommand,
    },
}

#[derive(Subcommand, Debug)]
pub enum OpenClawFollowUpCommand {
    /// Evaluate configured cron follow-ups and dispatch any due reminders
    Run {
        /// Emit machine-readable JSON output
        #[arg(long, default_value_t = false)]
        json: bool,
    },
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum OpenClawEventTopicArg {
    Completion,
    Review,
    Stall,
    Merge,
    Escalation,
    #[value(name = "delivery-failure")]
    DeliveryFailure,
    Lifecycle,
}

impl NudgeIntervention {
    /// Return the marker file suffix for this intervention.
    #[allow(dead_code)]
    pub fn marker_name(self) -> &'static str {
        match self {
            Self::Replenish => "replenish",
            Self::Triage => "triage",
            Self::Review => "review",
            Self::Dispatch => "dispatch",
            Self::Utilization => "utilization",
            Self::OwnedTask => "owned-task",
        }
    }

    /// All known interventions.
    #[allow(dead_code)]
    pub const ALL: [NudgeIntervention; 6] = [
        Self::Replenish,
        Self::Triage,
        Self::Review,
        Self::Dispatch,
        Self::Utilization,
        Self::OwnedTask,
    ];
}

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

    #[test]
    fn board_command_defaults_to_tui() {
        let cli = Cli::parse_from(["batty", "board"]);
        match cli.command {
            Command::Board { command } => assert!(command.is_none()),
            other => panic!("expected board command, got {other:?}"),
        }
    }

    #[test]
    fn board_list_subcommand_parses() {
        let cli = Cli::parse_from(["batty", "board", "list"]);
        match cli.command {
            Command::Board {
                command: Some(BoardCommand::List { status }),
            } => assert_eq!(status, None),
            other => panic!("expected board list command, got {other:?}"),
        }
    }

    #[test]
    fn board_list_subcommand_parses_status_filter() {
        let cli = Cli::parse_from(["batty", "board", "list", "--status", "review"]);
        match cli.command {
            Command::Board {
                command: Some(BoardCommand::List { status }),
            } => assert_eq!(status.as_deref(), Some("review")),
            other => panic!("expected board list command, got {other:?}"),
        }
    }

    #[test]
    fn board_summary_subcommand_parses() {
        let cli = Cli::parse_from(["batty", "board", "summary"]);
        match cli.command {
            Command::Board {
                command: Some(BoardCommand::Summary),
            } => {}
            other => panic!("expected board summary command, got {other:?}"),
        }
    }

    #[test]
    fn board_deps_subcommand_defaults_to_tree() {
        let cli = Cli::parse_from(["batty", "board", "deps"]);
        match cli.command {
            Command::Board {
                command: Some(BoardCommand::Deps { format }),
            } => assert_eq!(format, DepsFormatArg::Tree),
            other => panic!("expected board deps command, got {other:?}"),
        }
    }

    #[test]
    fn reload_subcommand_parses() {
        let cli = Cli::parse_from(["batty", "reload"]);
        match cli.command {
            Command::Reload => {}
            other => panic!("expected reload command, got {other:?}"),
        }
    }

    #[test]
    fn research_start_subcommand_parses() {
        let cli = Cli::parse_from([
            "batty",
            "research",
            "start",
            "improve baseline",
            "--evaluator",
            "cargo test",
            "--format",
            "exit-code",
            "--keep-policy",
            "parity-improvement",
            "--max-iterations",
            "3",
            "--worktree",
            "tmp/research",
        ]);
        match cli.command {
            Command::Research {
                command:
                    ResearchCommand::Start {
                        hypothesis,
                        evaluator,
                        format,
                        keep_policy,
                        max_iterations,
                        worktree,
                    },
            } => {
                assert_eq!(hypothesis, "improve baseline");
                assert_eq!(evaluator, "cargo test");
                assert_eq!(format, ResearchFormatArg::ExitCode);
                assert_eq!(keep_policy, ResearchKeepPolicyArg::ParityImprovement);
                assert_eq!(max_iterations, 3);
                assert_eq!(worktree, PathBuf::from("tmp/research"));
            }
            other => panic!("expected research start command, got {other:?}"),
        }
    }

    #[test]
    fn research_status_subcommand_parses() {
        let cli = Cli::parse_from(["batty", "research", "status"]);
        match cli.command {
            Command::Research {
                command: ResearchCommand::Status,
            } => {}
            other => panic!("expected research status command, got {other:?}"),
        }
    }

    #[test]
    fn board_deps_subcommand_parses_format_flag() {
        for (arg, expected) in [
            ("tree", DepsFormatArg::Tree),
            ("flat", DepsFormatArg::Flat),
            ("dot", DepsFormatArg::Dot),
        ] {
            let cli = Cli::parse_from(["batty", "board", "deps", "--format", arg]);
            match cli.command {
                Command::Board {
                    command: Some(BoardCommand::Deps { format }),
                } => assert_eq!(format, expected, "format arg={arg}"),
                other => panic!("expected board deps command for {arg}, got {other:?}"),
            }
        }
    }

    #[test]
    fn board_archive_subcommand_parses() {
        let cli = Cli::parse_from(["batty", "board", "archive"]);
        match cli.command {
            Command::Board {
                command:
                    Some(BoardCommand::Archive {
                        older_than,
                        dry_run,
                    }),
            } => {
                assert_eq!(older_than, "0s");
                assert!(!dry_run);
            }
            other => panic!("expected board archive command, got {other:?}"),
        }
    }

    #[test]
    fn board_archive_subcommand_parses_older_than() {
        let cli = Cli::parse_from(["batty", "board", "archive", "--older-than", "7d"]);
        match cli.command {
            Command::Board {
                command:
                    Some(BoardCommand::Archive {
                        older_than,
                        dry_run,
                    }),
            } => {
                assert_eq!(older_than, "7d");
                assert!(!dry_run);
            }
            other => panic!("expected board archive command with older_than, got {other:?}"),
        }
    }

    #[test]
    fn board_archive_subcommand_parses_dry_run() {
        let cli = Cli::parse_from(["batty", "board", "archive", "--dry-run"]);
        match cli.command {
            Command::Board {
                command:
                    Some(BoardCommand::Archive {
                        older_than,
                        dry_run,
                    }),
            } => {
                assert_eq!(older_than, "0s");
                assert!(dry_run);
            }
            other => panic!("expected board archive command with dry_run, got {other:?}"),
        }
    }

    #[test]
    fn board_health_subcommand_parses() {
        let cli = Cli::parse_from(["batty", "board", "health"]);
        match cli.command {
            Command::Board {
                command: Some(BoardCommand::Health),
            } => {}
            other => panic!("expected board health command, got {other:?}"),
        }
    }

    #[test]
    fn init_subcommand_defaults_to_simple() {
        let cli = Cli::parse_from(["batty", "init"]);
        match cli.command {
            Command::Init {
                template,
                from,
                agent,
                ..
            } => {
                assert_eq!(template, None);
                assert_eq!(from, None);
                assert_eq!(agent, None);
            }
            other => panic!("expected init command, got {other:?}"),
        }
    }

    #[test]
    fn init_subcommand_accepts_large_template() {
        let cli = Cli::parse_from(["batty", "init", "--template", "large"]);
        match cli.command {
            Command::Init { template, from, .. } => {
                assert_eq!(template, Some(InitTemplate::Large));
                assert_eq!(from, None);
            }
            other => panic!("expected init command, got {other:?}"),
        }
    }

    #[test]
    fn init_subcommand_accepts_from_template_name() {
        let cli = Cli::parse_from(["batty", "init", "--from", "custom-team"]);
        match cli.command {
            Command::Init { template, from, .. } => {
                assert_eq!(template, None);
                assert_eq!(from.as_deref(), Some("custom-team"));
            }
            other => panic!("expected init command, got {other:?}"),
        }
    }

    #[test]
    fn init_subcommand_rejects_from_with_template() {
        let result = Cli::try_parse_from(["batty", "init", "--template", "large", "--from", "x"]);
        assert!(result.is_err());
    }

    #[test]
    fn init_agent_flag_parses() {
        let cli = Cli::parse_from(["batty", "init", "--agent", "codex"]);
        match cli.command {
            Command::Init { agent, .. } => {
                assert_eq!(agent.as_deref(), Some("codex"));
            }
            other => panic!("expected init command, got {other:?}"),
        }
    }

    #[test]
    fn install_alias_maps_to_init() {
        let cli = Cli::parse_from(["batty", "install"]);
        match cli.command {
            Command::Init {
                template,
                from,
                agent,
                ..
            } => {
                assert_eq!(template, None);
                assert_eq!(from, None);
                assert_eq!(agent, None);
            }
            other => panic!("expected init command via install alias, got {other:?}"),
        }
    }

    #[test]
    fn install_alias_with_agent_flag() {
        let cli = Cli::parse_from(["batty", "install", "--agent", "kiro"]);
        match cli.command {
            Command::Init { agent, .. } => {
                assert_eq!(agent.as_deref(), Some("kiro"));
            }
            other => panic!("expected init command via install alias, got {other:?}"),
        }
    }

    #[test]
    fn export_template_subcommand_parses() {
        let cli = Cli::parse_from(["batty", "export-template", "myteam"]);
        match cli.command {
            Command::ExportTemplate { name } => assert_eq!(name, "myteam"),
            other => panic!("expected export-template command, got {other:?}"),
        }
    }

    #[test]
    fn export_run_subcommand_parses() {
        let cli = Cli::parse_from(["batty", "export-run"]);
        match cli.command {
            Command::ExportRun => {}
            other => panic!("expected export-run command, got {other:?}"),
        }
    }

    #[test]
    fn retro_subcommand_parses() {
        let cli = Cli::parse_from(["batty", "retro"]);
        match cli.command {
            Command::Retro { events } => assert!(events.is_none()),
            other => panic!("expected retro command, got {other:?}"),
        }
    }

    #[test]
    fn retro_subcommand_parses_with_events_path() {
        let cli = Cli::parse_from(["batty", "retro", "--events", "/tmp/events.jsonl"]);
        match cli.command {
            Command::Retro { events } => {
                assert_eq!(events, Some(PathBuf::from("/tmp/events.jsonl")));
            }
            other => panic!("expected retro command, got {other:?}"),
        }
    }

    #[test]
    fn start_subcommand_defaults() {
        let cli = Cli::parse_from(["batty", "start"]);
        match cli.command {
            Command::Start { attach, quiet } => {
                assert!(!attach);
                assert!(!quiet);
            }
            other => panic!("expected start command, got {other:?}"),
        }
    }

    #[test]
    fn start_subcommand_with_attach() {
        let cli = Cli::parse_from(["batty", "start", "--attach"]);
        match cli.command {
            Command::Start { attach, quiet } => {
                assert!(attach);
                assert!(!quiet);
            }
            other => panic!("expected start command, got {other:?}"),
        }
    }

    #[test]
    fn start_subcommand_with_quiet() {
        let cli = Cli::parse_from(["batty", "start", "-q"]);
        match cli.command {
            Command::Start { attach, quiet } => {
                assert!(!attach);
                assert!(quiet);
            }
            other => panic!("expected start command, got {other:?}"),
        }
    }

    #[test]
    fn stop_subcommand_parses() {
        let cli = Cli::parse_from(["batty", "stop"]);
        assert!(matches!(cli.command, Command::Stop));
    }

    #[test]
    fn attach_subcommand_parses() {
        let cli = Cli::parse_from(["batty", "attach"]);
        assert!(matches!(cli.command, Command::Attach));
    }

    #[test]
    fn status_subcommand_defaults() {
        let cli = Cli::parse_from(["batty", "status"]);
        match cli.command {
            Command::Status {
                json,
                detail,
                health,
            } => {
                assert!(!json);
                assert!(!detail);
                assert!(!health);
            }
            other => panic!("expected status command, got {other:?}"),
        }
    }

    #[test]
    fn status_subcommand_json_flag() {
        let cli = Cli::parse_from(["batty", "status", "--json"]);
        match cli.command {
            Command::Status {
                json,
                detail,
                health,
            } => {
                assert!(json);
                assert!(!detail);
                assert!(!health);
            }
            other => panic!("expected status command, got {other:?}"),
        }
    }

    #[test]
    fn status_subcommand_detail_flag() {
        let cli = Cli::parse_from(["batty", "status", "--detail"]);
        match cli.command {
            Command::Status {
                json,
                detail,
                health,
            } => {
                assert!(!json);
                assert!(detail);
                assert!(!health);
            }
            other => panic!("expected status command, got {other:?}"),
        }
    }

    #[test]
    fn status_subcommand_health_flag() {
        let cli = Cli::parse_from(["batty", "status", "--health"]);
        match cli.command {
            Command::Status {
                json,
                detail,
                health,
            } => {
                assert!(!json);
                assert!(!detail);
                assert!(health);
            }
            other => panic!("expected status command, got {other:?}"),
        }
    }

    #[test]
    fn openclaw_register_subcommand_parses() {
        let cli = Cli::parse_from(["batty", "openclaw", "register", "--force"]);
        match cli.command {
            Command::OpenClaw { command } => match command {
                OpenClawCommand::Register { force } => assert!(force),
                other => panic!("expected openclaw register command, got {other:?}"),
            },
            other => panic!("expected openclaw command, got {other:?}"),
        }
    }

    #[test]
    fn openclaw_status_subcommand_parses_json_flag() {
        let cli = Cli::parse_from(["batty", "openclaw", "status", "--json"]);
        match cli.command {
            Command::OpenClaw { command } => match command {
                OpenClawCommand::Status { json } => assert!(json),
                other => panic!("expected openclaw status command, got {other:?}"),
            },
            other => panic!("expected openclaw command, got {other:?}"),
        }
    }

    #[test]
    fn openclaw_follow_up_run_subcommand_parses() {
        let cli = Cli::parse_from(["batty", "openclaw", "follow-up", "run"]);
        match cli.command {
            Command::OpenClaw { command } => match command {
                OpenClawCommand::FollowUp { command } => match command {
                    OpenClawFollowUpCommand::Run { json } => assert!(!json),
                },
                other => panic!("expected openclaw follow-up command, got {other:?}"),
            },
            other => panic!("expected openclaw command, got {other:?}"),
        }
    }

    #[test]
    fn openclaw_events_subcommand_parses_filters() {
        let cli = Cli::parse_from([
            "batty",
            "openclaw",
            "events",
            "--project-id",
            "fixture-degraded",
            "--json",
            "--topic",
            "escalation",
            "--topic",
            "completion",
            "--role",
            "eng-1-1",
            "--task-id",
            "449",
            "--event-type",
            "task.escalated",
            "--session-name",
            "batty-fixture-team-degraded",
            "--since-ts",
            "1712402000",
            "--limit",
            "5",
            "--include-archived",
        ]);
        match cli.command {
            Command::OpenClaw { command } => match command {
                OpenClawCommand::Events {
                    project_id,
                    all_projects,
                    json,
                    topics,
                    roles,
                    task_ids,
                    event_types,
                    session_names,
                    since_ts,
                    limit,
                    include_archived,
                } => {
                    assert_eq!(project_id.as_deref(), Some("fixture-degraded"));
                    assert!(!all_projects);
                    assert!(json);
                    assert_eq!(
                        topics,
                        vec![
                            OpenClawEventTopicArg::Escalation,
                            OpenClawEventTopicArg::Completion,
                        ]
                    );
                    assert_eq!(roles, vec!["eng-1-1"]);
                    assert_eq!(task_ids, vec!["449"]);
                    assert_eq!(event_types, vec!["task.escalated"]);
                    assert_eq!(session_names, vec!["batty-fixture-team-degraded"]);
                    assert_eq!(since_ts, Some(1_712_402_000));
                    assert_eq!(limit, Some(5));
                    assert!(include_archived);
                }
                other => panic!("expected openclaw events command, got {other:?}"),
            },
            other => panic!("expected openclaw command, got {other:?}"),
        }
    }

    #[test]
    fn send_subcommand_parses_role_and_message() {
        let cli = Cli::parse_from(["batty", "send", "architect", "hello world"]);
        match cli.command {
            Command::Send {
                from,
                role,
                message,
            } => {
                assert!(from.is_none());
                assert_eq!(role, "architect");
                assert_eq!(message, "hello world");
            }
            other => panic!("expected send command, got {other:?}"),
        }
    }

    #[test]
    fn assign_subcommand_parses_engineer_and_task() {
        let cli = Cli::parse_from(["batty", "assign", "eng-1-1", "fix auth bug"]);
        match cli.command {
            Command::Assign { engineer, task } => {
                assert_eq!(engineer, "eng-1-1");
                assert_eq!(task, "fix auth bug");
            }
            other => panic!("expected assign command, got {other:?}"),
        }
    }

    #[test]
    fn bench_subcommand_parses_reason() {
        let cli = Cli::parse_from(["batty", "bench", "eng-1-1", "--reason", "session end"]);
        match cli.command {
            Command::Bench { engineer, reason } => {
                assert_eq!(engineer, "eng-1-1");
                assert_eq!(reason.as_deref(), Some("session end"));
            }
            other => panic!("expected bench command, got {other:?}"),
        }
    }

    #[test]
    fn unbench_subcommand_parses_engineer() {
        let cli = Cli::parse_from(["batty", "unbench", "eng-1-1"]);
        match cli.command {
            Command::Unbench { engineer } => assert_eq!(engineer, "eng-1-1"),
            other => panic!("expected unbench command, got {other:?}"),
        }
    }

    #[test]
    fn validate_subcommand_parses() {
        let cli = Cli::parse_from(["batty", "validate"]);
        match cli.command {
            Command::Validate { show_checks } => assert!(!show_checks),
            other => panic!("expected validate command, got {other:?}"),
        }
    }

    #[test]
    fn validate_subcommand_show_checks_flag() {
        let cli = Cli::parse_from(["batty", "validate", "--show-checks"]);
        match cli.command {
            Command::Validate { show_checks } => assert!(show_checks),
            other => panic!("expected validate command with show_checks, got {other:?}"),
        }
    }

    #[test]
    fn config_subcommand_json_flag() {
        let cli = Cli::parse_from(["batty", "config", "--json"]);
        match cli.command {
            Command::Config { json } => assert!(json),
            other => panic!("expected config command, got {other:?}"),
        }
    }

    #[test]
    fn merge_subcommand_parses_engineer() {
        let cli = Cli::parse_from(["batty", "merge", "eng-1-1"]);
        match cli.command {
            Command::Merge { engineer } => assert_eq!(engineer, "eng-1-1"),
            other => panic!("expected merge command, got {other:?}"),
        }
    }

    #[test]
    fn completions_subcommand_parses_shell() {
        let cli = Cli::parse_from(["batty", "completions", "zsh"]);
        match cli.command {
            Command::Completions { shell } => assert_eq!(shell, CompletionShell::Zsh),
            other => panic!("expected completions command, got {other:?}"),
        }
    }

    #[test]
    fn inbox_subcommand_parses_defaults() {
        let cli = Cli::parse_from(["batty", "inbox", "architect"]);
        match cli.command {
            Command::Inbox {
                command,
                member,
                limit,
                all,
                raw,
            } => {
                assert!(command.is_none());
                assert_eq!(member.as_deref(), Some("architect"));
                assert_eq!(limit, 20);
                assert!(!all);
                assert!(!raw);
            }
            other => panic!("expected inbox command, got {other:?}"),
        }
    }

    #[test]
    fn inbox_subcommand_parses_limit_flag() {
        let cli = Cli::parse_from(["batty", "inbox", "architect", "-n", "50"]);
        match cli.command {
            Command::Inbox {
                command,
                member,
                limit,
                all,
                raw,
            } => {
                assert!(command.is_none());
                assert_eq!(member.as_deref(), Some("architect"));
                assert_eq!(limit, 50);
                assert!(!all);
                assert!(!raw);
            }
            other => panic!("expected inbox command, got {other:?}"),
        }
    }

    #[test]
    fn inbox_subcommand_parses_all_flag() {
        let cli = Cli::parse_from(["batty", "inbox", "architect", "--all"]);
        match cli.command {
            Command::Inbox {
                command,
                member,
                limit,
                all,
                raw,
            } => {
                assert!(command.is_none());
                assert_eq!(member.as_deref(), Some("architect"));
                assert_eq!(limit, 20);
                assert!(all);
                assert!(!raw);
            }
            other => panic!("expected inbox command, got {other:?}"),
        }
    }

    #[test]
    fn inbox_subcommand_parses_raw_flag() {
        let cli = Cli::parse_from(["batty", "inbox", "architect", "--raw"]);
        match cli.command {
            Command::Inbox {
                command,
                member,
                raw,
                ..
            } => {
                assert!(command.is_none());
                assert_eq!(member.as_deref(), Some("architect"));
                assert!(raw);
            }
            other => panic!("expected inbox command, got {other:?}"),
        }
    }

    #[test]
    fn inbox_purge_subcommand_parses_role_and_before() {
        let cli = Cli::parse_from(["batty", "inbox", "purge", "architect", "--before", "123"]);
        match cli.command {
            Command::Inbox {
                command:
                    Some(InboxCommand::Purge {
                        role,
                        all_roles,
                        before,
                        older_than,
                        all,
                    }),
                member,
                ..
            } => {
                assert!(member.is_none());
                assert_eq!(role.as_deref(), Some("architect"));
                assert!(!all_roles);
                assert_eq!(before, Some(123));
                assert!(older_than.is_none());
                assert!(!all);
            }
            other => panic!("expected inbox purge command, got {other:?}"),
        }
    }

    #[test]
    fn inbox_purge_subcommand_parses_all_roles_and_all() {
        let cli = Cli::parse_from(["batty", "inbox", "purge", "--all-roles", "--all"]);
        match cli.command {
            Command::Inbox {
                command:
                    Some(InboxCommand::Purge {
                        role,
                        all_roles,
                        before,
                        older_than,
                        all,
                    }),
                member,
                ..
            } => {
                assert!(member.is_none());
                assert!(role.is_none());
                assert!(all_roles);
                assert_eq!(before, None);
                assert!(older_than.is_none());
                assert!(all);
            }
            other => panic!("expected inbox purge command, got {other:?}"),
        }
    }

    #[test]
    fn inbox_purge_subcommand_parses_older_than() {
        let cli = Cli::parse_from(["batty", "inbox", "purge", "eng-1", "--older-than", "24h"]);
        match cli.command {
            Command::Inbox {
                command:
                    Some(InboxCommand::Purge {
                        role,
                        all_roles,
                        before,
                        older_than,
                        all,
                    }),
                ..
            } => {
                assert_eq!(role.as_deref(), Some("eng-1"));
                assert!(!all_roles);
                assert_eq!(before, None);
                assert_eq!(older_than.as_deref(), Some("24h"));
                assert!(!all);
            }
            other => panic!("expected inbox purge command, got {other:?}"),
        }
    }

    #[test]
    fn inbox_purge_rejects_older_than_with_before() {
        let result = Cli::try_parse_from([
            "batty",
            "inbox",
            "purge",
            "eng-1",
            "--older-than",
            "24h",
            "--before",
            "100",
        ]);
        assert!(result.is_err());
    }

    #[test]
    fn inbox_purge_rejects_older_than_with_all() {
        let result = Cli::try_parse_from([
            "batty",
            "inbox",
            "purge",
            "eng-1",
            "--older-than",
            "24h",
            "--all",
        ]);
        assert!(result.is_err());
    }

    #[test]
    fn read_subcommand_parses_member_and_id() {
        let cli = Cli::parse_from(["batty", "read", "architect", "abc123"]);
        match cli.command {
            Command::Read { member, id } => {
                assert_eq!(member, "architect");
                assert_eq!(id, "abc123");
            }
            other => panic!("expected read command, got {other:?}"),
        }
    }

    #[test]
    fn ack_subcommand_parses_member_and_id() {
        let cli = Cli::parse_from(["batty", "ack", "eng-1-1", "abc123"]);
        match cli.command {
            Command::Ack { member, id } => {
                assert_eq!(member, "eng-1-1");
                assert_eq!(id, "abc123");
            }
            other => panic!("expected ack command, got {other:?}"),
        }
    }

    #[test]
    fn pause_subcommand_parses() {
        let cli = Cli::parse_from(["batty", "pause"]);
        assert!(matches!(cli.command, Command::Pause));
    }

    #[test]
    fn resume_subcommand_parses() {
        let cli = Cli::parse_from(["batty", "resume"]);
        assert!(matches!(cli.command, Command::Resume));
    }

    #[test]
    fn telegram_subcommand_parses() {
        let cli = Cli::parse_from(["batty", "telegram"]);
        assert!(matches!(cli.command, Command::Telegram));
    }

    #[test]
    fn discord_subcommand_defaults_to_setup_flow() {
        let cli = Cli::parse_from(["batty", "discord"]);
        assert!(matches!(cli.command, Command::Discord { command: None }));
    }

    #[test]
    fn discord_status_subcommand_parses() {
        let cli = Cli::parse_from(["batty", "discord", "status"]);
        assert!(matches!(
            cli.command,
            Command::Discord {
                command: Some(DiscordCommand::Status)
            }
        ));
    }

    #[test]
    fn project_register_subcommand_parses() {
        let cli = Cli::parse_from([
            "batty",
            "project",
            "register",
            "--project-id",
            "batty-core",
            "--name",
            "Batty Core",
            "--project-root",
            "/tmp/batty",
            "--board-dir",
            "/tmp/batty/.batty/team_config/board",
            "--team-name",
            "batty",
            "--session-name",
            "batty-batty",
            "--owner",
            "platform",
            "--tag",
            "openclaw",
            "--channel-binding",
            "telegram=chat:123",
            "--allow-openclaw-supervision",
            "--json",
        ]);
        match cli.command {
            Command::Project {
                command:
                    ProjectCommand::Register {
                        project_id,
                        name,
                        aliases,
                        project_root,
                        board_dir,
                        team_name,
                        session_name,
                        owner,
                        tags,
                        channel_bindings,
                        thread_bindings,
                        allow_openclaw_supervision,
                        allow_cross_project_routing,
                        allow_shared_service_routing,
                        archived,
                        json,
                    },
            } => {
                assert_eq!(project_id, "batty-core");
                assert_eq!(name, "Batty Core");
                assert_eq!(project_root, PathBuf::from("/tmp/batty"));
                assert_eq!(
                    board_dir,
                    PathBuf::from("/tmp/batty/.batty/team_config/board")
                );
                assert_eq!(team_name, "batty");
                assert_eq!(session_name, "batty-batty");
                assert_eq!(owner.as_deref(), Some("platform"));
                assert!(aliases.is_empty());
                assert_eq!(tags, vec!["openclaw"]);
                assert_eq!(channel_bindings, vec!["telegram=chat:123"]);
                assert!(thread_bindings.is_empty());
                assert!(allow_openclaw_supervision);
                assert!(!allow_cross_project_routing);
                assert!(!allow_shared_service_routing);
                assert!(!archived);
                assert!(json);
            }
            other => panic!("expected project register command, got {other:?}"),
        }
    }

    #[test]
    fn project_get_subcommand_parses() {
        let cli = Cli::parse_from(["batty", "project", "get", "batty-core", "--json"]);
        match cli.command {
            Command::Project {
                command: ProjectCommand::Get { project_id, json },
            } => {
                assert_eq!(project_id, "batty-core");
                assert!(json);
            }
            other => panic!("expected project get command, got {other:?}"),
        }
    }

    #[test]
    fn project_lifecycle_subcommands_parse() {
        let start = Cli::parse_from(["batty", "project", "start", "batty-core", "--json"]);
        assert!(matches!(
            start.command,
            Command::Project {
                command: ProjectCommand::Start {
                    project_id,
                    json: true
                }
            } if project_id == "batty-core"
        ));

        let stop = Cli::parse_from(["batty", "project", "stop", "batty-core"]);
        assert!(matches!(
            stop.command,
            Command::Project {
                command: ProjectCommand::Stop {
                    project_id,
                    json: false
                }
            } if project_id == "batty-core"
        ));

        let restart = Cli::parse_from(["batty", "project", "restart", "batty-core"]);
        assert!(matches!(
            restart.command,
            Command::Project {
                command: ProjectCommand::Restart {
                    project_id,
                    json: false
                }
            } if project_id == "batty-core"
        ));

        let status = Cli::parse_from(["batty", "project", "status", "batty-core"]);
        assert!(matches!(
            status.command,
            Command::Project {
                command: ProjectCommand::Status {
                    project_id,
                    json: false
                }
            } if project_id == "batty-core"
        ));
    }

    #[test]
    fn project_set_active_subcommand_parses_thread_scope() {
        let cli = Cli::parse_from([
            "batty",
            "project",
            "set-active",
            "batty-core",
            "--channel",
            "slack",
            "--binding",
            "channel:C123",
            "--thread-binding",
            "thread:abc",
        ]);
        match cli.command {
            Command::Project {
                command:
                    ProjectCommand::SetActive {
                        project_id,
                        channel,
                        binding,
                        thread_binding,
                        json,
                    },
            } => {
                assert_eq!(project_id, "batty-core");
                assert_eq!(channel.as_deref(), Some("slack"));
                assert_eq!(binding.as_deref(), Some("channel:C123"));
                assert_eq!(thread_binding.as_deref(), Some("thread:abc"));
                assert!(!json);
            }
            other => panic!("expected project set-active command, got {other:?}"),
        }
    }

    #[test]
    fn project_resolve_subcommand_parses() {
        let cli = Cli::parse_from([
            "batty",
            "project",
            "resolve",
            "check batty",
            "--channel",
            "telegram",
            "--binding",
            "chat:123",
            "--json",
        ]);
        match cli.command {
            Command::Project {
                command:
                    ProjectCommand::Resolve {
                        message,
                        channel,
                        binding,
                        thread_binding,
                        json,
                    },
            } => {
                assert_eq!(message, "check batty");
                assert_eq!(channel.as_deref(), Some("telegram"));
                assert_eq!(binding.as_deref(), Some("chat:123"));
                assert!(thread_binding.is_none());
                assert!(json);
            }
            other => panic!("expected project resolve command, got {other:?}"),
        }
    }

    #[test]
    fn doctor_subcommand_parses() {
        let cli = Cli::parse_from(["batty", "doctor"]);
        assert!(matches!(
            cli.command,
            Command::Doctor {
                fix: false,
                yes: false
            }
        ));
    }

    #[test]
    fn doctor_subcommand_parses_fix_flag() {
        let cli = Cli::parse_from(["batty", "doctor", "--fix"]);
        assert!(matches!(
            cli.command,
            Command::Doctor {
                fix: true,
                yes: false
            }
        ));
    }

    #[test]
    fn doctor_subcommand_parses_fix_yes_flags() {
        let cli = Cli::parse_from(["batty", "doctor", "--fix", "--yes"]);
        assert!(matches!(
            cli.command,
            Command::Doctor {
                fix: true,
                yes: true
            }
        ));
    }

    #[test]
    fn worktree_health_command_parses() {
        let cli = Cli::parse_from(["batty", "worktree", "--health"]);
        assert!(matches!(cli.command, Command::Worktree { health: true }));
    }

    #[test]
    fn load_subcommand_parses() {
        let cli = Cli::parse_from(["batty", "load"]);
        assert!(matches!(cli.command, Command::Load));
    }

    #[test]
    fn parity_subcommand_parses_defaults() {
        let cli = Cli::parse_from(["batty", "parity"]);
        match cli.command {
            Command::Parity { detail, gaps } => {
                assert!(!detail);
                assert!(!gaps);
            }
            other => panic!("expected parity command, got {other:?}"),
        }
    }

    #[test]
    fn parity_subcommand_parses_gaps_flag() {
        let cli = Cli::parse_from(["batty", "parity", "--gaps"]);
        match cli.command {
            Command::Parity { detail, gaps } => {
                assert!(!detail);
                assert!(gaps);
            }
            other => panic!("expected parity command, got {other:?}"),
        }
    }

    #[test]
    fn verify_subcommand_parses() {
        let cli = Cli::parse_from(["batty", "verify"]);
        assert!(matches!(cli.command, Command::Verify));
    }

    #[test]
    fn release_subcommand_parses_defaults() {
        let cli = Cli::parse_from(["batty", "release"]);
        match cli.command {
            Command::Release { tag } => assert!(tag.is_none()),
            other => panic!("expected release command, got {other:?}"),
        }
    }

    #[test]
    fn release_subcommand_parses_tag_override() {
        let cli = Cli::parse_from(["batty", "release", "--tag", "batty-2026-04-10"]);
        match cli.command {
            Command::Release { tag } => {
                assert_eq!(tag.as_deref(), Some("batty-2026-04-10"))
            }
            other => panic!("expected release command, got {other:?}"),
        }
    }

    #[test]
    fn queue_subcommand_parses() {
        let cli = Cli::parse_from(["batty", "queue"]);
        assert!(matches!(cli.command, Command::Queue));
    }

    #[test]
    fn dispatch_explain_subcommand_parses() {
        let cli = Cli::parse_from(["batty", "dispatch", "--explain", "--task", "42"]);
        match cli.command {
            Command::Dispatch { explain, task } => {
                assert!(explain);
                assert_eq!(task, Some(42));
            }
            other => panic!("expected dispatch command, got {other:?}"),
        }
    }

    #[test]
    fn cost_subcommand_parses() {
        let cli = Cli::parse_from(["batty", "cost"]);
        assert!(matches!(cli.command, Command::Cost));
    }

    #[test]
    fn verbose_flag_is_global() {
        let cli = Cli::parse_from(["batty", "-vv", "status"]);
        assert_eq!(cli.verbose, 2);
    }

    #[test]
    fn task_transition_subcommand_parses() {
        let cli = Cli::parse_from(["batty", "task", "transition", "24", "in-progress"]);
        match cli.command {
            Command::Task {
                command:
                    TaskCommand::Transition {
                        task_id,
                        target_state,
                    },
            } => {
                assert_eq!(task_id, 24);
                assert_eq!(target_state, TaskStateArg::InProgress);
            }
            other => panic!("expected task transition command, got {other:?}"),
        }
    }

    #[test]
    fn task_assign_subcommand_parses() {
        let cli = Cli::parse_from([
            "batty",
            "task",
            "assign",
            "24",
            "--execution-owner",
            "eng-1-2",
            "--review-owner",
            "manager-1",
        ]);
        match cli.command {
            Command::Task {
                command:
                    TaskCommand::Assign {
                        task_id,
                        execution_owner,
                        review_owner,
                    },
            } => {
                assert_eq!(task_id, 24);
                assert_eq!(execution_owner.as_deref(), Some("eng-1-2"));
                assert_eq!(review_owner.as_deref(), Some("manager-1"));
            }
            other => panic!("expected task assign command, got {other:?}"),
        }
    }

    #[test]
    fn task_review_subcommand_parses() {
        let cli = Cli::parse_from([
            "batty",
            "task",
            "review",
            "24",
            "--disposition",
            "changes_requested",
        ]);
        match cli.command {
            Command::Task {
                command:
                    TaskCommand::Review {
                        task_id,
                        disposition,
                        feedback,
                    },
            } => {
                assert_eq!(task_id, 24);
                assert_eq!(disposition, ReviewDispositionArg::ChangesRequested);
                assert!(feedback.is_none());
            }
            other => panic!("expected task review command, got {other:?}"),
        }
    }

    #[test]
    fn task_update_subcommand_parses() {
        let cli = Cli::parse_from([
            "batty",
            "task",
            "update",
            "24",
            "--branch",
            "eng-1-2/task-24",
            "--commit",
            "abc1234",
            "--blocked-on",
            "waiting for review",
            "--clear-blocked",
        ]);
        match cli.command {
            Command::Task {
                command:
                    TaskCommand::Update {
                        task_id,
                        branch,
                        commit,
                        blocked_on,
                        clear_blocked,
                    },
            } => {
                assert_eq!(task_id, 24);
                assert_eq!(branch.as_deref(), Some("eng-1-2/task-24"));
                assert_eq!(commit.as_deref(), Some("abc1234"));
                assert_eq!(blocked_on.as_deref(), Some("waiting for review"));
                assert!(clear_blocked);
            }
            other => panic!("expected task update command, got {other:?}"),
        }
    }

    #[test]
    fn nudge_disable_parses() {
        let cli = Cli::parse_from(["batty", "nudge", "disable", "triage"]);
        match cli.command {
            Command::Nudge {
                command: NudgeCommand::Disable { name },
            } => assert_eq!(name, NudgeIntervention::Triage),
            other => panic!("expected nudge disable, got {other:?}"),
        }
    }

    #[test]
    fn nudge_enable_parses() {
        let cli = Cli::parse_from(["batty", "nudge", "enable", "replenish"]);
        match cli.command {
            Command::Nudge {
                command: NudgeCommand::Enable { name },
            } => assert_eq!(name, NudgeIntervention::Replenish),
            other => panic!("expected nudge enable, got {other:?}"),
        }
    }

    #[test]
    fn nudge_status_parses() {
        let cli = Cli::parse_from(["batty", "nudge", "status"]);
        match cli.command {
            Command::Nudge {
                command: NudgeCommand::Status,
            } => {}
            other => panic!("expected nudge status, got {other:?}"),
        }
    }

    #[test]
    fn nudge_disable_owned_task_parses() {
        let cli = Cli::parse_from(["batty", "nudge", "disable", "owned-task"]);
        match cli.command {
            Command::Nudge {
                command: NudgeCommand::Disable { name },
            } => assert_eq!(name, NudgeIntervention::OwnedTask),
            other => panic!("expected nudge disable owned-task, got {other:?}"),
        }
    }

    #[test]
    fn nudge_rejects_unknown_intervention() {
        let result = Cli::try_parse_from(["batty", "nudge", "disable", "unknown"]);
        assert!(result.is_err());
    }

    #[test]
    fn nudge_intervention_marker_names() {
        assert_eq!(NudgeIntervention::Replenish.marker_name(), "replenish");
        assert_eq!(NudgeIntervention::Triage.marker_name(), "triage");
        assert_eq!(NudgeIntervention::Review.marker_name(), "review");
        assert_eq!(NudgeIntervention::Dispatch.marker_name(), "dispatch");
        assert_eq!(NudgeIntervention::Utilization.marker_name(), "utilization");
        assert_eq!(NudgeIntervention::OwnedTask.marker_name(), "owned-task");
    }

    #[test]
    fn parse_task_schedule_at() {
        let cli = Cli::parse_from([
            "batty",
            "task",
            "schedule",
            "50",
            "--at",
            "2026-03-25T09:00:00-04:00",
        ]);
        match cli.command {
            Command::Task {
                command:
                    TaskCommand::Schedule {
                        task_id,
                        at,
                        cron,
                        clear,
                    },
            } => {
                assert_eq!(task_id, 50);
                assert_eq!(at.as_deref(), Some("2026-03-25T09:00:00-04:00"));
                assert!(cron.is_none());
                assert!(!clear);
            }
            other => panic!("expected task schedule command, got {other:?}"),
        }
    }

    #[test]
    fn parse_task_schedule_cron() {
        let cli = Cli::parse_from(["batty", "task", "schedule", "51", "--cron", "0 9 * * *"]);
        match cli.command {
            Command::Task {
                command:
                    TaskCommand::Schedule {
                        task_id,
                        at,
                        cron,
                        clear,
                    },
            } => {
                assert_eq!(task_id, 51);
                assert!(at.is_none());
                assert_eq!(cron.as_deref(), Some("0 9 * * *"));
                assert!(!clear);
            }
            other => panic!("expected task schedule command, got {other:?}"),
        }
    }

    #[test]
    fn parse_task_schedule_clear() {
        let cli = Cli::parse_from(["batty", "task", "schedule", "52", "--clear"]);
        match cli.command {
            Command::Task {
                command:
                    TaskCommand::Schedule {
                        task_id,
                        at,
                        cron,
                        clear,
                    },
            } => {
                assert_eq!(task_id, 52);
                assert!(at.is_none());
                assert!(cron.is_none());
                assert!(clear);
            }
            other => panic!("expected task schedule command, got {other:?}"),
        }
    }

    #[test]
    fn parse_task_schedule_both() {
        let cli = Cli::parse_from([
            "batty",
            "task",
            "schedule",
            "53",
            "--at",
            "2026-04-01T00:00:00Z",
            "--cron",
            "0 9 * * 1",
        ]);
        match cli.command {
            Command::Task {
                command:
                    TaskCommand::Schedule {
                        task_id,
                        at,
                        cron,
                        clear,
                    },
            } => {
                assert_eq!(task_id, 53);
                assert_eq!(at.as_deref(), Some("2026-04-01T00:00:00Z"));
                assert_eq!(cron.as_deref(), Some("0 9 * * 1"));
                assert!(!clear);
            }
            other => panic!("expected task schedule command, got {other:?}"),
        }
    }

    #[test]
    fn review_approve_parses() {
        let cli = Cli::parse_from(["batty", "review", "42", "approve"]);
        match cli.command {
            Command::Review {
                task_id,
                disposition,
                feedback,
                reviewer,
            } => {
                assert_eq!(task_id, 42);
                assert_eq!(disposition, ReviewAction::Approve);
                assert!(feedback.is_none());
                assert_eq!(reviewer, "human");
            }
            other => panic!("expected review command, got {other:?}"),
        }
    }

    #[test]
    fn review_request_changes_with_feedback_parses() {
        let cli = Cli::parse_from([
            "batty",
            "review",
            "99",
            "request-changes",
            "fix the error handling",
        ]);
        match cli.command {
            Command::Review {
                task_id,
                disposition,
                feedback,
                reviewer,
            } => {
                assert_eq!(task_id, 99);
                assert_eq!(disposition, ReviewAction::RequestChanges);
                assert_eq!(feedback.as_deref(), Some("fix the error handling"));
                assert_eq!(reviewer, "human");
            }
            other => panic!("expected review command, got {other:?}"),
        }
    }

    #[test]
    fn review_reject_with_reviewer_flag_parses() {
        let cli = Cli::parse_from([
            "batty",
            "review",
            "7",
            "reject",
            "does not meet requirements",
            "--reviewer",
            "manager-1",
        ]);
        match cli.command {
            Command::Review {
                task_id,
                disposition,
                feedback,
                reviewer,
            } => {
                assert_eq!(task_id, 7);
                assert_eq!(disposition, ReviewAction::Reject);
                assert_eq!(feedback.as_deref(), Some("does not meet requirements"));
                assert_eq!(reviewer, "manager-1");
            }
            other => panic!("expected review command, got {other:?}"),
        }
    }

    #[test]
    fn review_rejects_invalid_disposition() {
        let result = Cli::try_parse_from(["batty", "review", "42", "maybe"]);
        assert!(result.is_err());
    }

    // --- send: missing required args ---

    #[test]
    fn send_rejects_missing_role() {
        let result = Cli::try_parse_from(["batty", "send"]);
        assert!(result.is_err());
    }

    #[test]
    fn send_rejects_missing_message() {
        let result = Cli::try_parse_from(["batty", "send", "architect"]);
        assert!(result.is_err());
    }

    // --- assign: missing required args ---

    #[test]
    fn assign_rejects_missing_engineer() {
        let result = Cli::try_parse_from(["batty", "assign"]);
        assert!(result.is_err());
    }

    #[test]
    fn assign_rejects_missing_task() {
        let result = Cli::try_parse_from(["batty", "assign", "eng-1-1"]);
        assert!(result.is_err());
    }

    // --- review: missing required args ---

    #[test]
    fn review_rejects_missing_task_id() {
        let result = Cli::try_parse_from(["batty", "review"]);
        assert!(result.is_err());
    }

    #[test]
    fn review_rejects_missing_disposition() {
        let result = Cli::try_parse_from(["batty", "review", "42"]);
        assert!(result.is_err());
    }

    // --- merge: missing required args ---

    #[test]
    fn merge_rejects_missing_engineer() {
        let result = Cli::try_parse_from(["batty", "merge"]);
        assert!(result.is_err());
    }

    // --- read/ack: missing required args ---

    #[test]
    fn read_rejects_missing_member() {
        let result = Cli::try_parse_from(["batty", "read"]);
        assert!(result.is_err());
    }

    #[test]
    fn read_rejects_missing_id() {
        let result = Cli::try_parse_from(["batty", "read", "architect"]);
        assert!(result.is_err());
    }

    #[test]
    fn ack_rejects_missing_args() {
        let result = Cli::try_parse_from(["batty", "ack"]);
        assert!(result.is_err());
    }

    // --- telemetry subcommands ---

    #[test]
    fn telemetry_summary_parses() {
        let cli = Cli::parse_from(["batty", "telemetry", "summary"]);
        match cli.command {
            Command::Telemetry {
                command: TelemetryCommand::Summary,
            } => {}
            other => panic!("expected telemetry summary, got {other:?}"),
        }
    }

    #[test]
    fn telemetry_agents_parses() {
        let cli = Cli::parse_from(["batty", "telemetry", "agents"]);
        match cli.command {
            Command::Telemetry {
                command: TelemetryCommand::Agents,
            } => {}
            other => panic!("expected telemetry agents, got {other:?}"),
        }
    }

    #[test]
    fn telemetry_tasks_parses() {
        let cli = Cli::parse_from(["batty", "telemetry", "tasks"]);
        match cli.command {
            Command::Telemetry {
                command: TelemetryCommand::Tasks,
            } => {}
            other => panic!("expected telemetry tasks, got {other:?}"),
        }
    }

    #[test]
    fn telemetry_reviews_parses() {
        let cli = Cli::parse_from(["batty", "telemetry", "reviews"]);
        match cli.command {
            Command::Telemetry {
                command: TelemetryCommand::Reviews,
            } => {}
            other => panic!("expected telemetry reviews, got {other:?}"),
        }
    }

    #[test]
    fn telemetry_events_default_limit() {
        let cli = Cli::parse_from(["batty", "telemetry", "events"]);
        match cli.command {
            Command::Telemetry {
                command: TelemetryCommand::Events { limit },
            } => assert_eq!(limit, 50),
            other => panic!("expected telemetry events, got {other:?}"),
        }
    }

    #[test]
    fn telemetry_events_custom_limit() {
        let cli = Cli::parse_from(["batty", "telemetry", "events", "-n", "10"]);
        match cli.command {
            Command::Telemetry {
                command: TelemetryCommand::Events { limit },
            } => assert_eq!(limit, 10),
            other => panic!("expected telemetry events with limit, got {other:?}"),
        }
    }

    #[test]
    fn telemetry_rejects_missing_subcommand() {
        let result = Cli::try_parse_from(["batty", "telemetry"]);
        assert!(result.is_err());
    }

    // --- grafana ---

    #[test]
    fn grafana_setup_parses() {
        let cli = Cli::parse_from(["batty", "grafana", "setup"]);
        assert!(matches!(
            cli.command,
            Command::Grafana {
                command: GrafanaCommand::Setup
            }
        ));
    }

    #[test]
    fn grafana_status_parses() {
        let cli = Cli::parse_from(["batty", "grafana", "status"]);
        assert!(matches!(
            cli.command,
            Command::Grafana {
                command: GrafanaCommand::Status
            }
        ));
    }

    #[test]
    fn grafana_open_parses() {
        let cli = Cli::parse_from(["batty", "grafana", "open"]);
        assert!(matches!(
            cli.command,
            Command::Grafana {
                command: GrafanaCommand::Open
            }
        ));
    }

    #[test]
    fn grafana_rejects_missing_subcommand() {
        let result = Cli::try_parse_from(["batty", "grafana"]);
        assert!(result.is_err());
    }

    // --- task auto-merge ---

    #[test]
    fn task_auto_merge_enable_parses() {
        let cli = Cli::parse_from(["batty", "task", "auto-merge", "30", "enable"]);
        match cli.command {
            Command::Task {
                command: TaskCommand::AutoMerge { task_id, action },
            } => {
                assert_eq!(task_id, 30);
                assert_eq!(action, AutoMergeAction::Enable);
            }
            other => panic!("expected task auto-merge enable, got {other:?}"),
        }
    }

    #[test]
    fn task_auto_merge_disable_parses() {
        let cli = Cli::parse_from(["batty", "task", "auto-merge", "31", "disable"]);
        match cli.command {
            Command::Task {
                command: TaskCommand::AutoMerge { task_id, action },
            } => {
                assert_eq!(task_id, 31);
                assert_eq!(action, AutoMergeAction::Disable);
            }
            other => panic!("expected task auto-merge disable, got {other:?}"),
        }
    }

    #[test]
    fn task_auto_merge_rejects_invalid_action() {
        let result = Cli::try_parse_from(["batty", "task", "auto-merge", "30", "toggle"]);
        assert!(result.is_err());
    }

    // --- task assign with partial owners ---

    #[test]
    fn task_assign_execution_owner_only() {
        let cli = Cli::parse_from([
            "batty",
            "task",
            "assign",
            "10",
            "--execution-owner",
            "eng-1-3",
        ]);
        match cli.command {
            Command::Task {
                command:
                    TaskCommand::Assign {
                        task_id,
                        execution_owner,
                        review_owner,
                    },
            } => {
                assert_eq!(task_id, 10);
                assert_eq!(execution_owner.as_deref(), Some("eng-1-3"));
                assert!(review_owner.is_none());
            }
            other => panic!("expected task assign command, got {other:?}"),
        }
    }

    // --- task rejects missing subcommand ---

    #[test]
    fn task_rejects_missing_subcommand() {
        let result = Cli::try_parse_from(["batty", "task"]);
        assert!(result.is_err());
    }

    // --- doctor: --yes requires --fix ---

    #[test]
    fn doctor_rejects_yes_without_fix() {
        let result = Cli::try_parse_from(["batty", "doctor", "--yes"]);
        assert!(result.is_err());
    }

    // --- daemon hidden subcommand ---

    #[test]
    fn daemon_subcommand_parses() {
        let cli = Cli::parse_from(["batty", "daemon", "--project-root", "/tmp/project"]);
        match cli.command {
            Command::Daemon {
                project_root,
                resume,
            } => {
                assert_eq!(project_root, "/tmp/project");
                assert!(!resume);
            }
            other => panic!("expected daemon command, got {other:?}"),
        }
    }

    #[test]
    fn daemon_subcommand_parses_resume_flag() {
        let cli = Cli::parse_from([
            "batty",
            "daemon",
            "--project-root",
            "/tmp/project",
            "--resume",
        ]);
        match cli.command {
            Command::Daemon {
                project_root,
                resume,
            } => {
                assert_eq!(project_root, "/tmp/project");
                assert!(resume);
            }
            other => panic!("expected daemon command with resume, got {other:?}"),
        }
    }

    #[test]
    fn watchdog_subcommand_parses() {
        let cli = Cli::parse_from(["batty", "watchdog", "--project-root", "/tmp/project"]);
        match cli.command {
            Command::Watchdog {
                project_root,
                resume,
            } => {
                assert_eq!(project_root, "/tmp/project");
                assert!(!resume);
            }
            other => panic!("expected watchdog command, got {other:?}"),
        }
    }

    #[test]
    fn watchdog_subcommand_parses_resume_flag() {
        let cli = Cli::parse_from([
            "batty",
            "watchdog",
            "--project-root",
            "/tmp/project",
            "--resume",
        ]);
        match cli.command {
            Command::Watchdog {
                project_root,
                resume,
            } => {
                assert_eq!(project_root, "/tmp/project");
                assert!(resume);
            }
            other => panic!("expected watchdog command with resume, got {other:?}"),
        }
    }

    // --- completions: all shell variants ---

    #[test]
    fn completions_all_shells_parse() {
        for (arg, expected) in [
            ("bash", CompletionShell::Bash),
            ("zsh", CompletionShell::Zsh),
            ("fish", CompletionShell::Fish),
        ] {
            let cli = Cli::parse_from(["batty", "completions", arg]);
            match cli.command {
                Command::Completions { shell } => assert_eq!(shell, expected, "shell arg={arg}"),
                other => panic!("expected completions command for {arg}, got {other:?}"),
            }
        }
    }

    #[test]
    fn completions_rejects_unknown_shell() {
        let result = Cli::try_parse_from(["batty", "completions", "powershell"]);
        assert!(result.is_err());
    }

    // --- init: all template variants ---

    #[test]
    fn init_all_template_variants() {
        for (arg, expected) in [
            ("solo", InitTemplate::Solo),
            ("pair", InitTemplate::Pair),
            ("simple", InitTemplate::Simple),
            ("squad", InitTemplate::Squad),
            ("large", InitTemplate::Large),
            ("research", InitTemplate::Research),
            ("software", InitTemplate::Software),
            ("cleanroom", InitTemplate::Cleanroom),
            ("batty", InitTemplate::Batty),
            ("python", InitTemplate::Python),
        ] {
            let cli = Cli::parse_from(["batty", "init", "--template", arg]);
            match cli.command {
                Command::Init { template, from, .. } => {
                    assert_eq!(template, Some(expected), "template arg={arg}");
                    assert!(from.is_none());
                }
                other => panic!("expected init command for template {arg}, got {other:?}"),
            }
        }
    }

    // --- task review with feedback ---

    #[test]
    fn task_review_with_feedback_parses() {
        let cli = Cli::parse_from([
            "batty",
            "task",
            "review",
            "15",
            "--disposition",
            "changes_requested",
            "--feedback",
            "please fix tests",
        ]);
        match cli.command {
            Command::Task {
                command:
                    TaskCommand::Review {
                        task_id,
                        disposition,
                        feedback,
                    },
            } => {
                assert_eq!(task_id, 15);
                assert_eq!(disposition, ReviewDispositionArg::ChangesRequested);
                assert_eq!(feedback.as_deref(), Some("please fix tests"));
            }
            other => panic!("expected task review command, got {other:?}"),
        }
    }

    // --- task transition: all states ---

    #[test]
    fn task_transition_all_states() {
        for (arg, expected) in [
            ("backlog", TaskStateArg::Backlog),
            ("todo", TaskStateArg::Todo),
            ("in-progress", TaskStateArg::InProgress),
            ("review", TaskStateArg::Review),
            ("blocked", TaskStateArg::Blocked),
            ("done", TaskStateArg::Done),
            ("archived", TaskStateArg::Archived),
        ] {
            let cli = Cli::parse_from(["batty", "task", "transition", "1", arg]);
            match cli.command {
                Command::Task {
                    command:
                        TaskCommand::Transition {
                            task_id,
                            target_state,
                        },
                } => {
                    assert_eq!(task_id, 1);
                    assert_eq!(target_state, expected, "state arg={arg}");
                }
                other => panic!("expected task transition for {arg}, got {other:?}"),
            }
        }
    }

    #[test]
    fn task_transition_rejects_invalid_state() {
        let result = Cli::try_parse_from(["batty", "task", "transition", "1", "cancelled"]);
        assert!(result.is_err());
    }

    // --- unknown subcommand ---

    #[test]
    fn rejects_unknown_subcommand() {
        let result = Cli::try_parse_from(["batty", "foobar"]);
        assert!(result.is_err());
    }

    // --- no args ---

    #[test]
    fn rejects_no_subcommand() {
        let result = Cli::try_parse_from(["batty"]);
        assert!(result.is_err());
    }

    // --- inbox purge requires role or all-roles ---

    #[test]
    fn inbox_purge_rejects_missing_role_and_all_roles() {
        let result = Cli::try_parse_from(["batty", "inbox", "purge", "--all"]);
        assert!(result.is_err());
    }

    // --- nudge: all intervention variants ---

    #[test]
    fn nudge_enable_all_interventions() {
        for (arg, expected) in [
            ("replenish", NudgeIntervention::Replenish),
            ("triage", NudgeIntervention::Triage),
            ("review", NudgeIntervention::Review),
            ("dispatch", NudgeIntervention::Dispatch),
            ("utilization", NudgeIntervention::Utilization),
            ("owned-task", NudgeIntervention::OwnedTask),
        ] {
            let cli = Cli::parse_from(["batty", "nudge", "enable", arg]);
            match cli.command {
                Command::Nudge {
                    command: NudgeCommand::Enable { name },
                } => assert_eq!(name, expected, "nudge enable arg={arg}"),
                other => panic!("expected nudge enable for {arg}, got {other:?}"),
            }
        }
    }

    // --- config: default (no --json) ---

    #[test]
    fn config_subcommand_defaults_no_json() {
        let cli = Cli::parse_from(["batty", "config"]);
        match cli.command {
            Command::Config { json } => assert!(!json),
            other => panic!("expected config command, got {other:?}"),
        }
    }

    // --- completion generation tests ---

    /// Helper: generate completion script for a shell into a String.
    fn generate_completions(shell: clap_complete::Shell) -> String {
        use clap::CommandFactory;
        let mut buf = Vec::new();
        clap_complete::generate(shell, &mut Cli::command(), "batty", &mut buf);
        String::from_utf8(buf).expect("completions should be valid UTF-8")
    }

    #[test]
    fn completions_bash_generates() {
        let output = generate_completions(clap_complete::Shell::Bash);
        assert!(!output.is_empty(), "bash completions should not be empty");
        assert!(
            output.contains("_batty"),
            "bash completions should define _batty function"
        );
    }

    #[test]
    fn completions_zsh_generates() {
        let output = generate_completions(clap_complete::Shell::Zsh);
        assert!(!output.is_empty(), "zsh completions should not be empty");
        assert!(
            output.contains("#compdef batty"),
            "zsh completions should start with #compdef"
        );
    }

    #[test]
    fn completions_fish_generates() {
        let output = generate_completions(clap_complete::Shell::Fish);
        assert!(!output.is_empty(), "fish completions should not be empty");
        assert!(
            output.contains("complete -c batty"),
            "fish completions should contain complete -c batty"
        );
    }

    #[test]
    fn completions_include_grafana_subcommands() {
        let output = generate_completions(clap_complete::Shell::Fish);
        // Top-level grafana command
        assert!(
            output.contains("grafana"),
            "completions should include grafana command"
        );
        // Grafana subcommands
        assert!(
            output.contains("setup"),
            "completions should include grafana setup"
        );
        assert!(
            output.contains("status"),
            "completions should include grafana status"
        );
        assert!(
            output.contains("open"),
            "completions should include grafana open"
        );
    }

    #[test]
    fn completions_include_all_recent_commands() {
        let output = generate_completions(clap_complete::Shell::Fish);
        let expected_commands = [
            "task",
            "metrics",
            "grafana",
            "telemetry",
            "nudge",
            "load",
            "queue",
            "cost",
            "doctor",
            "pause",
            "resume",
        ];
        for cmd in &expected_commands {
            assert!(
                output.contains(cmd),
                "completions should include '{cmd}' command"
            );
        }
    }
}
