//! Command-line argument definitions for the `bce` binary.

use clap::{Args, Parser, Subcommand};
use std::path::PathBuf;

/// Extract and query `BlueSky` posts from a local `SQLite` database.
#[derive(Parser, Debug)]
#[command(name = "bce")]
#[command(version)]
#[command(about = "Extract and query BlueSky posts from a local SQLite database")]
#[command(after_help = "\
CREDENTIALS:
  Set BSKY_APP_PASSWORD before running.
  Create an app password at https://bsky.app/settings/app-passwords

WORKFLOW:
  1. bce fetch alice.bsky.social    # download posts
  2. bce migrate                     # expand schema + build search index
  3. bce query 'text ~ rust'         # search and filter

EXAMPLES:
  bce fetch alice.bsky.social
  bce fetch alice.bsky.social --since '3 months ago'
  bce migrate --db /path/to/db
  bce query 'text ~ bluesky AND is_reply = false'
  bce query 'created_at > 2024-06-01' --limit 25
  bce query --limit 25 --offset 50
  bce --agent-help")]
pub struct Cli {
    /// Select the networked fetch path or the local read-only query path.
    #[command(subcommand)]
    pub command: Option<Command>,
}

/// Available `bce` subcommands.
#[derive(Subcommand, Debug)]
pub enum Command {
    /// Fetch posts from the network into the local database.
    Fetch(FetchArgs),
    /// Query posts from the local database without making network requests.
    Query(QueryArgs),
    /// Migrate the database schema to support structured queries and full-text search.
    Migrate(MigrateArgs),
}

/// Arguments for the networked extractor path.
#[derive(Args, Debug)]
pub struct FetchArgs {
    /// `BlueSky` handle (e.g. alice.bsky.social) or DID (e.g. did:plc:abc123).
    pub handle: String,

    /// Path to the `SQLite` database file.
    ///
    /// Defaults to `~/.local/share/bce/bsky-posts.db` (XDG data directory).
    #[arg(long, value_name = "PATH")]
    pub db: Option<PathBuf>,

    /// Only extract posts created after this date.
    ///
    /// Accepts human-friendly dates like "2025-01-01", "3 months ago", "yesterday".
    #[arg(long, value_name = "DATE")]
    pub since: Option<String>,

    /// Suppress the progress spinner.
    #[arg(short, long)]
    pub quiet: bool,
}

/// Arguments for the read-only local query path.
#[derive(Args, Debug)]
#[command(after_long_help = "\
QUERY LANGUAGE:
  Pass a filter expression as a positional argument to search and filter posts.
  Requires a migrated database (run `bce migrate` first).

  FIELDS:
    text              Post body (use ~ for full-text search)
    is_reply          Whether the post is a reply (true/false)
    author_did        DID of the post author
    created_at        ISO 8601 timestamp
    reply_parent_uri  AT URI of the parent post (replies only)
    reply_root_uri    AT URI of the thread root (replies only)
    embed_type        Embed type: images, external, record, recordWithMedia, video
    lang              Primary language (BCP-47, e.g. en, pt, ja)

  OPERATORS:
    ~                 Full-text search (text field only, case-insensitive)
    = !=              Equality / inequality
    > < >= <=         Comparison (useful for created_at)

  COMBINING:
    Use AND to combine multiple conditions.

  EXAMPLES:
    bce query 'text ~ rust'
    bce query 'text ~ \"async runtime\" AND lang = en'
    bce query 'is_reply = false AND created_at > 2024-06-01'
    bce query 'embed_type = images AND author_did = did:plc:abc'
    bce query 'text ~ bluesky AND is_reply = true' --limit 10
")]
pub struct QueryArgs {
    /// Filter expression (e.g. `text ~ rust AND is_reply = false`).
    ///
    /// Requires a migrated database. Run `bce migrate` first.
    pub filter: Option<String>,

    /// Path to the existing `SQLite` database file.
    #[arg(long, value_name = "PATH")]
    pub db: Option<PathBuf>,

    /// Maximum number of posts to return.
    #[arg(long, default_value_t = 50)]
    pub limit: u64,

    /// Number of posts to skip before returning results.
    #[arg(long, default_value_t = 0)]
    pub offset: u64,
}

/// Arguments for the schema migration command.
#[derive(Args, Debug)]
pub struct MigrateArgs {
    /// Path to the `SQLite` database file to migrate.
    ///
    /// Defaults to `~/.local/share/bce/bsky-posts.db` (XDG data directory).
    #[arg(long, value_name = "PATH")]
    pub db: Option<PathBuf>,
}

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

    #[test]
    fn test_cli_parse_fetch_subcommand() {
        let cli = Cli::try_parse_from([
            "bce",
            "fetch",
            "alice.bsky.social",
            "--db",
            "/tmp/test.db",
            "--since",
            "2025-01-01",
            "-q",
        ])
        .unwrap();

        let Some(Command::Fetch(args)) = cli.command else {
            panic!("expected Fetch subcommand");
        };
        assert_eq!(args.handle, "alice.bsky.social");
        assert_eq!(args.db, Some(std::path::PathBuf::from("/tmp/test.db")));
        assert_eq!(args.since, Some("2025-01-01".to_string()));
        assert!(args.quiet);
    }

    #[test]
    fn test_cli_parse_query_defaults() {
        let cli = Cli::try_parse_from(["bce", "query"]).unwrap();

        let Some(Command::Query(args)) = cli.command else {
            panic!("expected Query subcommand");
        };
        assert!(args.db.is_none());
        assert!(args.filter.is_none());
        assert_eq!(args.limit, 50);
        assert_eq!(args.offset, 0);
    }

    #[test]
    fn test_cli_parse_query_with_filter() {
        let cli =
            Cli::try_parse_from(["bce", "query", "text ~ rust AND is_reply = false"]).unwrap();

        let Some(Command::Query(args)) = cli.command else {
            panic!("expected Query subcommand");
        };
        assert_eq!(
            args.filter.as_deref(),
            Some("text ~ rust AND is_reply = false")
        );
    }

    #[test]
    fn test_cli_parse_query_overrides() {
        let cli = Cli::try_parse_from([
            "bce",
            "query",
            "--db",
            "/tmp/query.db",
            "--limit",
            "25",
            "--offset",
            "75",
        ])
        .unwrap();

        let Some(Command::Query(args)) = cli.command else {
            panic!("expected Query subcommand");
        };
        assert_eq!(args.db, Some(std::path::PathBuf::from("/tmp/query.db")));
        assert_eq!(args.limit, 25);
        assert_eq!(args.offset, 75);
    }

    #[test]
    fn test_cli_parse_migrate_defaults() {
        let cli = Cli::try_parse_from(["bce", "migrate"]).unwrap();

        let Some(Command::Migrate(args)) = cli.command else {
            panic!("expected Migrate subcommand");
        };
        assert!(args.db.is_none());
    }

    #[test]
    fn test_cli_parse_migrate_with_db() {
        let cli = Cli::try_parse_from(["bce", "migrate", "--db", "/tmp/m.db"]).unwrap();

        let Some(Command::Migrate(args)) = cli.command else {
            panic!("expected Migrate subcommand");
        };
        assert_eq!(args.db, Some(std::path::PathBuf::from("/tmp/m.db")));
    }

    #[test]
    fn test_cli_parse_flat_invocation_fails() {
        assert!(Cli::try_parse_from(["bce", "alice.bsky.social"]).is_err());
    }

    #[test]
    fn test_cli_parse_query_rejects_since_flag() {
        assert!(Cli::try_parse_from(["bce", "query", "--since", "2025-01-01"]).is_err());
    }

    #[test]
    fn test_query_help_shows_query_language_docs() {
        let mut command = Cli::command();
        let query = command
            .find_subcommand_mut("query")
            .expect("query subcommand must exist");
        let mut output = Vec::new();
        query
            .write_long_help(&mut output)
            .expect("query help must render");

        let help = String::from_utf8(output).expect("help must be utf-8");
        assert!(help.contains("--db"));
        assert!(help.contains("--limit"));
        assert!(help.contains("--offset"));
        assert!(help.contains("QUERY LANGUAGE"));
        assert!(help.contains("FIELDS"));
        assert!(help.contains("OPERATORS"));
        assert!(help.contains("EXAMPLES"));
    }
}
