use std::path::PathBuf;

use clap::{builder::PossibleValuesParser, Args, Parser, Subcommand};

/// Parses the top-level `vdl` command line and selects a subcommand to run.
///
/// # Examples
///
/// ```rust,ignore
/// let cli = Cli::parse_from(["vdl", "yt", "--url", "https://example.com/watch?v=123"]);
/// ```
#[derive(Debug, Parser)]
#[command(name = "vdl", about = "Video Downloader CLI")]
pub struct Cli {
    /// Starts the local web UI server instead of running a CLI subcommand.
    #[arg(short = 'S', long)]
    pub serve: bool,

    /// Configures the local web UI server when `--serve` is passed.
    #[command(flatten)]
    pub serve_args: ServeArgs,

    /// Selects which platform command or utility action should run.
    #[command(subcommand)]
    pub command: Option<Commands>,
}

/// Captures the flags accepted by `vdl --serve`.
#[derive(Debug, Clone, Args)]
pub struct ServeArgs {
    /// Sets the address the server binds to.
    #[arg(long, default_value = "127.0.0.1")]
    pub host: String,
    /// Sets the port the server listens on.
    #[arg(long, short = 'p', default_value_t = 7272)]
    pub port: u16,
    /// Opens the default browser at the server URL once it starts.
    #[arg(long, short = 'o')]
    pub open: bool,
}

/// Lists all supported platform and utility subcommands.
#[derive(Debug, Subcommand)]
pub enum Commands {
    /// Downloads media from YouTube.
    #[command(visible_alias = "youtube", about = "Download from YouTube")]
    Yt(CommonArgs),
    /// Downloads media from TikTok.
    #[command(visible_alias = "tiktok", about = "Download from TikTok")]
    Tk(CommonArgs),
    /// Downloads media from Instagram.
    #[command(visible_alias = "instagram", about = "Download from Instagram")]
    Ig(CommonArgs),
    /// Downloads media from Twitter/X.
    #[command(visible_alias = "twitter", about = "Download from Twitter/X")]
    Tw(CommonArgs),
    /// Downloads audio from Spotify.
    #[command(visible_alias = "spotify", about = "Download from Spotify (audio)")]
    Sp(SpotifyArgs),
    /// Updates sandboxed helper binaries.
    #[command(about = "Update sandboxed yt-dlp and ffmpeg binaries")]
    Update,
    /// Prints the current config path and YAML contents.
    #[command(about = "Show current config file path and contents")]
    Config,
}

/// Captures the shared download flags used by YouTube, TikTok, Instagram, and Twitter/X.
#[derive(Debug, Clone, Args)]
pub struct CommonArgs {
    /// Supplies a direct media URL to download.
    #[arg(short = 'u', long, value_name = "URL", conflicts_with = "search")]
    pub url: Option<String>,
    /// Selects the preferred output quality when a platform supports it.
    #[arg(short = 'q', long, value_name = "Q", value_parser = quality_parser())]
    pub quality: Option<String>,
    /// Downloads only the audio stream.
    #[arg(short = 'a', long, conflicts_with = "video_only")]
    pub audio_only: bool,
    /// Downloads only the video stream without audio.
    #[arg(short = 'v', long, conflicts_with = "audio_only")]
    pub video_only: bool,
    /// Overrides the output container format for this run.
    #[arg(short = 'f', long, value_name = "FMT", value_parser = common_format_parser())]
    pub format: Option<String>,
    /// Overrides the destination directory for this run only.
    #[arg(short = 'o', long, value_name = "PATH")]
    pub output: Option<PathBuf>,
    /// Searches by query instead of using a direct URL.
    #[arg(short = 's', long, value_name = "QUERY", conflicts_with = "url")]
    pub search: Option<String>,
    /// Skips the confirmation prompt after metadata preview.
    #[arg(short = 'y', long)]
    pub yes: bool,
}

/// Captures the Spotify-specific flags for audio downloads.
#[derive(Debug, Clone, Args)]
pub struct SpotifyArgs {
    /// Supplies a direct Spotify URL to download.
    #[arg(short = 'u', long, value_name = "URL")]
    pub url: Option<String>,
    /// Overrides the destination directory for this run only.
    #[arg(short = 'o', long, value_name = "PATH")]
    pub output: Option<PathBuf>,
    /// Selects the audio container written to disk.
    #[arg(short = 'f', long, value_name = "FMT", value_parser = spotify_format_parser())]
    pub format: Option<String>,
    /// Skips the confirmation prompt after metadata preview.
    #[arg(short = 'y', long)]
    pub yes: bool,
}

fn quality_parser() -> PossibleValuesParser {
    PossibleValuesParser::new(["best", "1080", "720", "480", "360", "worst"])
}

fn common_format_parser() -> PossibleValuesParser {
    PossibleValuesParser::new(["mp4", "mkv", "webm", "mp3", "m4a", "opus"])
}

fn spotify_format_parser() -> PossibleValuesParser {
    PossibleValuesParser::new(["mp3", "m4a", "opus"])
}

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

    use super::*;

    #[test]
    fn top_level_help_lists_expected_subcommands() {
        let mut cmd = Cli::command();
        let mut buffer = Vec::new();

        cmd.write_long_help(&mut buffer)
            .expect("top-level help should render");

        let help = String::from_utf8(buffer).expect("help should be valid utf-8");

        assert!(help.contains("yt"));
        assert!(help.contains("youtube"));
        assert!(help.contains("tk"));
        assert!(help.contains("tiktok"));
        assert!(help.contains("ig"));
        assert!(help.contains("instagram"));
        assert!(help.contains("tw"));
        assert!(help.contains("twitter"));
        assert!(help.contains("sp"));
        assert!(help.contains("spotify"));
        assert!(help.contains("update"));
        assert!(help.contains("config"));
    }

    #[test]
    fn yt_help_lists_expected_flags() {
        let mut cmd = Cli::command();
        let mut buffer = Vec::new();

        cmd.find_subcommand_mut("yt")
            .expect("yt subcommand should exist")
            .write_long_help(&mut buffer)
            .expect("yt help should render");

        let help = String::from_utf8(buffer).expect("help should be valid utf-8");

        for flag in [
            "--url",
            "--quality",
            "--audio-only",
            "--video-only",
            "--format",
            "--output",
            "--search",
            "--yes",
        ] {
            assert!(help.contains(flag), "missing flag in help: {flag}");
        }
    }

    #[test]
    fn command_aliases_parse() {
        let cli = Cli::try_parse_from(["vdl", "youtube", "--url", "https://example.com"])
            .expect("youtube alias should parse");

        assert!(matches!(cli.command, Some(Commands::Yt(_))));
    }

    #[test]
    fn conflicting_audio_and_video_flags_error() {
        let err = Cli::try_parse_from(["vdl", "yt", "--audio-only", "--video-only"])
            .expect_err("conflicting audio/video flags should error");

        assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
    }

    #[test]
    fn conflicting_url_and_search_flags_error() {
        let err = Cli::try_parse_from([
            "vdl",
            "yt",
            "--url",
            "https://example.com",
            "--search",
            "rust",
        ])
        .expect_err("conflicting url/search flags should error");

        assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
    }

    #[test]
    fn invalid_quality_is_rejected() {
        let err = Cli::try_parse_from(["vdl", "yt", "--quality", "144"])
            .expect_err("invalid quality should error");

        assert_eq!(err.kind(), clap::error::ErrorKind::InvalidValue);
    }

    #[test]
    fn spotify_rejects_video_only_flag() {
        let err = Cli::try_parse_from(["vdl", "sp", "--video-only"])
            .expect_err("spotify should reject unsupported flags");

        assert_eq!(err.kind(), clap::error::ErrorKind::UnknownArgument);
    }

    #[test]
    fn serve_flag_parses_with_custom_port_and_host() {
        let cli = Cli::try_parse_from([
            "vdl", "--serve", "--host", "0.0.0.0", "--port", "9090", "--open",
        ])
        .expect("serve flags should parse");

        assert!(cli.serve);
        assert_eq!(cli.serve_args.host, "0.0.0.0");
        assert_eq!(cli.serve_args.port, 9090);
        assert!(cli.serve_args.open);
    }

    #[test]
    fn serve_flag_defaults_to_loopback_and_default_port() {
        let cli = Cli::try_parse_from(["vdl", "-S"]).expect("-S should parse");

        assert!(cli.serve);
        assert_eq!(cli.serve_args.host, "127.0.0.1");
        assert_eq!(cli.serve_args.port, 7272);
        assert!(!cli.serve_args.open);
    }
}