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

const SESSION_AFTER_HELP: &str = "Examples:\n  paceflow session                 # default: grouped by model\n  paceflow session --model codex/gpt-5.4\n  paceflow session --overall\n  paceflow session --group-by provider\n  paceflow session --group-by branch\n  paceflow session --branch fix/cursor-new-partial-fate-schema\n  paceflow session --list-sessions\n\nMetrics:\n  Average user prompts: average number of user prompts per session.\n  Avg time to first accepted change: minutes from session start to the first accepted code change.\n  Debug loop rate: share of sessions that look like repeated fix-retry cycles.\n  Error paste rate: share of sessions where an error message was pasted mid-session.\n  Session-to-commit rate: share of sessions followed by a commit within 4 hours.\n  No-output session rate: share of sessions with no accepted code changes.";
const DELIVERY_AFTER_HELP: &str = "Examples:\n  paceflow delivery                # default: grouped by model\n  paceflow delivery --model codex/gpt-5.4\n  paceflow delivery --overall\n  paceflow delivery --group-by provider\n  paceflow delivery --group-by task --task ABC-123\n  paceflow delivery --group-by branch\n  paceflow delivery --branch fix/cursor-new-partial-fate-schema\n\nMetrics:\n  Heavy commits: commits where matched AI-attributed lines are at least half of changed lines.\n  PR sync: completed GitHub PR lookups per heavy commit on github.com (see table column).\n  PR reach rate: among completed lookups, share where a pull request existed.\n  Mainline reach rate: share of heavy AI commits that later reached mainline.\n  Mainline lead: average hours from commit time to mainline reach (prefer a later mainline reach timestamp; otherwise use later PR merged time).\n  PR merge rate: among completed PR-linked lookups, share whose PR merged.";
const QUALITY_AFTER_HELP: &str = "Examples:\n  paceflow quality                 # default: grouped by model\n  paceflow quality --model codex/gpt-5.4\n  paceflow quality --overall\n  paceflow quality --group-by provider\n  paceflow quality --group-by task --task ABC-123\n  paceflow quality --group-by branch\n  paceflow quality --branch fix/cursor-new-partial-fate-schema\n\nMetrics:\n  Code churn rate: share of AI-added lines on heavy AI commits that were removed again within the churn window.\n  Bug-after-merge rate: share of merged heavy AI commits that drew a later fix-like commit within 60 days.\n  Revert rate: share of heavy AI commits that were later reverted.";
const COST_AFTER_HELP: &str = "Examples:\n  paceflow cost                    # default: grouped by model\n  paceflow cost --overall\n  paceflow cost --group-by provider\n  paceflow cost --group-by task --task ABC-123\n  paceflow cost --provider=opencode --all-projects   # cross-repo provider totals\n\nScoped reports default to the current git repo (unless --all-projects). Filters such as --provider still apply after that scope.\n\nMetrics:\n  Cost: API-equivalent model cost when token usage can be priced.\n  Cost/accepted LOC: priced session cost divided by accepted changed lines.\n  Coverage: sessions with priced cost over sessions with token usage.";
const GITHUB_AFTER_HELP: &str = "Examples:\n  paceflow github token\n\nGitHub token setup:\n  Use this command to save, replace, or delete the local GitHub token used for PR sync during ingest.";
const SYNC_AFTER_HELP: &str = "Examples:\n  paceflow sync config\n  paceflow sync status\n  paceflow sync push --all-projects\n  paceflow sync schedule install\n\nSync setup:\n  Use `paceflow sync config` to authenticate with the PaceFlow backend and choose a default organization.\n  Sync uploads normalized local analytics events so shared org views stay consistent across devices.";
const SYNC_SCHEDULE_AFTER_HELP: &str = "Examples:\n  paceflow sync schedule install\n  paceflow sync schedule status\n  paceflow sync schedule uninstall\n  paceflow sync schedule run\n\nSchedule setup:\n  Installs a user-level Paceflow schedule that runs ingest and sync push --all-projects every 6 hours.";
const HOOKS_AFTER_HELP: &str = "Examples:\n  paceflow hooks install                         # install the pre-commit setup gate in the current repo\n  paceflow hooks install --repo /path/to/repo    # install in a specific repo\n  paceflow hooks status\n  paceflow hooks pre-commit --repo .             # dry-run the gate against the current repo\n  paceflow hooks uninstall\n\nHook setup:\n  Paceflow-managed hooks verify that sync is configured locally and that the periodic sync schedule can be installed.\n  The pre-commit gate fails the commit if `paceflow sync config` has not been run (or the PACEFLOW_SYNC_* env vars are not set), and installs the periodic sync schedule on first use.";

/// Version string baked at build time, e.g.
/// `0.2.0 (abc123def456 clean, 2026-05-13T15:00:00+03:00)`.
/// The git metadata comes from `build.rs`; if `git` is unavailable the
/// fields fall back to `unknown` so the binary still reports a sensible
/// version.
const VERSION: &str = concat!(
    env!("CARGO_PKG_VERSION"),
    " (",
    env!("PACEFLOW_GIT_HASH"),
    " ",
    env!("PACEFLOW_GIT_DIRTY"),
    ", ",
    env!("PACEFLOW_GIT_COMMIT_TIME"),
    ")"
);
const TOP_LEVEL_AFTER_HELP: &str = "Quick start:\n  paceflow ingest\n  paceflow session\n  paceflow delivery\n  paceflow quality\n  paceflow cost\n\nStart here:\n  paceflow session       # default: compare workflow trust by model\n  paceflow delivery      # default: compare ship-rate by model\n  paceflow quality       # default: compare durability by model\n  paceflow cost          # default: compare spend by model\n\nTeam setup:\n  paceflow github token              # save the GitHub token used for PR sync\n  paceflow sync config               # authenticate and pick a default PaceFlow org\n  paceflow sync schedule install     # install the 6-hour ingest + push schedule\n  paceflow hooks install             # install the pre-commit setup gate in this repo\n\nManual validation:\n  paceflow event-stream --stream session-base\n\nDiscover options:\n  paceflow --help\n  paceflow <command> --help";

#[derive(Parser)]
#[command(
    name = "paceflow",
    version = VERSION,
    about = "Local-first analytics for improving agent-assisted engineering outcomes",
    after_help = TOP_LEVEL_AFTER_HELP
)]
pub struct Cli {
    #[arg(short, long, global = true)]
    pub verbose: bool,
    #[command(subcommand)]
    pub command: Commands,
}

#[derive(Subcommand)]
pub enum Commands {
    /// Build the analytics data model from local sessions, code changes, and git history
    Ingest,
    /// Show session efficiency and delivery metrics
    Session(SessionReportArgs),
    /// Show commit attribution and merge outcome metrics
    Delivery(DeliveryReportArgs),
    /// Show churn, bug-fix, and revert follow-through for heavy AI commits
    Quality(QualityReportArgs),
    /// Show token usage and estimated cost per useful output
    Cost(CostReportArgs),
    /// Print analytics-ready base-view rows as NDJSON for manual validation
    EventStream(EventStreamArgs),
    #[command(name = "github")]
    /// Manage GitHub token setup for live PR sync
    GitHub(GitHubArgs),
    #[command(name = "sync")]
    /// Configure and push shared analytics sync to the PaceFlow backend
    Sync(SyncArgs),
    #[command(name = "hooks")]
    /// Install and manage Paceflow git hooks
    Hooks(HooksArgs),
}

#[derive(Args, Debug, Clone)]
#[command(after_help = GITHUB_AFTER_HELP)]
pub struct GitHubArgs {
    #[command(subcommand)]
    pub command: GitHubCommands,
}

#[derive(Subcommand, Debug, Clone)]
pub enum GitHubCommands {
    /// Save, replace, or delete the local GitHub token
    Token,
}

#[derive(Args, Debug, Clone)]
#[command(after_help = SYNC_AFTER_HELP)]
pub struct SyncArgs {
    #[command(subcommand)]
    pub command: SyncCommands,
}

#[derive(Subcommand, Debug, Clone)]
pub enum SyncCommands {
    /// Authenticate and save the default PaceFlow organization for sync
    Config,
    /// Upload pending normalized analytics events for the current repo or all projects
    Push(SyncPushArgs),
    /// Show local pending sync state and remote org sync status
    Status(SyncStatusArgs),
    /// Install, inspect, or run the periodic all-projects sync schedule
    Schedule(SyncScheduleArgs),
    /// Delete saved sync credentials and clear local sync cursors
    Reset,
}

#[derive(Args, Debug, Clone)]
#[command(after_help = SYNC_SCHEDULE_AFTER_HELP)]
pub struct SyncScheduleArgs {
    #[command(subcommand)]
    pub command: SyncScheduleCommands,
}

#[derive(Subcommand, Debug, Clone)]
pub enum SyncScheduleCommands {
    /// Install or update the user-level periodic all-projects sync schedule
    Install,
    /// Show whether the periodic all-projects sync schedule is installed
    Status,
    /// Remove the Paceflow-managed periodic all-projects sync schedule
    Uninstall,
    /// Run one scheduled ingest and all-projects sync push pass
    Run,
}

#[derive(Args, Debug, Clone)]
#[command(after_help = HOOKS_AFTER_HELP)]
pub struct HooksArgs {
    #[command(subcommand)]
    pub command: HooksCommands,
}

#[derive(Subcommand, Debug, Clone)]
pub enum HooksCommands {
    /// Install the Paceflow-managed pre-commit setup gate
    Install(HooksRepoArgs),
    /// Remove the Paceflow-managed pre-commit setup gate
    Uninstall(HooksRepoArgs),
    /// Show whether the Paceflow-managed pre-commit hook is installed
    Status(HooksRepoArgs),
    /// Run the local-only pre-commit setup gate
    #[command(name = "pre-commit")]
    PreCommit(HooksRepoArgs),
}

#[derive(Args, Debug, Clone)]
pub struct HooksRepoArgs {
    /// Restrict hook management to a specific repository root or path inside a repository
    #[arg(long)]
    pub repo: Option<String>,
}

#[derive(Args, Debug, Clone)]
pub struct SyncPushArgs {
    /// Show results across all tracked projects instead of defaulting to the current repo
    #[arg(long)]
    pub all_projects: bool,
    /// Restrict sync to a specific repository root
    #[arg(long)]
    pub repo: Option<String>,
    /// Max number of events to upload per request
    #[arg(long, default_value_t = 500)]
    pub batch_size: usize,
}

#[derive(Args, Debug, Clone)]
pub struct SyncStatusArgs {
    /// Show results across all tracked projects instead of defaulting to the current repo
    #[arg(long)]
    pub all_projects: bool,
    /// Restrict status to a specific repository root
    #[arg(long)]
    pub repo: Option<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum GroupBy {
    Repo,
    Provider,
    Task,
    Branch,
    Model,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum EventCategory {
    Session,
    Delivery,
    Quality,
    All,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum EventStreamKind {
    SessionBase,
    TaskSessionBase,
    ChangeBase,
    CommitSessionBase,
    TaskCommitBase,
    All,
}

#[derive(Args, Debug, Clone)]
pub struct ReportArgs {
    /// Bucket results by calendar week
    #[arg(long)]
    pub weekly: bool,
    /// Group aggregated results by a dimension
    #[arg(long, value_enum)]
    pub group_by: Option<GroupBy>,
    /// Inclusive start date (YYYY-MM-DD)
    #[arg(long, value_name = "YYYY-MM-DD")]
    pub from: Option<String>,
    /// Inclusive end date (YYYY-MM-DD)
    #[arg(long, value_name = "YYYY-MM-DD")]
    pub to: Option<String>,
    /// Restrict to a specific repository root
    #[arg(long)]
    pub repo: Option<String>,
    /// Show results across all tracked projects instead of defaulting to the current repo
    #[arg(long)]
    pub all_projects: bool,
    /// Restrict to a provider (for delivery/quality this can include `human`)
    #[arg(long)]
    pub provider: Option<String>,
    /// Restrict to a specific task key (ticket format, e.g. ABC-123)
    #[arg(long)]
    pub task: Option<String>,
    /// Restrict to a specific branch name
    #[arg(long)]
    pub branch: Option<String>,
    /// Restrict to a specific model name
    #[arg(long)]
    pub model: Option<String>,
    /// Max number of grouped rows to display
    #[arg(long, default_value_t = 50)]
    pub limit: usize,
}

#[derive(Args, Debug, Clone)]
#[command(after_help = SESSION_AFTER_HELP)]
pub struct SessionReportArgs {
    #[command(flatten)]
    pub report: ReportArgs,
    /// Show the overall report instead of the default model-grouped comparison
    #[arg(long, conflicts_with = "group_by")]
    pub overall: bool,
    /// List per-session productivity rows instead of KPI aggregations
    #[arg(long)]
    pub list_sessions: bool,
}

#[derive(Args, Debug, Clone)]
#[command(after_help = DELIVERY_AFTER_HELP)]
pub struct DeliveryReportArgs {
    #[command(flatten)]
    pub report: ReportArgs,
    /// Show the overall report instead of the default model-grouped comparison
    #[arg(long, conflicts_with = "group_by")]
    pub overall: bool,
}

#[derive(Args, Debug, Clone)]
#[command(after_help = QUALITY_AFTER_HELP)]
pub struct QualityReportArgs {
    #[command(flatten)]
    pub report: ReportArgs,
    /// Show the overall report instead of the default model-grouped comparison
    #[arg(long, conflicts_with = "group_by")]
    pub overall: bool,
}

#[derive(Args, Debug, Clone)]
#[command(after_help = COST_AFTER_HELP)]
pub struct CostReportArgs {
    #[command(flatten)]
    pub report: ReportArgs,
    /// Show the overall report instead of the default model-grouped comparison
    #[arg(long, conflicts_with = "group_by")]
    pub overall: bool,
}

#[derive(Args, Debug, Clone)]
pub struct EventStreamArgs {
    /// Restrict output to a KPI category
    #[arg(long, value_enum, default_value_t = EventCategory::All)]
    pub category: EventCategory,
    /// Restrict output to a specific base stream
    #[arg(long, value_enum, default_value_t = EventStreamKind::All)]
    pub stream: EventStreamKind,
    /// Inclusive start date (YYYY-MM-DD)
    #[arg(long, value_name = "YYYY-MM-DD")]
    pub from: Option<String>,
    /// Inclusive end date (YYYY-MM-DD)
    #[arg(long, value_name = "YYYY-MM-DD")]
    pub to: Option<String>,
    /// Restrict to a specific repository root
    #[arg(long)]
    pub repo: Option<String>,
    /// Restrict to a provider (for commit-session streams this can include `human`)
    #[arg(long)]
    pub provider: Option<String>,
    /// Restrict to a specific task key (ticket format, e.g. ABC-123)
    #[arg(long)]
    pub task: Option<String>,
    /// Restrict to a specific model name
    #[arg(long)]
    pub model: Option<String>,
    /// Max number of stream rows to display
    #[arg(long)]
    pub limit: Option<usize>,
    /// Pretty-print each event as formatted JSON instead of NDJSON
    #[arg(long)]
    pub pretty: bool,
}

#[cfg(test)]
mod tests {
    use super::*;
    use clap::{CommandFactory, Parser, error::ErrorKind};

    #[test]
    fn parses_session_group_by_repo() {
        let cli = Cli::parse_from(["paceflow", "session", "--group-by", "repo"]);
        match cli.command {
            Commands::Session(args) => assert_eq!(args.report.group_by, Some(GroupBy::Repo)),
            _ => panic!("expected session command"),
        }
    }

    #[test]
    fn parses_delivery_weekly_group_by_task() {
        let cli = Cli::parse_from(["paceflow", "delivery", "--weekly", "--group-by", "task"]);
        match cli.command {
            Commands::Delivery(args) => {
                assert!(args.report.weekly);
                assert_eq!(args.report.group_by, Some(GroupBy::Task));
            }
            _ => panic!("expected delivery command"),
        }
    }

    #[test]
    fn parses_quality_group_by_branch() {
        let cli = Cli::parse_from(["paceflow", "quality", "--group-by", "branch"]);
        match cli.command {
            Commands::Quality(args) => assert_eq!(args.report.group_by, Some(GroupBy::Branch)),
            _ => panic!("expected quality command"),
        }
    }

    #[test]
    fn parses_quality_model_filter() {
        let cli = Cli::parse_from(["paceflow", "quality", "--model", "gpt-5"]);
        match cli.command {
            Commands::Quality(args) => assert_eq!(args.report.model.as_deref(), Some("gpt-5")),
            _ => panic!("expected quality command"),
        }
    }

    #[test]
    fn parses_cost_group_by_task() {
        let cli = Cli::parse_from(["paceflow", "cost", "--group-by", "task"]);
        match cli.command {
            Commands::Cost(args) => assert_eq!(args.report.group_by, Some(GroupBy::Task)),
            _ => panic!("expected cost command"),
        }
    }

    #[test]
    fn parses_report_branch_filter() {
        let cli = Cli::parse_from(["paceflow", "session", "--branch", "fix/test"]);
        match cli.command {
            Commands::Session(args) => assert_eq!(args.report.branch.as_deref(), Some("fix/test")),
            _ => panic!("expected session command"),
        }
    }

    #[test]
    fn parses_report_all_projects_flag() {
        let cli = Cli::parse_from(["paceflow", "session", "--all-projects"]);
        match cli.command {
            Commands::Session(args) => assert!(args.report.all_projects),
            _ => panic!("expected session command"),
        }
    }

    #[test]
    fn parses_session_overall_flag() {
        let cli = Cli::parse_from(["paceflow", "session", "--overall"]);
        match cli.command {
            Commands::Session(args) => assert!(args.overall),
            _ => panic!("expected session command"),
        }
    }

    #[test]
    fn overall_conflicts_with_group_by() {
        let result =
            Cli::try_parse_from(["paceflow", "delivery", "--overall", "--group-by", "model"]);
        assert!(result.is_err());
        let err = result.err().expect("expected clap conflict");
        assert_eq!(err.kind(), ErrorKind::ArgumentConflict);
    }

    #[test]
    fn parses_event_stream_defaults() {
        let cli = Cli::parse_from(["paceflow", "event-stream"]);
        match cli.command {
            Commands::EventStream(args) => {
                assert_eq!(args.category, EventCategory::All);
                assert_eq!(args.stream, EventStreamKind::All);
                assert!(!args.pretty);
            }
            _ => panic!("expected event-stream command"),
        }
    }

    #[test]
    fn parses_event_stream_category_session() {
        let cli = Cli::parse_from(["paceflow", "event-stream", "--category", "session"]);
        match cli.command {
            Commands::EventStream(args) => assert_eq!(args.category, EventCategory::Session),
            _ => panic!("expected event-stream command"),
        }
    }

    #[test]
    fn parses_event_stream_delivery_category_and_task_filter() {
        let cli = Cli::parse_from([
            "paceflow",
            "event-stream",
            "--category",
            "delivery",
            "--stream",
            "task-commit-base",
            "--task",
            "PAC-999",
        ]);
        match cli.command {
            Commands::EventStream(args) => {
                assert_eq!(args.category, EventCategory::Delivery);
                assert_eq!(args.stream, EventStreamKind::TaskCommitBase);
                assert_eq!(args.task.as_deref(), Some("PAC-999"));
            }
            _ => panic!("expected event-stream command"),
        }
    }

    #[test]
    fn parses_event_stream_pretty_flag() {
        let cli = Cli::parse_from(["paceflow", "event-stream", "--pretty"]);
        match cli.command {
            Commands::EventStream(args) => assert!(args.pretty),
            _ => panic!("expected event-stream command"),
        }
    }

    #[test]
    fn parses_github_token_command() {
        let cli = Cli::parse_from(["paceflow", "github", "token"]);
        match cli.command {
            Commands::GitHub(args) => match args.command {
                GitHubCommands::Token => {}
            },
            _ => panic!("expected github command"),
        }
    }

    #[test]
    fn parses_sync_push_all_projects() {
        let cli = Cli::parse_from(["paceflow", "sync", "push", "--all-projects"]);
        match cli.command {
            Commands::Sync(args) => match args.command {
                SyncCommands::Push(push) => assert!(push.all_projects),
                _ => panic!("expected sync push command"),
            },
            _ => panic!("expected sync command"),
        }
    }

    #[test]
    fn parses_sync_status_repo_filter() {
        let cli = Cli::parse_from(["paceflow", "sync", "status", "--repo", "/tmp/repo"]);
        match cli.command {
            Commands::Sync(args) => match args.command {
                SyncCommands::Status(status) => {
                    assert_eq!(status.repo.as_deref(), Some("/tmp/repo"))
                }
                _ => panic!("expected sync status command"),
            },
            _ => panic!("expected sync command"),
        }
    }

    #[test]
    fn parses_sync_schedule_commands() {
        for (command, expected) in [
            ("install", "install"),
            ("status", "status"),
            ("uninstall", "uninstall"),
            ("run", "run"),
        ] {
            let cli = Cli::parse_from(["paceflow", "sync", "schedule", command]);
            match cli.command {
                Commands::Sync(args) => match args.command {
                    SyncCommands::Schedule(schedule) => match (schedule.command, expected) {
                        (SyncScheduleCommands::Install, "install") => {}
                        (SyncScheduleCommands::Status, "status") => {}
                        (SyncScheduleCommands::Uninstall, "uninstall") => {}
                        (SyncScheduleCommands::Run, "run") => {}
                        _ => panic!("unexpected sync schedule command"),
                    },
                    _ => panic!("expected sync schedule command"),
                },
                _ => panic!("expected sync command"),
            }
        }
    }

    #[test]
    fn parses_hooks_install_repo_filter() {
        let cli = Cli::parse_from(["paceflow", "hooks", "install", "--repo", "/tmp/repo"]);
        match cli.command {
            Commands::Hooks(args) => match args.command {
                HooksCommands::Install(hooks) => {
                    assert_eq!(hooks.repo.as_deref(), Some("/tmp/repo"))
                }
                _ => panic!("expected hooks install command"),
            },
            _ => panic!("expected hooks command"),
        }
    }

    #[test]
    fn parses_hooks_pre_commit_repo_filter() {
        let cli = Cli::parse_from(["paceflow", "hooks", "pre-commit", "--repo", "/tmp/repo"]);
        match cli.command {
            Commands::Hooks(args) => match args.command {
                HooksCommands::PreCommit(hooks) => {
                    assert_eq!(hooks.repo.as_deref(), Some("/tmp/repo"))
                }
                _ => panic!("expected hooks pre-commit command"),
            },
            _ => panic!("expected hooks command"),
        }
    }

    #[test]
    fn parses_hooks_status_repo_filter() {
        let cli = Cli::parse_from(["paceflow", "hooks", "status", "--repo", "/tmp/repo"]);
        match cli.command {
            Commands::Hooks(args) => match args.command {
                HooksCommands::Status(hooks) => {
                    assert_eq!(hooks.repo.as_deref(), Some("/tmp/repo"))
                }
                _ => panic!("expected hooks status command"),
            },
            _ => panic!("expected hooks command"),
        }
    }

    #[test]
    fn parses_hooks_uninstall_repo_filter() {
        let cli = Cli::parse_from(["paceflow", "hooks", "uninstall", "--repo", "/tmp/repo"]);
        match cli.command {
            Commands::Hooks(args) => match args.command {
                HooksCommands::Uninstall(hooks) => {
                    assert_eq!(hooks.repo.as_deref(), Some("/tmp/repo"))
                }
                _ => panic!("expected hooks uninstall command"),
            },
            _ => panic!("expected hooks command"),
        }
    }

    #[test]
    fn rejects_legacy_change_and_lifecycle_commands() {
        assert!(Cli::try_parse_from(["paceflow", "change"]).is_err());
        assert!(Cli::try_parse_from(["paceflow", "lifecycle"]).is_err());
    }

    #[test]
    fn session_help_explains_metrics() {
        let mut command = Cli::command();
        let mut buffer = Vec::new();
        command
            .find_subcommand_mut("session")
            .expect("session subcommand")
            .write_long_help(&mut buffer)
            .expect("write session help");
        let help = String::from_utf8(buffer).expect("utf8");

        assert!(help.contains("Average user prompts"));
        assert!(help.contains("Debug loop rate"));
        assert!(help.contains("Session-to-commit rate"));
    }

    #[test]
    fn version_embeds_pkg_version_and_git_metadata() {
        let cmd = Cli::command();
        let version = cmd.get_version().expect("version should be set on Cli");

        assert!(
            version.starts_with(env!("CARGO_PKG_VERSION")),
            "version should start with the Cargo package version: got {version:?}"
        );
        assert!(
            version.contains(env!("PACEFLOW_GIT_HASH")),
            "version should include the git hash from build.rs: got {version:?}"
        );
        let dirty_flag = env!("PACEFLOW_GIT_DIRTY");
        assert!(
            ["clean", "dirty", "unknown"].contains(&dirty_flag),
            "PACEFLOW_GIT_DIRTY should be clean/dirty/unknown: got {dirty_flag:?}"
        );
        assert!(
            version.contains(dirty_flag),
            "version should include the dirty flag: got {version:?}"
        );
    }

    #[test]
    fn top_level_help_advertises_team_setup_commands() {
        let mut command = Cli::command();
        let mut buffer = Vec::new();
        command
            .write_long_help(&mut buffer)
            .expect("write top-level help");
        let help = String::from_utf8(buffer).expect("utf8");

        assert!(help.contains("Team setup:"));
        assert!(help.contains("paceflow github token"));
        assert!(help.contains("paceflow sync config"));
        assert!(help.contains("paceflow sync schedule install"));
        assert!(help.contains("paceflow hooks install"));
    }

    #[test]
    fn hooks_help_documents_pre_commit_dry_run_and_sync_prereq() {
        let mut command = Cli::command();
        let mut buffer = Vec::new();
        command
            .find_subcommand_mut("hooks")
            .expect("hooks subcommand")
            .write_long_help(&mut buffer)
            .expect("write hooks help");
        let help = String::from_utf8(buffer).expect("utf8");

        assert!(help.contains("paceflow hooks install"));
        assert!(help.contains("paceflow hooks pre-commit --repo ."));
        assert!(help.contains("paceflow sync config"));
    }

    #[test]
    fn delivery_quality_and_cost_help_explain_metrics_and_human_provider_context() {
        let mut command = Cli::command();
        let mut delivery_buffer = Vec::new();
        command
            .find_subcommand_mut("delivery")
            .expect("delivery subcommand")
            .write_long_help(&mut delivery_buffer)
            .expect("write delivery help");
        let delivery_help = String::from_utf8(delivery_buffer).expect("utf8");
        assert!(delivery_help.contains("Heavy commits"));
        assert!(delivery_help.contains("PR reach rate"));
        assert!(delivery_help.contains("Mainline reach rate"));
        assert!(delivery_help.contains("Mainline lead"));
        assert!(delivery_help.contains("PR merge rate"));

        let mut command = Cli::command();
        let mut quality_buffer = Vec::new();
        command
            .find_subcommand_mut("quality")
            .expect("quality subcommand")
            .write_long_help(&mut quality_buffer)
            .expect("write quality help");
        let quality_help = String::from_utf8(quality_buffer).expect("utf8");
        assert!(quality_help.contains("Code churn rate"));
        assert!(quality_help.contains("Bug-after-merge rate"));
        assert!(quality_help.contains("Revert rate"));

        let mut command = Cli::command();
        let mut cost_buffer = Vec::new();
        command
            .find_subcommand_mut("cost")
            .expect("cost subcommand")
            .write_long_help(&mut cost_buffer)
            .expect("write cost help");
        let cost_help = String::from_utf8(cost_buffer).expect("utf8");
        assert!(cost_help.contains("API-equivalent model cost"));
        assert!(cost_help.contains("Cost/accepted LOC"));
        assert!(cost_help.contains("Coverage"));
    }
}
