use anyhow::{Context, Result};
use serde::Deserialize;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};

use crate::cli::Args;

/// Represents the application configuration loaded from a file
#[derive(Debug, Deserialize, Default)]
pub struct Config {
    #[serde(default)]
    pub output: OutputConfig,

    #[serde(default)]
    pub provider: ProviderConfig,

    #[serde(default)]
    pub filter: FilterConfig,

    #[serde(default)]
    pub network: NetworkConfig,

    #[serde(default)]
    pub testing: TestingConfig,

    #[serde(default)]
    pub cache: CacheConfig,
}

#[derive(Debug, Deserialize, Default)]
pub struct OutputConfig {
    pub output: Option<String>,
    pub format: Option<String>,
    pub merge_endpoint: Option<bool>,
}

#[derive(Debug, Deserialize, Default)]
pub struct ProviderConfig {
    pub providers: Option<Vec<String>>,
    pub subs: Option<bool>,
    pub cc_index: Option<String>,
    pub vt_api_key: Option<String>,
    pub urlscan_api_key: Option<String>,
    pub zoomeye_api_key: Option<String>,
    pub include_robots: Option<bool>,
    pub include_sitemap: Option<bool>,
    pub exclude_robots: Option<bool>,
    pub exclude_sitemap: Option<bool>,
}

/// Provider-config file: a small TOML that holds only API keys so the main
/// config (filter rules, output formatting, etc.) can be checked into source
/// control without leaking secrets. Comma-separated values rotate.
#[derive(Debug, Deserialize, Default)]
pub struct ProviderKeysConfig {
    pub vt_api_key: Option<String>,
    pub urlscan_api_key: Option<String>,
    pub zoomeye_api_key: Option<String>,
}

impl ProviderKeysConfig {
    /// Parse a provider-config TOML file from `path`. Returns the parsed
    /// struct or an error.
    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
        let content = fs::read_to_string(&path).with_context(|| {
            format!(
                "Failed to read provider-config file: {}",
                path.as_ref().display()
            )
        })?;
        let parsed: ProviderKeysConfig = toml::from_str(&content).with_context(|| {
            format!(
                "Failed to parse provider-config file: {}",
                path.as_ref().display()
            )
        })?;
        Ok(parsed)
    }

    /// Default lookup path mirrors the main config: $XDG_CONFIG_HOME/urx or
    /// %APPDATA%\urx. Returns None when neither exists; unlike `Config`, we
    /// do NOT auto-create the file because that would land an empty
    /// "credentials" path the user didn't ask for.
    pub fn default_path() -> Option<PathBuf> {
        #[cfg(windows)]
        {
            if let Some(app_data) = env::var_os("APPDATA").map(PathBuf::from) {
                let p = app_data.join("urx").join("provider-config.toml");
                if p.exists() {
                    return Some(p);
                }
            }
        }
        #[cfg(not(windows))]
        {
            if let Some(home) = home_dir() {
                let p = home
                    .join(".config")
                    .join("urx")
                    .join("provider-config.toml");
                if p.exists() {
                    return Some(p);
                }
            }
        }
        None
    }

    /// Load using the same precedence as the main config: --provider-config
    /// flag wins, then the default path. Returns an empty config when no file
    /// is found so callers can chain it freely.
    pub fn load(args: &Args) -> Result<Self> {
        if let Some(path) = &args.provider_config {
            return Self::from_file(path);
        }
        if let Some(path) = Self::default_path() {
            return Self::from_file(path);
        }
        Ok(ProviderKeysConfig::default())
    }

    /// Apply keys to args, but only for slots not already supplied via CLI
    /// (or env-via-CLI). The main `Config` runs first and may have filled
    /// these slots; this method then overwrites them when the provider-config
    /// has a value, so provider-config beats main config.
    ///
    /// `cli_supplied_*` flags carry the original CLI state captured BEFORE
    /// either config layer ran, so CLI input is preserved.
    pub fn apply_to_args(
        &self,
        args: &mut Args,
        cli_supplied_vt: bool,
        cli_supplied_urlscan: bool,
        cli_supplied_zoomeye: bool,
    ) {
        fn split_csv(s: &str) -> Vec<String> {
            s.split(',')
                .map(|p| p.trim().to_string())
                .filter(|p| !p.is_empty())
                .collect()
        }

        if !cli_supplied_vt {
            if let Some(keys) = &self.vt_api_key {
                args.vt_api_key = split_csv(keys);
            }
        }
        if !cli_supplied_urlscan {
            if let Some(keys) = &self.urlscan_api_key {
                args.urlscan_api_key = split_csv(keys);
            }
        }
        if !cli_supplied_zoomeye {
            if let Some(keys) = &self.zoomeye_api_key {
                args.zoomeye_api_key = split_csv(keys);
            }
        }
    }
}

#[derive(Debug, Deserialize, Default)]
pub struct FilterConfig {
    pub preset: Option<Vec<String>>,
    pub extensions: Option<Vec<String>>,
    pub exclude_extensions: Option<Vec<String>>,
    pub patterns: Option<Vec<String>>,
    pub exclude_patterns: Option<Vec<String>>,
    pub show_only_host: Option<bool>,
    pub show_only_path: Option<bool>,
    pub show_only_param: Option<bool>,
    pub min_length: Option<usize>,
    pub max_length: Option<usize>,
}

#[derive(Debug, Deserialize, Default)]
pub struct NetworkConfig {
    pub network_scope: Option<String>,
    pub proxy: Option<String>,
    pub proxy_auth: Option<String>,
    pub insecure: Option<bool>,
    pub random_agent: Option<bool>,
    pub timeout: Option<u64>,
    pub retries: Option<u32>,
    pub parallel: Option<u32>,
    pub rate_limit: Option<f32>,
}

#[derive(Debug, Deserialize, Default)]
pub struct TestingConfig {
    pub check_status: Option<bool>,
    pub include_status: Option<Vec<String>>,
    pub exclude_status: Option<Vec<String>>,
    pub extract_links: Option<bool>,
}

#[derive(Debug, Deserialize, Default)]
pub struct CacheConfig {
    pub incremental: Option<bool>,
    pub cache_type: Option<String>,
    pub cache_path: Option<String>,
    pub redis_url: Option<String>,
    pub cache_ttl: Option<u64>,
    pub no_cache: Option<bool>,
}

fn normalize_output_format(format: &str) -> Option<String> {
    match format.trim().to_ascii_lowercase().as_str() {
        "plain" => Some("plain".to_string()),
        "json" => Some("json".to_string()),
        "csv" => Some("csv".to_string()),
        _ => None,
    }
}

fn normalize_network_scope(scope: &str) -> Option<String> {
    match scope.trim().to_ascii_lowercase().as_str() {
        "all" => Some("all".to_string()),
        "providers" => Some("providers".to_string()),
        "testers" => Some("testers".to_string()),
        "providers,testers" | "testers,providers" => Some("providers,testers".to_string()),
        _ => None,
    }
}

impl Config {
    /// Load configuration from a specific file path
    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
        let content = fs::read_to_string(&path)
            .with_context(|| format!("Failed to read config file: {}", path.as_ref().display()))?;

        let config: Config = toml::from_str(&content)
            .with_context(|| format!("Failed to parse config file: {}", path.as_ref().display()))?;

        Ok(config)
    }

    /// Get the default configuration file path
    /// - Linux/macOS: ~/.config/urx/config.toml
    /// - Windows: %AppData%\urx\config.toml
    ///
    /// If the directory doesn't exist, it will be created.
    /// If the file doesn't exist, an empty config.toml file will be created.
    pub fn default_path() -> Option<PathBuf> {
        #[cfg(windows)]
        {
            if let Some(app_data) = env::var_os("APPDATA").map(PathBuf::from) {
                let config_dir = app_data.join("urx");
                let config_path = config_dir.join("config.toml");

                // Create directory if it doesn't exist
                if !config_dir.exists() && fs::create_dir_all(&config_dir).is_err() {
                    return None;
                }

                // Create empty config file if it doesn't exist
                if !config_path.exists() && fs::write(&config_path, "").is_err() {
                    return None;
                }

                return Some(config_path);
            }
        }

        #[cfg(not(windows))]
        {
            if let Some(home) = home_dir() {
                let config_dir = home.join(".config").join("urx");
                let config_path = config_dir.join("config.toml");

                // Create directory if it doesn't exist
                if !config_dir.exists() && fs::create_dir_all(&config_dir).is_err() {
                    return None;
                }

                // Create empty config file if it doesn't exist
                if !config_path.exists() && fs::write(&config_path, "").is_err() {
                    return None;
                }

                return Some(config_path);
            }
        }

        None
    }

    /// Load configuration based on command line arguments
    /// Priority: --config flag > default path > default values
    pub fn load(args: &Args) -> Result<Self> {
        // Try to load from --config flag first
        if let Some(path) = &args.config {
            return Self::from_file(path);
        }

        // Then try default location
        if let Some(default_path) = Self::default_path() {
            return Self::from_file(default_path);
        }

        // Otherwise use default values
        Ok(Config::default())
    }

    /// Apply configuration values to Args, respecting priority
    /// Command line arguments take precedence over config file values
    pub fn apply_to_args(self, args: &mut Args) {
        self.apply_output_config(args);
        self.apply_provider_config(args);
        self.apply_filter_config(args);
        self.apply_network_config(args);
        self.apply_testing_config(args);
        self.apply_cache_config(args);
    }

    fn apply_output_config(&self, args: &mut Args) {
        // Output options
        if args.output.is_none() {
            if let Some(output) = &self.output.output {
                args.output = Some(PathBuf::from(output));
            }
        }

        if args.format == "plain" {
            if let Some(format) = &self.output.format {
                if let Some(format) = normalize_output_format(format) {
                    args.format = format;
                } else if !args.silent {
                    eprintln!(
                        "Ignoring [output].format={format:?} in config: expected plain, json, or csv"
                    );
                }
            }
        }

        if !args.merge_endpoint && self.output.merge_endpoint.unwrap_or(false) {
            args.merge_endpoint = true;
        }
    }

    fn apply_provider_config(&self, args: &mut Args) {
        // Provider options
        if args.providers == vec!["wayback", "cc", "otx"] {
            if let Some(providers) = &self.provider.providers {
                args.providers = providers.clone();
            }
        }

        if !args.subs && self.provider.subs.unwrap_or(false) {
            args.subs = true;
        }

        // Treat the default singleton list as "not user-supplied" so the file
        // value wins. Config file still accepts a single string; we split it
        // on commas so users can configure multi-index there too.
        let cc_default = vec!["latest".to_string()];
        if args.cc_index == cc_default {
            if let Some(cc_index) = &self.provider.cc_index {
                let split: Vec<String> = cc_index
                    .split(',')
                    .map(|s| s.trim().to_string())
                    .filter(|s| !s.is_empty())
                    .collect();
                if !split.is_empty() {
                    args.cc_index = split;
                }
            }
        }

        if args.vt_api_key.is_empty() {
            if let Some(vt_api_key) = &self.provider.vt_api_key {
                args.vt_api_key.push(vt_api_key.clone());
            }
        }

        if args.urlscan_api_key.is_empty() {
            if let Some(urlscan_api_key) = &self.provider.urlscan_api_key {
                args.urlscan_api_key.push(urlscan_api_key.clone());
            }
        }

        if args.zoomeye_api_key.is_empty() {
            if let Some(zoomeye_api_key) = &self.provider.zoomeye_api_key {
                args.zoomeye_api_key.push(zoomeye_api_key.clone());
            }
        }

        // Handle robots.txt and sitemap.xml discovery options
        if !args.exclude_robots && self.provider.exclude_robots.unwrap_or(false) {
            args.exclude_robots = true;
        }

        if !args.exclude_sitemap && self.provider.exclude_sitemap.unwrap_or(false) {
            args.exclude_sitemap = true;
        }

        // Only apply include_* if exclude_* is not set (exclude takes precedence)
        if !args.exclude_robots && args.include_robots {
            if let Some(include_robots) = self.provider.include_robots {
                args.include_robots = include_robots;
            }
        }

        if !args.exclude_sitemap && args.include_sitemap {
            if let Some(include_sitemap) = self.provider.include_sitemap {
                args.include_sitemap = include_sitemap;
            }
        }
    }

    fn apply_filter_config(&self, args: &mut Args) {
        // Filter options
        if args.preset.is_empty() {
            if let Some(preset) = &self.filter.preset {
                args.preset = preset.clone();
            }
        }

        if args.extensions.is_empty() {
            if let Some(extensions) = &self.filter.extensions {
                args.extensions = extensions.clone();
            }
        }

        if args.exclude_extensions.is_empty() {
            if let Some(exclude_extensions) = &self.filter.exclude_extensions {
                args.exclude_extensions = exclude_extensions.clone();
            }
        }

        if args.patterns.is_empty() {
            if let Some(patterns) = &self.filter.patterns {
                args.patterns = patterns.clone();
            }
        }

        if args.exclude_patterns.is_empty() {
            if let Some(exclude_patterns) = &self.filter.exclude_patterns {
                args.exclude_patterns = exclude_patterns.clone();
            }
        }

        if !args.show_only_host && self.filter.show_only_host.unwrap_or(false) {
            args.show_only_host = true;
        }

        if !args.show_only_path && self.filter.show_only_path.unwrap_or(false) {
            args.show_only_path = true;
        }

        if !args.show_only_param && self.filter.show_only_param.unwrap_or(false) {
            args.show_only_param = true;
        }

        if args.min_length.is_none() && self.filter.min_length.is_some() {
            args.min_length = self.filter.min_length;
        }

        if args.max_length.is_none() && self.filter.max_length.is_some() {
            args.max_length = self.filter.max_length;
        }
    }

    fn apply_network_config(&self, args: &mut Args) {
        // Network options
        if args.network_scope == "all" {
            if let Some(network_scope) = &self.network.network_scope {
                if let Some(network_scope) = normalize_network_scope(network_scope) {
                    args.network_scope = network_scope;
                } else if !args.silent {
                    eprintln!(
                        "Ignoring [network].network_scope={network_scope:?} in config: expected all, providers, testers, or providers,testers"
                    );
                }
            }
        }

        if args.proxy.is_none() && self.network.proxy.is_some() {
            args.proxy = self.network.proxy.clone();
        }

        if args.proxy_auth.is_none() && self.network.proxy_auth.is_some() {
            args.proxy_auth = self.network.proxy_auth.clone();
        }

        if !args.insecure && self.network.insecure.unwrap_or(false) {
            args.insecure = true;
        }

        if !args.random_agent && self.network.random_agent.unwrap_or(false) {
            args.random_agent = true;
        }

        if args.timeout == 120 {
            if let Some(timeout) = self.network.timeout {
                if timeout > 0 {
                    args.timeout = timeout;
                } else if !args.silent {
                    eprintln!(
                        "Ignoring [network].timeout=0 in config: value must be at least 1 second"
                    );
                }
            }
        }

        if args.retries == 2 {
            if let Some(retries) = self.network.retries {
                args.retries = retries;
            }
        }

        if args.parallel.unwrap_or(5) == 5 && self.network.parallel.is_some() {
            if let Some(parallel) = self.network.parallel {
                if parallel > 0 {
                    args.parallel = Some(parallel);
                } else if !args.silent {
                    eprintln!("Ignoring [network].parallel=0 in config: value must be at least 1");
                }
            }
        }

        if args.rate_limit.is_none() && self.network.rate_limit.is_some() {
            args.rate_limit = self.network.rate_limit;
        }
    }

    fn apply_testing_config(&self, args: &mut Args) {
        // Testing options
        if !args.check_status && self.testing.check_status.unwrap_or(false) {
            args.check_status = true;
        }

        if args.include_status.is_empty() {
            if let Some(include_status) = &self.testing.include_status {
                args.include_status = include_status.clone();
            }
        }

        if args.exclude_status.is_empty() {
            if let Some(exclude_status) = &self.testing.exclude_status {
                args.exclude_status = exclude_status.clone();
            }
        }

        if !args.extract_links && self.testing.extract_links.unwrap_or(false) {
            args.extract_links = true;
        }
    }

    fn apply_cache_config(&self, args: &mut Args) {
        // Cache options
        if !args.incremental && self.cache.incremental.unwrap_or(false) {
            args.incremental = true;
        }

        if args.cache_type == "sqlite" {
            if let Some(cache_type) = &self.cache.cache_type {
                args.cache_type = cache_type.clone();
            }
        }

        if args.cache_path.is_none() {
            if let Some(cache_path) = &self.cache.cache_path {
                args.cache_path = Some(PathBuf::from(cache_path));
            }
        }

        if args.redis_url.is_none() && self.cache.redis_url.is_some() {
            args.redis_url = self.cache.redis_url.clone();
        }

        if args.cache_ttl == 86400 {
            if let Some(cache_ttl) = self.cache.cache_ttl {
                args.cache_ttl = cache_ttl;
            }
        }

        if !args.no_cache && self.cache.no_cache.unwrap_or(false) {
            args.no_cache = true;
        }
    }
}

#[cfg_attr(windows, allow(dead_code))]
/// Helper function to get the home directory
fn home_dir() -> Option<PathBuf> {
    env::var_os("HOME").map(PathBuf::from).or({
        #[cfg(windows)]
        {
            // On Windows, try USERPROFILE first, then HOMEDRIVE + HOMEPATH
            if let Some(profile) = env::var_os("USERPROFILE").map(PathBuf::from) {
                return Some(profile);
            }

            match (env::var_os("HOMEDRIVE"), env::var_os("HOMEPATH")) {
                (Some(drive), Some(path)) => {
                    let mut drive_path = PathBuf::from(drive);
                    drive_path.push(path);
                    Some(drive_path)
                }
                _ => None,
            }
        }

        #[cfg(not(windows))]
        None
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use clap::Parser;
    use std::io::Write;
    use tempfile::NamedTempFile;

    fn create_temp_config_file(content: &str) -> NamedTempFile {
        let mut file = NamedTempFile::new().unwrap();
        file.write_all(content.as_bytes()).unwrap();
        file
    }

    #[test]
    fn test_config_from_file() {
        // Create a temporary config file
        let config_content = r#"
            [output]
            output = "test-output.txt"
            format = "json"
            merge_endpoint = true

            [provider]
            providers = ["wayback", "cc"]
            subs = true
            cc_index = "CC-MAIN-2025-04"

            [filter]
            extensions = ["php", "js"]
            show_only_host = true
        "#;

        let temp_file = create_temp_config_file(config_content);

        // Load the config from the temp file
        let config = Config::from_file(temp_file.path()).unwrap();

        // Verify the loaded config values
        assert_eq!(config.output.output, Some("test-output.txt".to_string()));
        assert_eq!(config.output.format, Some("json".to_string()));
        assert_eq!(config.output.merge_endpoint, Some(true));

        assert_eq!(
            config.provider.providers,
            Some(vec!["wayback".to_string(), "cc".to_string()])
        );
        assert_eq!(config.provider.subs, Some(true));
        assert_eq!(
            config.provider.cc_index,
            Some("CC-MAIN-2025-04".to_string())
        );

        assert_eq!(
            config.filter.extensions,
            Some(vec!["php".to_string(), "js".to_string()])
        );
        assert_eq!(config.filter.show_only_host, Some(true));
    }

    #[test]
    fn test_default_config() {
        // Default config should have default values
        let config = Config::default();

        assert_eq!(config.output.output, None);
        assert_eq!(config.output.format, None);
        assert_eq!(config.output.merge_endpoint, None);

        assert_eq!(config.provider.providers, None);
        assert_eq!(config.provider.subs, None);
        assert_eq!(config.provider.cc_index, None);

        assert_eq!(config.filter.extensions, None);
        assert_eq!(config.filter.show_only_host, None);
    }

    #[test]
    fn test_apply_to_args() {
        // Create a config with some values
        let mut config = Config::default();
        config.output.output = Some("output.txt".to_string());
        config.output.format = Some("json".to_string());
        config.provider.providers = Some(vec!["cc".to_string()]);

        // Create default args
        let mut args = Args {
            domains: vec![],
            config: None,
            files: vec![],
            output: None,
            format: "plain".to_string(),
            merge_endpoint: false,
            normalize_url: false,
            providers: vec!["wayback".to_string(), "cc".to_string(), "otx".to_string()],
            subs: false,
            cc_index: vec!["CC-MAIN-2026-17".to_string()],
            vt_api_key: vec![],
            urlscan_api_key: vec![],
            zoomeye_api_key: vec![],
            verbose: false,
            silent: false,
            no_progress: false,
            no_color: false,
            preset: vec![],
            extensions: vec![],
            exclude_extensions: vec![],
            patterns: vec![],
            exclude_patterns: vec![],
            show_only_host: false,
            show_only_path: false,
            show_only_param: false,
            min_length: None,
            max_length: None,
            strict: true,
            no_strict: false,
            network_scope: "all".to_string(),
            proxy: None,
            proxy_auth: None,
            insecure: false,
            random_agent: false,
            timeout: 120,
            retries: 2,
            parallel: Some(5),
            rate_limit: None,
            check_status: false,
            include_status: vec![],
            exclude_status: vec![],
            extract_links: false,
            include_robots: true,
            include_sitemap: true,
            exclude_robots: false,
            exclude_sitemap: false,
            incremental: false,
            cache_type: "sqlite".to_string(),
            cache_path: None,
            redis_url: None,
            cache_ttl: 86400,
            no_cache: false,
            exclude_providers: vec![],
            all_providers: false,
            list_providers: false,
            show_sources: false,
            stats: false,
            domain_list: vec![],
            max_time: 0,
            rate_limit_by: vec![],
            provider_config: None,
            output_dir: None,
            wayback_from: None,
            wayback_to: None,
            github_api_key: vec![],
        };
        assert_eq!(args.output, None);
        assert_eq!(args.format, "plain");
        assert_eq!(args.providers, vec!["wayback", "cc", "otx"]);

        // Apply config to args
        config.apply_to_args(&mut args);

        // Verify args were updated correctly
        assert_eq!(args.output, Some(PathBuf::from("output.txt")));
        assert_eq!(args.format, "json");
        assert_eq!(args.providers, vec!["cc"]);
    }

    #[test]
    fn test_apply_to_args_ignores_invalid_network_values() {
        let mut config = Config::default();
        config.network.timeout = Some(0);
        config.network.parallel = Some(0);

        let mut args = Args::parse_from(["urx", "example.com"]);
        config.apply_to_args(&mut args);

        assert_eq!(args.timeout, 120);
        assert_eq!(args.parallel, Some(5));
    }

    #[test]
    fn test_apply_to_args_ignores_invalid_output_format_and_network_scope() {
        let mut config = Config::default();
        config.output.format = Some("yaml".to_string());
        config.network.network_scope = Some("providers only".to_string());

        let mut args = Args::parse_from(["urx", "example.com"]);
        config.apply_to_args(&mut args);

        assert_eq!(args.format, "plain");
        assert_eq!(args.network_scope, "all");
    }

    #[test]
    fn test_apply_to_args_normalizes_output_format_and_network_scope() {
        let mut config = Config::default();
        config.output.format = Some("JSON".to_string());
        config.network.network_scope = Some("TESTERS,PROVIDERS".to_string());

        let mut args = Args::parse_from(["urx", "example.com"]);
        config.apply_to_args(&mut args);

        assert_eq!(args.format, "json");
        assert_eq!(args.network_scope, "providers,testers");
    }

    #[test]
    fn test_provider_keys_config_parses_csv() -> Result<()> {
        let content = r#"
            vt_api_key = "key1, key2 ,key3"
            urlscan_api_key = "us1"
        "#;
        let file = create_temp_config_file(content);
        let cfg = ProviderKeysConfig::from_file(file.path())?;
        assert_eq!(cfg.vt_api_key.as_deref(), Some("key1, key2 ,key3"));
        assert_eq!(cfg.urlscan_api_key.as_deref(), Some("us1"));
        assert_eq!(cfg.zoomeye_api_key, None);
        Ok(())
    }

    #[test]
    fn test_config_load_returns_error_for_explicit_missing_file() {
        let args = Args::parse_from(["urx", "--config", "/definitely/missing.toml", "example.com"]);
        let err = Config::load(&args).unwrap_err();
        assert!(err.to_string().contains("Failed to read config file"));
    }

    #[test]
    fn test_provider_keys_load_returns_error_for_explicit_missing_file() {
        let args = Args::parse_from([
            "urx",
            "--provider-config",
            "/definitely/missing-provider.toml",
            "example.com",
        ]);
        let err = ProviderKeysConfig::load(&args).unwrap_err();
        assert!(err
            .to_string()
            .contains("Failed to read provider-config file"));
    }

    #[test]
    fn test_config_load_succeeds_without_explicit_file() -> Result<()> {
        let args = Args::parse_from(["urx", "example.com"]);
        let _cfg = Config::load(&args)?;
        Ok(())
    }

    #[test]
    fn test_provider_keys_load_succeeds_without_explicit_file() -> Result<()> {
        let args = Args::parse_from(["urx", "example.com"]);
        let _cfg = ProviderKeysConfig::load(&args)?;
        Ok(())
    }

    #[test]
    fn test_provider_keys_apply_to_args_respects_cli_supplied() {
        let cfg = ProviderKeysConfig {
            vt_api_key: Some("from-file".to_string()),
            urlscan_api_key: Some("us-from-file".to_string()),
            zoomeye_api_key: None,
        };
        let mut args = <Args as clap::Parser>::parse_from(["urx", "example.com"]);
        // Pretend the user supplied vt via CLI: provider-config should NOT
        // overwrite that.
        args.vt_api_key = vec!["cli-key".to_string()];

        cfg.apply_to_args(&mut args, true, false, false);

        assert_eq!(args.vt_api_key, vec!["cli-key".to_string()]);
        // urlscan was empty and not CLI-supplied -> file value applies and
        // is split on commas.
        assert_eq!(args.urlscan_api_key, vec!["us-from-file".to_string()]);
        // zoomeye not supplied anywhere -> stays empty.
        assert!(args.zoomeye_api_key.is_empty());
    }

    #[test]
    fn test_provider_keys_apply_to_args_splits_csv() {
        let cfg = ProviderKeysConfig {
            vt_api_key: Some("k1, k2 , ,k3".to_string()),
            urlscan_api_key: None,
            zoomeye_api_key: None,
        };
        let mut args = <Args as clap::Parser>::parse_from(["urx", "example.com"]);
        cfg.apply_to_args(&mut args, false, false, false);
        assert_eq!(args.vt_api_key, vec!["k1", "k2", "k3"]);
    }
}
