use std::path::PathBuf;

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

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum OutputFormat {
    Cli,
    Json,
}

#[derive(Debug, Parser)]
#[command(name = "kbolt", version, about = "local-first retrieval engine")]
pub struct Cli {
    #[arg(
        short = 's',
        long = "space",
        value_name = "name",
        help = "Active space (overrides KBOLT_SPACE and the default space)"
    )]
    pub space: Option<String>,

    #[arg(
        short = 'f',
        long = "format",
        value_enum,
        default_value_t = OutputFormat::Cli,
        help = "Output format"
    )]
    pub format: OutputFormat,

    #[command(subcommand)]
    pub command: Command,
}

#[derive(Debug, Subcommand)]
pub enum Command {
    #[command(about = "Check system configuration and model readiness")]
    Doctor,
    #[command(about = "Configure kbolt with a local inference stack")]
    Setup(SetupArgs),
    #[command(about = "Manage local llama-server processes")]
    Local(LocalArgs),
    #[command(about = "Create, list, and manage spaces")]
    Space(SpaceArgs),
    #[command(about = "Add, list, and manage document collections")]
    Collection(CollectionArgs),
    #[command(about = "Manage file ignore patterns for collections")]
    Ignore(IgnoreArgs),
    #[command(about = "Show configured model bindings")]
    Models(ModelsArgs),
    #[command(about = "Run retrieval benchmarks")]
    Eval(EvalArgs),
    #[command(about = "Manage automatic re-indexing schedules")]
    Schedule(ScheduleArgs),
    #[command(about = "Keep collections fresh as files change")]
    Watch(WatchArgs),
    #[command(about = "Start the MCP server for AI agent integration")]
    Mcp,
    #[command(about = "Search indexed documents")]
    Search(SearchArgs),
    #[command(about = "Re-scan and re-index collections")]
    Update(UpdateArgs),
    #[command(about = "Show index status, disk usage, and model readiness")]
    Status,
    #[command(about = "List files in a collection")]
    Ls(LsArgs),
    #[command(about = "Retrieve a document by path or docid")]
    Get(GetArgs),
    #[command(about = "Retrieve multiple documents at once")]
    MultiGet(MultiGetArgs),
}

#[derive(Debug, Args)]
pub struct SpaceArgs {
    #[command(subcommand)]
    pub command: SpaceCommand,
}

#[derive(Debug, Args)]
pub struct SetupArgs {
    #[command(subcommand)]
    pub command: SetupCommand,
}

#[derive(Debug, Args)]
pub struct LocalArgs {
    #[command(subcommand)]
    pub command: LocalCommand,
}

#[derive(Debug, Args)]
pub struct CollectionArgs {
    #[command(subcommand)]
    pub command: CollectionCommand,
}

#[derive(Debug, Args)]
pub struct IgnoreArgs {
    #[command(subcommand)]
    pub command: IgnoreCommand,
}

#[derive(Debug, Args)]
pub struct ModelsArgs {
    #[command(subcommand)]
    pub command: ModelsCommand,
}

#[derive(Debug, Args)]
pub struct EvalArgs {
    #[command(subcommand)]
    pub command: EvalCommand,
}

#[derive(Debug, Args, PartialEq, Eq)]
pub struct EvalImportArgs {
    #[command(subcommand)]
    pub dataset: EvalImportCommand,
}

#[derive(Debug, Args, PartialEq, Eq)]
pub struct EvalRunArgs {
    #[arg(
        long,
        value_name = "path",
        help = "Path to an eval.toml manifest (defaults to the configured eval set)"
    )]
    pub file: Option<PathBuf>,
}

#[derive(Debug, Args)]
pub struct ScheduleArgs {
    #[command(subcommand)]
    pub command: ScheduleCommand,
}

#[derive(Debug, Args)]
pub struct WatchArgs {
    #[arg(
        long,
        help = "Run the watcher attached to this terminal for debugging or custom supervision"
    )]
    pub foreground: bool,
    #[command(subcommand)]
    pub command: Option<WatchCommand>,
}

#[derive(Debug, Args, PartialEq, Eq)]
pub struct UpdateArgs {
    #[arg(
        long = "collection",
        value_delimiter = ',',
        help = "Restrict update to specific collections (comma-separated)"
    )]
    pub collections: Vec<String>,
    #[arg(long, help = "Skip embedding; only refresh keyword index and metadata")]
    pub no_embed: bool,
    #[arg(long, help = "Show what would change without writing to the index")]
    pub dry_run: bool,
    #[arg(
        long,
        help = "Include per-file decisions and the full error list in the final report"
    )]
    pub verbose: bool,
}

#[derive(Debug, Args, PartialEq, Eq)]
pub struct LsArgs {
    #[arg(help = "Collection to list files from")]
    pub collection: String,
    #[arg(help = "Only show files whose path starts with this prefix")]
    pub prefix: Option<String>,
    #[arg(long, help = "Include deactivated files")]
    pub all: bool,
}

#[derive(Debug, Args, PartialEq, Eq)]
pub struct GetArgs {
    #[arg(help = "Document path (collection/relative/path) or docid (#abc123)")]
    pub identifier: String,
    #[arg(long, help = "Start reading at this line number")]
    pub offset: Option<usize>,
    #[arg(long, help = "Maximum number of lines to return")]
    pub limit: Option<usize>,
}

#[derive(Debug, Args, PartialEq, Eq)]
pub struct MultiGetArgs {
    #[arg(
        value_delimiter = ',',
        help = "Comma-separated list of document paths or docids (#abc123)"
    )]
    pub locators: Vec<String>,
    #[arg(
        long,
        default_value_t = 20,
        help = "Maximum number of documents to return"
    )]
    pub max_files: usize,
    #[arg(
        long,
        default_value_t = 51_200,
        help = "Maximum total bytes to return across all documents"
    )]
    pub max_bytes: usize,
}

#[derive(Debug, Args, PartialEq)]
pub struct SearchArgs {
    #[arg(help = "The search query")]
    pub query: String,
    #[arg(
        long = "collection",
        value_delimiter = ',',
        help = "Restrict search to specific collections (comma-separated)"
    )]
    pub collections: Vec<String>,
    #[arg(
        long,
        default_value_t = 10,
        help = "Maximum number of results to return"
    )]
    pub limit: usize,
    #[arg(
        long,
        default_value_t = 0.0,
        help = "Filter out results below this score (0.0-1.0)"
    )]
    pub min_score: f32,
    #[arg(
        long,
        help = "Query expansion for vocabulary-mismatch or underspecified queries (slower)"
    )]
    pub deep: bool,
    #[arg(long, help = "Keyword-only (BM25) search; skips dense retrieval")]
    pub keyword: bool,
    #[arg(long, help = "Dense-vector-only search; skips keyword retrieval")]
    pub semantic: bool,
    #[arg(
        long,
        conflicts_with = "rerank",
        help = "Skip cross-encoder reranking (faster, lower quality)"
    )]
    pub no_rerank: bool,
    #[arg(
        long,
        conflicts_with = "no_rerank",
        help = "Enable cross-encoder reranking on auto mode (slower, higher quality)"
    )]
    pub rerank: bool,
    #[arg(
        long,
        help = "Show pipeline stages and per-signal scores for each result"
    )]
    pub debug: bool,
}

#[derive(Debug, Subcommand, PartialEq, Eq)]
pub enum SpaceCommand {
    #[command(about = "Create a new space")]
    Add {
        #[arg(help = "Name of the new space")]
        name: String,
        #[arg(long, help = "Human-readable space description")]
        description: Option<String>,
        #[arg(
            long,
            help = "Validate all directories up-front and roll back the space if any collection registration fails"
        )]
        strict: bool,
        #[arg(help = "Directories to register as collections in this space")]
        dirs: Vec<PathBuf>,
    },
    #[command(about = "Set a space description")]
    Describe {
        #[arg(help = "Space name")]
        name: String,
        #[arg(help = "New description text")]
        text: String,
    },
    #[command(about = "Rename a space")]
    Rename {
        #[arg(help = "Current space name")]
        old: String,
        #[arg(help = "New space name")]
        new: String,
    },
    #[command(about = "Remove a space and all its data")]
    Remove {
        #[arg(help = "Space to delete (all collections and indexes are removed)")]
        name: String,
    },
    #[command(about = "Show the active space")]
    Current,
    #[command(about = "Get or set the default space")]
    Default {
        #[arg(help = "Space to set as default (omit to show the current default)")]
        name: Option<String>,
    },
    #[command(about = "List all spaces")]
    List,
    #[command(about = "Show details about a space")]
    Info {
        #[arg(help = "Space name")]
        name: String,
    },
}

#[derive(Debug, Subcommand, PartialEq, Eq)]
pub enum SetupCommand {
    #[command(about = "Set up local embedder and reranker using llama-server")]
    Local,
}

#[derive(Debug, Subcommand, PartialEq, Eq)]
pub enum LocalCommand {
    #[command(about = "Show local server status")]
    Status,
    #[command(about = "Start local inference servers")]
    Start,
    #[command(about = "Stop local inference servers")]
    Stop,
    #[command(about = "Enable an optional local feature")]
    Enable {
        #[arg(
            help = "Feature to enable (`deep` downloads the expander model for query expansion)"
        )]
        feature: LocalFeature,
    },
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum LocalFeature {
    Deep,
}

#[derive(Debug, Subcommand, PartialEq, Eq)]
pub enum CollectionCommand {
    #[command(about = "Add a directory as a document collection")]
    Add {
        #[arg(help = "Directory to index")]
        path: PathBuf,
        #[arg(long, help = "Collection name (defaults to the directory basename)")]
        name: Option<String>,
        #[arg(long, help = "Human-readable collection description")]
        description: Option<String>,
        #[arg(
            long,
            value_delimiter = ',',
            help = "Only index files with these extensions (comma-separated)"
        )]
        extensions: Option<Vec<String>>,
        #[arg(
            long,
            help = "Register the collection without running an initial indexing pass"
        )]
        no_index: bool,
    },
    #[command(about = "List all collections")]
    List,
    #[command(about = "Show details about a collection")]
    Info {
        #[arg(help = "Collection name")]
        name: String,
    },
    #[command(about = "Set a collection description")]
    Describe {
        #[arg(help = "Collection name")]
        name: String,
        #[arg(help = "New description text")]
        text: String,
    },
    #[command(about = "Rename a collection")]
    Rename {
        #[arg(help = "Current collection name")]
        old: String,
        #[arg(help = "New collection name")]
        new: String,
    },
    #[command(about = "Remove a collection and its indexed data")]
    Remove {
        #[arg(help = "Collection to delete (chunks and embeddings are removed)")]
        name: String,
    },
}

#[derive(Debug, Subcommand, PartialEq, Eq)]
pub enum IgnoreCommand {
    #[command(about = "Show ignore patterns for a collection")]
    Show {
        #[arg(help = "Collection name")]
        collection: String,
    },
    #[command(about = "Add an ignore pattern to a collection")]
    Add {
        #[arg(help = "Collection name")]
        collection: String,
        #[arg(help = "Gitignore-style pattern to add")]
        pattern: String,
    },
    #[command(about = "Remove an ignore pattern from a collection")]
    Remove {
        #[arg(help = "Collection name")]
        collection: String,
        #[arg(help = "Exact pattern text to remove")]
        pattern: String,
    },
    #[command(about = "Open ignore patterns in an editor")]
    Edit {
        #[arg(help = "Collection name")]
        collection: String,
    },
    #[command(about = "List all collections with ignore patterns")]
    List,
}

#[derive(Debug, Subcommand, PartialEq, Eq)]
pub enum ModelsCommand {
    #[command(about = "List configured models and their status")]
    List,
}

#[derive(Debug, Subcommand, PartialEq, Eq)]
pub enum EvalCommand {
    #[command(about = "Run a retrieval evaluation")]
    Run(EvalRunArgs),
    #[command(about = "Import a benchmark dataset")]
    Import(EvalImportArgs),
}

#[derive(Debug, Subcommand, PartialEq, Eq)]
pub enum EvalImportCommand {
    #[command(
        about = "import a canonical BEIR dataset from an extracted directory",
        long_about = "Import a canonical BEIR dataset from an extracted directory.\n\nExpected source layout:\n  corpus.jsonl\n  queries.jsonl\n  qrels/test.tsv\n\nThis command always imports the test split."
    )]
    Beir(EvalImportBeirArgs),
}

#[derive(Debug, Args, PartialEq, Eq)]
pub struct EvalImportBeirArgs {
    #[arg(
        long,
        value_name = "name",
        help = "Dataset identifier used in eval reports (e.g. fiqa, scifact)"
    )]
    pub dataset: String,
    #[arg(
        long,
        value_name = "dir",
        help = "Extracted BEIR dataset directory (corpus.jsonl, queries.jsonl, qrels/)"
    )]
    pub source: PathBuf,
    #[arg(
        long,
        value_name = "dir",
        help = "Directory where the imported corpus and eval.toml will be written"
    )]
    pub output: PathBuf,
    #[arg(
        long,
        value_name = "name",
        help = "Override the collection name (defaults to the dataset name)"
    )]
    pub collection: Option<String>,
}

#[derive(Debug, Subcommand, PartialEq, Eq)]
pub enum ScheduleCommand {
    #[command(about = "Create a new re-indexing schedule")]
    Add(ScheduleAddArgs),
    #[command(about = "Show schedule status and last run info")]
    Status,
    #[command(about = "Remove a schedule")]
    Remove(ScheduleRemoveArgs),
}

#[derive(Debug, Subcommand, PartialEq, Eq)]
pub enum WatchCommand {
    #[command(about = "Enable and start the background watcher")]
    Enable,
    #[command(about = "Disable the background watcher")]
    Disable,
    #[command(about = "Show watcher service and runtime status")]
    Status,
    #[command(about = "Show recent watcher activity")]
    Logs {
        #[arg(long, default_value_t = 80, help = "Number of log lines to show")]
        lines: usize,
    },
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum ScheduleDayArg {
    Mon,
    Tue,
    Wed,
    Thu,
    Fri,
    Sat,
    Sun,
}

#[derive(Debug, Args, PartialEq, Eq)]
#[command(group(
    ArgGroup::new("trigger")
        .required(true)
        .args(["every", "at"])
))]
pub struct ScheduleAddArgs {
    #[arg(
        long,
        conflicts_with = "at",
        help = "Interval trigger (e.g. 30m, 2h); minimum 5 minutes"
    )]
    pub every: Option<String>,
    #[arg(
        long,
        conflicts_with = "every",
        help = "Daily trigger time in HH:MM (24-hour)"
    )]
    pub at: Option<String>,
    #[arg(
        long = "on",
        value_delimiter = ',',
        requires = "at",
        value_enum,
        help = "Days for the weekly trigger (comma-separated: Mon,Tue,...); requires --at"
    )]
    pub on: Vec<ScheduleDayArg>,
    #[arg(long, help = "Restrict the schedule to a specific space")]
    pub space: Option<String>,
    #[arg(
        long = "collection",
        requires = "space",
        help = "Restrict the schedule to specific collections; requires --space"
    )]
    pub collections: Vec<String>,
}

#[derive(Debug, Args, PartialEq, Eq)]
#[command(group(
    ArgGroup::new("selector")
        .required(true)
        .args(["id", "all", "space"])
))]
pub struct ScheduleRemoveArgs {
    #[arg(help = "Schedule ID to remove (from `kbolt schedule status`)")]
    pub id: Option<String>,
    #[arg(
        long,
        conflicts_with_all = ["id", "space", "collections"],
        help = "Remove every configured schedule"
    )]
    pub all: bool,
    #[arg(
        long,
        conflicts_with = "id",
        help = "Remove all schedules for a specific space"
    )]
    pub space: Option<String>,
    #[arg(
        long = "collection",
        requires = "space",
        conflicts_with = "id",
        help = "Remove schedules for specific collections; requires --space"
    )]
    pub collections: Vec<String>,
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use clap::Parser;

    use super::{
        Cli, CollectionCommand, Command, EvalCommand, EvalImportArgs, EvalImportBeirArgs,
        EvalImportCommand, EvalRunArgs, GetArgs, LocalCommand, LocalFeature, MultiGetArgs,
        OutputFormat, ScheduleAddArgs, ScheduleCommand, ScheduleDayArg, ScheduleRemoveArgs,
        SearchArgs, SetupCommand, SpaceCommand, UpdateArgs, WatchArgs, WatchCommand,
    };

    fn parse<const N: usize>(args: [&str; N]) -> Cli {
        Cli::try_parse_from(args).expect("parse cli")
    }

    #[test]
    fn parses_output_format_variants() {
        let parsed = parse(["kbolt", "status"]);
        assert_eq!(parsed.format, OutputFormat::Cli);
        let parsed = parse(["kbolt", "--format", "json", "status"]);
        assert_eq!(parsed.format, OutputFormat::Json);
    }

    #[test]
    fn parses_doctor_command() {
        let parsed = parse(["kbolt", "doctor"]);
        assert!(matches!(parsed.command, Command::Doctor));
    }

    #[test]
    fn parses_setup_local_command() {
        let parsed = parse(["kbolt", "setup", "local"]);
        assert!(matches!(
            parsed.command,
            Command::Setup(args) if args.command == SetupCommand::Local
        ));
    }

    #[test]
    fn parses_local_enable_deep_command() {
        let parsed = parse(["kbolt", "local", "enable", "deep"]);
        assert!(matches!(
            parsed.command,
            Command::Local(args)
                if args.command == LocalCommand::Enable {
                    feature: LocalFeature::Deep
                }
        ));
    }

    #[test]
    fn parses_global_space_override() {
        let parsed = parse(["kbolt", "--space", "work", "space", "current"]);
        assert_eq!(parsed.space.as_deref(), Some("work"));
        assert!(matches!(
            parsed.command,
            Command::Space(space) if space.command == SpaceCommand::Current
        ));
    }

    #[test]
    fn parses_collection_add_with_options() {
        let parsed = parse([
            "kbolt",
            "collection",
            "add",
            "/tmp/work-api",
            "--name",
            "api",
            "--description",
            "api docs",
            "--extensions",
            "rs,md",
            "--no-index",
        ]);
        assert_eq!(parsed.space, None);

        assert!(matches!(
            parsed.command,
            Command::Collection(collection)
                if collection.command
                    == CollectionCommand::Add {
                        path: PathBuf::from("/tmp/work-api"),
                        name: Some("api".to_string()),
                        description: Some("api docs".to_string()),
                        extensions: Some(vec!["rs".to_string(), "md".to_string()]),
                        no_index: true
                    }
        ));
    }

    #[test]
    fn parses_update_with_defaults() {
        let parsed = parse(["kbolt", "update"]);
        assert_eq!(parsed.space, None);
        assert!(matches!(
            parsed.command,
            Command::Update(UpdateArgs {
                collections,
                no_embed: false,
                dry_run: false,
                verbose: false,
            }) if collections.is_empty()
        ));
    }

    #[test]
    fn parses_update_with_flags() {
        let parsed = parse([
            "kbolt",
            "--space",
            "work",
            "update",
            "--collection",
            "api,wiki",
            "--no-embed",
            "--dry-run",
            "--verbose",
        ]);
        assert_eq!(parsed.space.as_deref(), Some("work"));
        assert!(matches!(
            parsed.command,
            Command::Update(UpdateArgs {
                collections,
                no_embed: true,
                dry_run: true,
                verbose: true,
            }) if collections == vec!["api".to_string(), "wiki".to_string()]
        ));
    }

    #[test]
    fn parses_get_with_options() {
        let parsed = parse(["kbolt", "get", "api/src/lib.rs"]);
        assert_eq!(parsed.space, None);
        assert!(matches!(
            parsed.command,
            Command::Get(GetArgs {
                identifier,
                offset: None,
                limit: None,
            }) if identifier == "api/src/lib.rs"
        ));

        let parsed = parse([
            "kbolt", "--space", "work", "get", "#abc123", "--offset", "10", "--limit", "25",
        ]);
        assert_eq!(parsed.space.as_deref(), Some("work"));
        assert!(matches!(
            parsed.command,
            Command::Get(GetArgs {
                identifier,
                offset: Some(10),
                limit: Some(25),
            }) if identifier == "#abc123"
        ));
    }

    #[test]
    fn parses_multi_get_with_options() {
        let parsed = parse(["kbolt", "multi-get", "api/a.md,#abc123"]);
        assert_eq!(parsed.space, None);
        assert!(matches!(
            parsed.command,
            Command::MultiGet(MultiGetArgs {
                locators,
                max_files: 20,
                max_bytes: 51_200,
            }) if locators == vec!["api/a.md".to_string(), "#abc123".to_string()]
        ));

        let parsed = parse([
            "kbolt",
            "--space",
            "work",
            "multi-get",
            "api/a.md,api/b.md",
            "--max-files",
            "5",
            "--max-bytes",
            "1024",
        ]);
        assert_eq!(parsed.space.as_deref(), Some("work"));
        assert!(matches!(
            parsed.command,
            Command::MultiGet(MultiGetArgs {
                locators,
                max_files: 5,
                max_bytes: 1024,
            }) if locators == vec!["api/a.md".to_string(), "api/b.md".to_string()]
        ));
    }

    #[test]
    fn parses_search_with_defaults_and_flags() {
        let parsed = parse(["kbolt", "search", "alpha"]);
        assert_eq!(parsed.space, None);
        assert!(matches!(
            parsed.command,
            Command::Search(SearchArgs {
                query,
                collections,
                limit: 10,
                min_score,
                deep: false,
                keyword: false,
                semantic: false,
                no_rerank: false,
                rerank: false,
                debug: false,
            }) if query == "alpha" && collections.is_empty() && min_score == 0.0
        ));

        let parsed = parse([
            "kbolt",
            "--space",
            "work",
            "search",
            "alpha beta",
            "--collection",
            "api,wiki",
            "--limit",
            "7",
            "--min-score",
            "0.25",
            "--keyword",
            "--no-rerank",
            "--debug",
        ]);
        assert_eq!(parsed.space.as_deref(), Some("work"));
        assert!(matches!(
            parsed.command,
            Command::Search(SearchArgs {
                query,
                collections,
                limit: 7,
                min_score,
                deep: false,
                keyword: true,
                semantic: false,
                no_rerank: true,
                rerank: false,
                debug: true,
            }) if query == "alpha beta"
                && collections == vec!["api".to_string(), "wiki".to_string()]
                && min_score == 0.25
        ));
    }

    #[test]
    fn parses_search_rerank_opt_in_flag() {
        let parsed = parse(["kbolt", "search", "alpha", "--rerank"]);
        assert!(matches!(
            parsed.command,
            Command::Search(SearchArgs {
                rerank: true,
                no_rerank: false,
                ..
            })
        ));
    }

    #[test]
    fn parses_schedule_add_interval_and_weekly_variants() {
        let parsed = parse(["kbolt", "schedule", "add", "--every", "30m"]);
        assert!(matches!(
            parsed.command,
            Command::Schedule(schedule)
                if schedule.command
                    == ScheduleCommand::Add(ScheduleAddArgs {
                        every: Some("30m".to_string()),
                        at: None,
                        on: vec![],
                        space: None,
                        collections: vec![],
                    })
        ));

        let parsed = parse([
            "kbolt",
            "schedule",
            "add",
            "--at",
            "3pm",
            "--on",
            "mon,fri",
            "--space",
            "work",
            "--collection",
            "api",
            "--collection",
            "docs",
        ]);
        assert!(matches!(
            parsed.command,
            Command::Schedule(schedule)
                if schedule.command
                    == ScheduleCommand::Add(ScheduleAddArgs {
                        every: None,
                        at: Some("3pm".to_string()),
                        on: vec![ScheduleDayArg::Mon, ScheduleDayArg::Fri],
                        space: Some("work".to_string()),
                        collections: vec!["api".to_string(), "docs".to_string()],
                    })
        ));
    }

    #[test]
    fn parses_schedule_remove_selectors() {
        let parsed = parse(["kbolt", "schedule", "remove", "s2"]);
        assert!(matches!(
            parsed.command,
            Command::Schedule(schedule)
                if schedule.command
                    == ScheduleCommand::Remove(ScheduleRemoveArgs {
                        id: Some("s2".to_string()),
                        all: false,
                        space: None,
                        collections: vec![],
                    })
        ));

        let parsed = parse([
            "kbolt",
            "schedule",
            "remove",
            "--space",
            "work",
            "--collection",
            "api",
        ]);
        assert!(matches!(
            parsed.command,
            Command::Schedule(schedule)
                if schedule.command
                    == ScheduleCommand::Remove(ScheduleRemoveArgs {
                        id: None,
                        all: false,
                        space: Some("work".to_string()),
                        collections: vec!["api".to_string()],
                    })
        ));
    }

    #[test]
    fn parses_watch_commands_and_foreground_flag() {
        let parsed = parse(["kbolt", "watch"]);
        assert!(matches!(
            parsed.command,
            Command::Watch(WatchArgs {
                foreground: false,
                command: None,
            })
        ));

        let parsed = parse(["kbolt", "watch", "enable"]);
        assert!(matches!(
            parsed.command,
            Command::Watch(WatchArgs {
                foreground: false,
                command: Some(WatchCommand::Enable),
            })
        ));

        let parsed = parse(["kbolt", "watch", "--foreground"]);
        assert!(matches!(
            parsed.command,
            Command::Watch(WatchArgs {
                foreground: true,
                command: None,
            })
        ));

        let parsed = parse(["kbolt", "watch", "logs", "--lines", "20"]);
        assert!(matches!(
            parsed.command,
            Command::Watch(WatchArgs {
                foreground: false,
                command: Some(WatchCommand::Logs { lines: 20 }),
            })
        ));
    }

    #[test]
    fn parses_eval_run_with_optional_manifest_path() {
        let parsed = parse(["kbolt", "eval", "run"]);
        assert!(matches!(
            parsed.command,
            Command::Eval(eval) if eval.command == EvalCommand::Run(EvalRunArgs { file: None })
        ));

        let parsed = parse(["kbolt", "eval", "run", "--file", "/tmp/scifact.toml"]);
        assert!(matches!(
            parsed.command,
            Command::Eval(eval)
                if eval.command
                    == EvalCommand::Run(EvalRunArgs {
                        file: Some(PathBuf::from("/tmp/scifact.toml"))
                    })
        ));
    }

    #[test]
    fn parses_eval_import_beir_with_required_paths() {
        let parsed = parse([
            "kbolt",
            "eval",
            "import",
            "beir",
            "--dataset",
            "fiqa",
            "--source",
            "/tmp/fiqa-source",
            "--output",
            "/tmp/fiqa-bench",
        ]);

        let Command::Eval(eval) = parsed.command else {
            panic!("expected eval command");
        };
        assert_eq!(
            eval.command,
            EvalCommand::Import(EvalImportArgs {
                dataset: EvalImportCommand::Beir(EvalImportBeirArgs {
                    dataset: "fiqa".to_string(),
                    source: PathBuf::from("/tmp/fiqa-source"),
                    output: PathBuf::from("/tmp/fiqa-bench"),
                    collection: None,
                })
            })
        );
    }

    #[test]
    fn parses_eval_import_beir_with_collection_override() {
        let parsed = parse([
            "kbolt",
            "eval",
            "import",
            "beir",
            "--dataset",
            "fiqa",
            "--source",
            "/tmp/fiqa-source",
            "--output",
            "/tmp/fiqa-bench",
            "--collection",
            "finance",
        ]);

        let Command::Eval(eval) = parsed.command else {
            panic!("expected eval command");
        };
        assert_eq!(
            eval.command,
            EvalCommand::Import(EvalImportArgs {
                dataset: EvalImportCommand::Beir(EvalImportBeirArgs {
                    dataset: "fiqa".to_string(),
                    source: PathBuf::from("/tmp/fiqa-source"),
                    output: PathBuf::from("/tmp/fiqa-bench"),
                    collection: Some("finance".to_string()),
                })
            })
        );
    }
}
