//! Tests demonstrating layered configuration from multiple sources:
//! CLI, environment variables, config files, and defaults.

use crate::assert_diag_snapshot;
use facet::Facet;
use figue::{self as args, Driver, MockEnv, builder};

/// A comprehensive server configuration with nested structures.
#[derive(Facet, Debug)]
struct Args {
    /// Enable verbose logging
    #[facet(args::named, args::short = 'v')]
    verbose: bool,

    /// Server configuration (from file/env)
    #[facet(args::config, args::env_prefix = "APP")]
    config: ServerConfig,
}

/// Server configuration loaded from config file or environment.
#[derive(Facet, Debug)]
struct ServerConfig {
    /// The host to bind to
    #[facet(default = "localhost")]
    host: String,

    /// The port to listen on
    #[facet(default = 8080)]
    port: u16,

    /// Database connection settings
    database: DatabaseConfig,

    /// Optional TLS configuration
    #[facet(default)]
    tls: Option<TlsConfig>,
}

/// Database connection configuration.
#[derive(Facet, Debug)]
struct DatabaseConfig {
    /// Database connection URL
    url: String,

    /// Maximum number of connections in the pool
    #[facet(default = 10)]
    max_connections: u32,

    /// Connection timeout in seconds
    #[facet(default = 30)]
    timeout_secs: u64,
}

/// TLS configuration for secure connections.
#[derive(Facet, Debug)]
struct TlsConfig {
    /// Path to the certificate file
    cert_path: String,

    /// Path to the private key file
    key_path: String,
}

#[derive(Facet, Debug)]
struct ArgsWithFlattenedConfigRoot {
    /// Run configuration; config sources keep this root namespace.
    #[facet(args::config, args::env_prefix = "BEE_RUN")]
    #[facet(flatten)]
    run: FlattenedRunConfig,
}

#[derive(Facet, Debug)]
struct FlattenedRunConfig {
    model: String,

    #[facet(default)]
    tag: Vec<String>,

    #[facet(default)]
    tui: bool,
}

#[test]
fn test_layered_all_sources() {
    // Config file content (lowest priority after defaults)
    let config_json = r#"{
        "config": {
            "host": "0.0.0.0",
            "port": 3000,
            "database": {
                "url": "postgres://localhost/mydb",
                "max_connections": 20
            }
        }
    }"#;

    // Environment variables (higher priority than file)
    let env = MockEnv::from_pairs([("APP__PORT", "4000"), ("APP__DATABASE__TIMEOUT_SECS", "60")]);

    // CLI args (highest priority)
    // --verbose is set via CLI
    // --config.host could override but we'll let file win for this field

    let config = builder::<Args>()
        .unwrap()
        .cli(|cli| cli.args(["--verbose"]))
        .env(|e| e.source(env))
        .file(|f| f.content(config_json, "config.json"))
        .build();

    let driver = Driver::new(config);
    let args = driver.run().unwrap();

    // CLI: --verbose
    assert!(args.verbose, "verbose should be true from CLI");
    // File: host = "0.0.0.0"
    assert_eq!(args.config.host, "0.0.0.0", "host should come from file");
    // Env overrides file: port
    assert_eq!(args.config.port, 4000, "port should be overridden by env");
    // File: database.url
    assert_eq!(
        args.config.database.url, "postgres://localhost/mydb",
        "database.url should come from file"
    );
    // File: database.max_connections
    assert_eq!(
        args.config.database.max_connections, 20,
        "max_connections should come from file"
    );
    // Env overrides default: database.timeout_secs
    assert_eq!(
        args.config.database.timeout_secs, 60,
        "timeout_secs should be overridden by env"
    );
    // Default: tls is None
    assert!(args.config.tls.is_none(), "tls should be None (default)");
}

#[test]
fn test_flattened_config_root_merges_namespaced_sources_then_deserializes_flat() {
    let config_json = r#"{
        "run": {
            "model": "file-model"
        }
    }"#;

    let env = MockEnv::from_pairs([("BEE_RUN__MODEL", "env-model")]);

    let config = builder::<ArgsWithFlattenedConfigRoot>()
        .unwrap()
        .cli(|cli| cli.args(["--tui", "--run.model", "cli-model"]))
        .env(|e| e.source(env))
        .file(|f| f.content(config_json, "config.json"))
        .build();

    let args = Driver::new(config).run().unwrap();

    assert!(args.run.tui, "flattened CLI flag should populate run.tui");
    assert!(
        args.run.tag.is_empty(),
        "explicit Vec default inside flattened config root should be preserved"
    );
    assert_eq!(
        args.run.model, "cli-model",
        "dotted CLI overrides should keep the config-root namespace and override env/file"
    );
}

#[test]
fn test_layered_missing_required_field() {
    // Config file missing database.url (required field)
    let config_json = r#"{
        "config": {
            "host": "127.0.0.1",
            "port": 5000,
            "database": {
                "max_connections": 5
            }
        }
    }"#;

    let env = MockEnv::from_pairs([("APP__DATABASE__TIMEOUT_SECS", "15")]);

    let config = builder::<Args>()
        .unwrap()
        .cli(|cli| cli.args(["-v"]))
        .env(|e| e.source(env))
        .file(|f| f.content(config_json, "config.json"))
        .build();

    let driver = Driver::new(config);
    let err = driver.run().unwrap_err();
    assert_diag_snapshot!(err);
}

#[test]
fn test_layered_cli_overrides_all() {
    // File sets everything
    let config_json = r#"{
        "config": {
            "host": "file-host",
            "port": 1111,
            "database": {
                "url": "postgres://file/db",
                "max_connections": 100,
                "timeout_secs": 999
            }
        }
    }"#;

    // Env also sets things
    let env = MockEnv::from_pairs([
        ("APP__HOST", "env-host"),
        ("APP__PORT", "2222"),
        ("APP__DATABASE__URL", "postgres://env/db"),
    ]);

    // CLI overrides host via --config.host
    let config = builder::<Args>()
        .unwrap()
        .cli(|cli| cli.args(["--config.host", "cli-host", "--config.port", "3333"]))
        .env(|e| e.source(env))
        .file(|f| f.content(config_json, "config.json"))
        .build();

    let driver = Driver::new(config);
    let args = driver.run().unwrap();

    // CLI wins for host
    assert_eq!(args.config.host, "cli-host", "host should come from CLI");
    // CLI wins for port
    assert_eq!(args.config.port, 3333, "port should come from CLI");
    // Env wins for database.url (no CLI override)
    assert_eq!(
        args.config.database.url, "postgres://env/db",
        "database.url should come from env"
    );
    // File wins for max_connections (no env or CLI override)
    assert_eq!(
        args.config.database.max_connections, 100,
        "max_connections should come from file"
    );
    // File wins for timeout_secs (no env or CLI override for this one)
    assert_eq!(
        args.config.database.timeout_secs, 999,
        "timeout_secs should come from file"
    );
}

#[derive(Facet, Debug)]
struct MultiConfigArgs {
    #[facet(args::config, args::env_prefix = "BEE", rename = "cfg")]
    cfg: PrimaryConfig,

    #[facet(args::config, args::env_prefix = "BEE_EVAL", rename = "eval")]
    eval: EvalConfig,

    #[facet(args::subcommand)]
    command: MultiConfigCommand,
}

#[derive(Facet, Debug)]
struct PrimaryConfig {
    #[facet(default = "localhost")]
    host: String,

    #[facet(default = 8080)]
    port: u16,
}

#[derive(Facet, Debug)]
struct EvalConfig {
    dataset: String,

    #[facet(default = 10)]
    samples: u32,

    #[facet(default)]
    enabled: bool,
}

#[derive(Facet, Debug)]
#[repr(u8)]
enum MultiConfigCommand {
    Run,
}

#[test]
fn test_layered_multiple_config_roots() {
    let config_json = r#"{
        "cfg": {
            "host": "file-host",
            "port": 3000
        },
        "eval": {
            "dataset": "file-dataset",
            "samples": 20
        }
    }"#;

    let env = MockEnv::from_pairs([("BEE__PORT", "4000"), ("BEE_EVAL__SAMPLES", "30")]);

    let config = builder::<MultiConfigArgs>()
        .unwrap()
        .cli(|cli| cli.args(["--cfg.host", "cli-host", "--eval.enabled", "true", "run"]))
        .env(|e| e.source(env))
        .file(|f| f.content(config_json, "config.json"))
        .build();

    let args = Driver::new(config).run().unwrap();

    assert_eq!(args.cfg.host, "cli-host");
    assert_eq!(args.cfg.port, 4000);
    assert_eq!(args.eval.dataset, "file-dataset");
    assert_eq!(args.eval.samples, 30);
    assert!(args.eval.enabled);
    assert!(matches!(args.command, MultiConfigCommand::Run));
}

#[test]
fn test_layered_multiple_config_roots_cli_file_targets_matching_root() {
    use std::io::Write;

    let mut cfg_file = tempfile::Builder::new().suffix(".json").tempfile().unwrap();
    write!(
        cfg_file,
        r#"{{
            "host": "file-host",
            "port": 3000
        }}"#
    )
    .unwrap();

    let cfg_path = cfg_file.path().to_str().unwrap();
    let config = builder::<MultiConfigArgs>()
        .unwrap()
        .cli(|cli| {
            cli.args([
                "--cfg",
                cfg_path,
                "--cfg.host",
                "cli-host",
                "--eval.dataset",
                "cli-dataset",
                "run",
            ])
        })
        .build();

    let args = Driver::new(config).run().unwrap();

    assert_eq!(args.cfg.host, "cli-host");
    assert_eq!(args.cfg.port, 3000);
    assert_eq!(args.eval.dataset, "cli-dataset");
    assert_eq!(args.eval.samples, 10);
    assert!(!args.eval.enabled);
    assert!(matches!(args.command, MultiConfigCommand::Run));
}

/// Simpler structure for testing dump output with all sources visible.
#[derive(Facet, Debug)]
struct SimpleArgs {
    /// Enable debug mode
    #[facet(args::named, args::short = 'd')]
    debug: bool,

    /// Application settings
    #[facet(args::config, args::env_prefix = "MYAPP")]
    settings: AppSettings,
}

#[derive(Facet, Debug)]
struct AppSettings {
    /// Application name
    name: String,

    /// Server host address
    #[facet(default = "127.0.0.1")]
    host: String,

    /// Server port number
    #[facet(default = 8080)]
    port: u16,

    /// Maximum retry attempts
    #[facet(default = 3)]
    max_retries: u32,

    /// Request timeout in milliseconds
    #[facet(default = 5000)]
    timeout_ms: u64,

    /// Enable experimental features
    #[facet(default)]
    experimental: bool,

    /// Logging configuration
    logging: LogConfig,

    /// Storage backend selection
    storage: StorageBackend,
}

/// Logging configuration with nested options.
#[derive(Facet, Debug)]
struct LogConfig {
    /// Log level (debug, info, warn, error)
    #[facet(default = "info")]
    level: String,

    /// Log output format
    #[facet(default)]
    format: LogFormat,

    /// Optional file output path
    #[facet(default)]
    file: Option<String>,
}

/// Log output format.
#[derive(Facet, Debug, Default)]
#[repr(u8)]
enum LogFormat {
    /// Plain text format
    #[default]
    Plain,
    /// JSON structured logging
    Json,
    /// Compact single-line format
    Compact,
}

/// Storage backend configuration - demonstrates enum with data.
#[derive(Facet, Debug)]
#[facet(rename_all = "kebab-case")]
#[repr(u8)]
#[allow(dead_code)]
enum StorageBackend {
    /// Local filesystem storage
    Local {
        /// Base path for storage
        path: String,
    },
    /// S3-compatible object storage
    S3 {
        /// S3 bucket name
        bucket: String,
        /// AWS region
        #[facet(default = "us-east-1")]
        region: String,
        /// Optional endpoint for S3-compatible services
        #[facet(default)]
        endpoint: Option<String>,
    },
    /// In-memory storage (for testing)
    Memory,
}

#[test]
fn test_layered_dump_shows_all_sources() {
    // This test shows missing required fields with values from multiple sources
    // Demonstrates: nested structs, enums with data, various sources, deep missing fields
    let config_json = r#"{
        "settings": {
            "host": "0.0.0.0",
            "port": 3000,
            "max_retries": 5,
            "logging": {
                "level": "debug",
                "format": "Json"
            },
            "storage": {
                "s3": {
                    "region": "eu-west-1"
                }
            }
        }
    }"#;

    let env = MockEnv::from_pairs([
        ("MYAPP__PORT", "4000"),                  // overrides file
        ("MYAPP__TIMEOUT_MS", "10000"),           // overrides default
        ("MYAPP__LOGGING__FILE", "/var/log/app"), // sets optional field
    ]);

    let config = builder::<SimpleArgs>()
        .unwrap()
        .cli(|cli| cli.args(["--debug", "--settings.experimental", "true"]))
        .env(|e| e.source(env))
        .file(|f| f.content(config_json, "app.json"))
        .build();

    let driver = Driver::new(config);
    let err = driver.run().unwrap_err();
    // This snapshot should show:
    // - debug: true (from CLI)
    // - name: MISSING (top-level)
    // - host: 0.0.0.0 (from file)
    // - port: 4000 (from env, overriding file's 3000)
    // - max_retries: 5 (from file)
    // - timeout_ms: 10000 (from env, overriding default)
    // - experimental: true (from CLI)
    // - logging.level: debug (from file)
    // - logging.format: Json (from file)
    // - logging.file: /var/log/app (from env)
    // - storage: S3:: (from file)
    //   - bucket: MISSING (deep missing field!)
    //   - region: eu-west-1 (from file)
    //   - endpoint: <default>
    assert_diag_snapshot!(err);
}
