use std::path::PathBuf;

use bitcoin::hashes::{sha256, Hash};
use cdk::nuts::{CurrencyUnit, PublicKey};
use cdk::Amount;
use cdk_axum::cache;
use cdk_common::common::QuoteTTL;
use config::{Config, ConfigError, File};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
#[serde(rename_all = "lowercase")]
pub enum LoggingOutput {
    /// Log to stderr only
    Stderr,
    /// Log to file only
    File,
    /// Log to both stderr and file (default)
    #[default]
    Both,
}

impl std::str::FromStr for LoggingOutput {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "stderr" => Ok(LoggingOutput::Stderr),
            "file" => Ok(LoggingOutput::File),
            "both" => Ok(LoggingOutput::Both),
            _ => Err(format!(
                "Unknown logging output: {s}. Valid options: stdout, file, both"
            )),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct LoggingConfig {
    /// Where to output logs: stdout, file, or both
    #[serde(default)]
    pub output: LoggingOutput,
    /// Log level for console output (when stdout or both)
    pub console_level: Option<String>,
    /// Log level for file output (when file or both)
    pub file_level: Option<String>,
}

#[derive(Clone, Serialize, Deserialize)]
pub struct Info {
    pub url: String,
    pub listen_host: String,
    pub listen_port: u16,
    /// Overrides mnemonic
    pub seed: Option<String>,
    pub mnemonic: Option<String>,
    pub signatory_url: Option<String>,
    pub signatory_certs: Option<String>,
    pub input_fee_ppk: Option<u64>,
    /// Use keyset v2
    pub use_keyset_v2: Option<bool>,

    pub http_cache: cache::Config,

    /// Logging configuration
    #[serde(default)]
    pub logging: LoggingConfig,

    /// When this is set to true, the mint exposes a very simple info page at `/`
    /// showing the mint name and description.
    ///
    /// This requires `mintd` was built with the `info-page` feature flag.
    pub enable_info_page: Option<bool>,

    /// Optional persisted quote TTL values (seconds) to initialize the database with
    /// when RPC is disabled or on first-run when RPC is enabled.
    /// If not provided, defaults are used.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub quote_ttl: Option<QuoteTTL>,
}

impl Default for Info {
    fn default() -> Self {
        Info {
            url: String::new(),
            listen_host: "127.0.0.1".to_string(),
            listen_port: 8091, // Default to port 8091 instead of 0
            seed: None,
            mnemonic: None,
            signatory_url: None,
            signatory_certs: None,
            input_fee_ppk: None,
            use_keyset_v2: None,
            http_cache: cache::Config::default(),
            enable_info_page: Some(true),
            logging: LoggingConfig::default(),
            quote_ttl: None,
        }
    }
}

impl std::fmt::Debug for Info {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        // Use a fallback approach that won't panic
        let mnemonic_display: String = {
            if let Some(mnemonic) = self.mnemonic.as_ref() {
                let hash = sha256::Hash::hash(mnemonic.as_bytes());
                format!("<hashed: {hash}>")
            } else {
                format!("<url: {}>", self.signatory_url.clone().unwrap_or_default())
            }
        };

        f.debug_struct("Info")
            .field("url", &self.url)
            .field("listen_host", &self.listen_host)
            .field("listen_port", &self.listen_port)
            .field("mnemonic", &mnemonic_display)
            .field("input_fee_ppk", &self.input_fee_ppk)
            .field("use_keyset_v2", &self.use_keyset_v2)
            .field("http_cache", &self.http_cache)
            .field("logging", &self.logging)
            .field("enable_info_page", &self.enable_info_page)
            .finish()
    }
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
#[serde(rename_all = "lowercase")]
pub enum LnBackend {
    #[default]
    None,
    #[cfg(feature = "cln")]
    Cln,
    #[cfg(feature = "lnbits")]
    LNbits,
    #[cfg(feature = "fakewallet")]
    FakeWallet,
    #[cfg(feature = "lnd")]
    Lnd,
    #[cfg(feature = "ldk-node")]
    LdkNode,
    #[cfg(feature = "grpc-processor")]
    GrpcProcessor,
}

impl std::str::FromStr for LnBackend {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            #[cfg(feature = "cln")]
            "cln" => Ok(LnBackend::Cln),
            #[cfg(feature = "lnbits")]
            "lnbits" => Ok(LnBackend::LNbits),
            #[cfg(feature = "fakewallet")]
            "fakewallet" => Ok(LnBackend::FakeWallet),
            #[cfg(feature = "lnd")]
            "lnd" => Ok(LnBackend::Lnd),
            #[cfg(feature = "ldk-node")]
            "ldk-node" | "ldknode" => Ok(LnBackend::LdkNode),
            #[cfg(feature = "grpc-processor")]
            "grpcprocessor" => Ok(LnBackend::GrpcProcessor),
            _ => Err(format!("Unknown Lightning backend: {s}")),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Ln {
    pub ln_backend: LnBackend,
    #[serde(default)]
    pub unit: CurrencyUnit,
    pub invoice_description: Option<String>,
    pub min_mint: Amount,
    pub max_mint: Amount,
    pub min_melt: Amount,
    pub max_melt: Amount,
}

impl Default for Ln {
    fn default() -> Self {
        Ln {
            ln_backend: LnBackend::default(),
            unit: CurrencyUnit::default(),
            invoice_description: None,
            min_mint: 1.into(),
            max_mint: 500_000.into(),
            min_melt: 1.into(),
            max_melt: 500_000.into(),
        }
    }
}

fn deserialize_ln<'de, D>(deserializer: D) -> Result<Vec<Ln>, D::Error>
where
    D: serde::Deserializer<'de>,
{
    #[derive(Deserialize)]
    #[serde(untagged)]
    enum LnOneOrMany {
        Many(Vec<Ln>),
        One(Ln),
    }

    match LnOneOrMany::deserialize(deserializer)? {
        LnOneOrMany::Many(ln) => Ok(ln),
        LnOneOrMany::One(ln) => Ok(vec![ln]),
    }
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
#[serde(rename_all = "lowercase")]
pub enum OnchainBackend {
    #[default]
    None,
    #[cfg(feature = "bdk")]
    Bdk,
    #[cfg(feature = "fakewallet")]
    FakeWallet,
}

impl std::str::FromStr for OnchainBackend {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "none" => Ok(OnchainBackend::None),
            #[cfg(feature = "bdk")]
            "bdk" => Ok(OnchainBackend::Bdk),
            #[cfg(feature = "fakewallet")]
            "fakewallet" => Ok(OnchainBackend::FakeWallet),
            _ => Err(format!("Unknown Onchain backend: {s}")),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Onchain {
    pub onchain_backend: OnchainBackend,
    pub min_mint: Amount,
    pub max_mint: Amount,
    pub min_melt: Amount,
    pub max_melt: Amount,
}

impl Default for Onchain {
    fn default() -> Self {
        Onchain {
            onchain_backend: OnchainBackend::default(),
            min_mint: 1.into(),
            max_mint: 500_000.into(),
            min_melt: 1.into(),
            max_melt: 500_000.into(),
        }
    }
}

#[cfg(feature = "bdk")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BatchConfig {
    /// How often the batch processor wakes up to check for ready intents
    #[serde(default = "default_bdk_poll_interval_secs")]
    pub poll_interval_secs: u64,
    /// Maximum number of intents to include in a single batch
    #[serde(default = "default_bdk_max_batch_size")]
    pub max_batch_size: usize,
    /// Average block interval used to derive default delayed tier deadlines.
    #[serde(default = "default_bdk_target_block_time_secs")]
    pub target_block_time_secs: u64,
    /// Optional override for how long standard-tier intents wait before being eligible
    #[serde(default)]
    pub standard_deadline_secs: Option<u64>,
    /// Optional override for how long economy-tier intents wait before being eligible
    #[serde(default)]
    pub economy_deadline_secs: Option<u64>,
    /// Fee tiers exposed in melt quotes. Order determines fee_index values.
    #[serde(default = "default_bdk_fee_options")]
    pub fee_options: Vec<String>,
    /// Quote-time fallback fee rate used when chain estimation fails, in sat/vB.
    #[serde(default = "default_bdk_fee_fallback_sat_per_vb")]
    pub fee_fallback_sat_per_vb: f64,
    /// Fee-rate cache TTL, in seconds.
    #[serde(default = "default_bdk_fee_cache_ttl_secs")]
    pub fee_cache_ttl_secs: u64,
    /// Maximum input count reserved for a quote estimate.
    #[serde(default = "default_bdk_quote_max_input_count")]
    pub quote_max_input_count: usize,
    /// Fixed safety margin added to quote-time fee estimates, in sats.
    #[serde(default = "default_bdk_quote_fixed_safety_sat")]
    pub quote_fixed_safety_sat: u64,
    /// Multiplicative safety margin applied after the raw quote fee estimate.
    #[serde(default = "default_bdk_quote_safety_multiplier")]
    pub quote_safety_multiplier: f64,
}

#[cfg(feature = "bdk")]
impl Default for BatchConfig {
    fn default() -> Self {
        Self {
            poll_interval_secs: default_bdk_poll_interval_secs(),
            max_batch_size: default_bdk_max_batch_size(),
            target_block_time_secs: default_bdk_target_block_time_secs(),
            standard_deadline_secs: None,
            economy_deadline_secs: None,
            fee_options: default_bdk_fee_options(),
            fee_fallback_sat_per_vb: default_bdk_fee_fallback_sat_per_vb(),
            fee_cache_ttl_secs: default_bdk_fee_cache_ttl_secs(),
            quote_max_input_count: default_bdk_quote_max_input_count(),
            quote_fixed_safety_sat: default_bdk_quote_fixed_safety_sat(),
            quote_safety_multiplier: default_bdk_quote_safety_multiplier(),
        }
    }
}

#[cfg(feature = "bdk")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Bdk {
    /// Fee percentage (e.g., 0.02 for 2%)
    #[serde(default = "default_fee_percent")]
    pub fee_percent: f32,
    /// Minimum reserve fee
    #[serde(default = "default_reserve_fee_min")]
    pub reserve_fee_min: Amount,
    /// Bitcoin network (mainnet, testnet, signet, regtest)
    pub network: Option<String>,
    /// Chain source type ("esplora" or "bitcoinrpc"; defaults to "bitcoinrpc")
    pub chain_source_type: Option<String>,
    /// Esplora URL (when chain_source_type = "esplora")
    pub esplora_url: Option<String>,
    /// Number of parallel Esplora requests during wallet sync.
    ///
    /// Public Esplora servers often rate-limit bursty clients, so the default
    /// is conservative. Increase only when using a private or higher-limit
    /// Esplora server.
    #[serde(default = "default_bdk_esplora_parallel_requests")]
    pub esplora_parallel_requests: usize,
    /// Bitcoin RPC host (when chain_source_type = "bitcoinrpc")
    pub bitcoind_rpc_host: Option<String>,
    /// Bitcoin RPC port
    pub bitcoind_rpc_port: Option<u16>,
    /// Bitcoin RPC user
    pub bitcoind_rpc_user: Option<String>,
    /// Bitcoin RPC password
    pub bitcoind_rpc_password: Option<String>,
    /// BIP-39 mnemonic for the BDK wallet
    pub mnemonic: Option<String>,
    /// Batch processor configuration
    #[serde(default)]
    pub batch_config: BatchConfig,
    /// Number of confirmations required for incoming payments.
    ///
    /// Must be >= 1. A value of 0 is rejected at startup because the
    /// confirmation check still requires the transaction to have an on-chain
    /// anchor (i.e. 0 would mean "confirmed in any block", not "accept
    /// unconfirmed"). Use 1 for "accept any confirmation".
    #[serde(default = "default_bdk_num_confs")]
    pub num_confs: u32,
    /// Minimum receive amount in sats
    #[serde(default = "default_bdk_min_receive_amount_sat")]
    pub min_receive_amount_sat: u64,
    /// Minimum send amount in sats
    #[serde(default = "default_bdk_min_send_amount_sat")]
    pub min_send_amount_sat: u64,
    /// Wallet sync interval in seconds
    #[serde(default = "default_bdk_sync_interval_secs")]
    pub sync_interval_secs: u64,
}

#[cfg(feature = "bdk")]
impl Default for Bdk {
    fn default() -> Self {
        Self {
            fee_percent: default_fee_percent(),
            reserve_fee_min: default_reserve_fee_min(),
            network: None,
            chain_source_type: None,
            esplora_url: None,
            esplora_parallel_requests: default_bdk_esplora_parallel_requests(),
            bitcoind_rpc_host: None,
            bitcoind_rpc_port: None,
            bitcoind_rpc_user: None,
            bitcoind_rpc_password: None,
            mnemonic: None,
            batch_config: BatchConfig::default(),
            num_confs: default_bdk_num_confs(),
            min_receive_amount_sat: default_bdk_min_receive_amount_sat(),
            min_send_amount_sat: default_bdk_min_send_amount_sat(),
            sync_interval_secs: default_bdk_sync_interval_secs(),
        }
    }
}

#[cfg(feature = "bdk")]
impl Bdk {
    /// Validate BDK settings that must be rejected before the backend starts.
    pub fn validate(&self) -> Result<(), String> {
        if self.num_confs == 0 {
            return Err(
                "BDK num_confs must be >= 1 (0 is rejected because it still \
                 requires an on-chain anchor and is almost never intended; \
                 use 1 for 'any confirmation')"
                    .to_string(),
            );
        }

        if self.min_send_amount_sat == 0 {
            return Err("BDK min_send_amount_sat must be >= 1".to_string());
        }

        if self.batch_config.target_block_time_secs == 0 {
            return Err("BDK batch_config.target_block_time_secs must be >= 1".to_string());
        }

        validate_bdk_fee_options(&self.batch_config.fee_options)?;

        Ok(())
    }
}

#[cfg(feature = "bdk")]
fn default_bdk_num_confs() -> u32 {
    6
}

#[cfg(feature = "bdk")]
fn default_bdk_min_receive_amount_sat() -> u64 {
    1000
}

#[cfg(feature = "bdk")]
fn default_bdk_min_send_amount_sat() -> u64 {
    546
}

#[cfg(feature = "bdk")]
fn default_bdk_sync_interval_secs() -> u64 {
    30
}

#[cfg(feature = "bdk")]
fn default_bdk_esplora_parallel_requests() -> usize {
    1
}

#[cfg(feature = "bdk")]
fn default_bdk_poll_interval_secs() -> u64 {
    30
}

#[cfg(feature = "bdk")]
fn default_bdk_max_batch_size() -> usize {
    50
}

#[cfg(feature = "bdk")]
fn default_bdk_target_block_time_secs() -> u64 {
    cdk_bdk::DEFAULT_TARGET_BLOCK_TIME_SECS
}

#[cfg(feature = "bdk")]
fn default_bdk_fee_options() -> Vec<String> {
    vec!["immediate".to_string()]
}

#[cfg(feature = "bdk")]
fn validate_bdk_fee_options(fee_options: &[String]) -> Result<(), String> {
    let tiers = fee_options
        .iter()
        .map(|tier| {
            cdk_bdk::PaymentTier::from_config_name(tier).ok_or_else(|| {
                format!(
                    "Unknown BDK batch_config.fee_options tier '{tier}'; expected immediate, standard, or economy"
                )
            })
        })
        .collect::<Result<Vec<_>, _>>()?;

    cdk_bdk::types::validate_fee_options(&tiers)
}

#[cfg(feature = "bdk")]
fn default_bdk_fee_fallback_sat_per_vb() -> f64 {
    2.0
}

#[cfg(feature = "bdk")]
fn default_bdk_fee_cache_ttl_secs() -> u64 {
    60
}

#[cfg(feature = "bdk")]
fn default_bdk_quote_max_input_count() -> usize {
    24
}

#[cfg(feature = "bdk")]
fn default_bdk_quote_fixed_safety_sat() -> u64 {
    500
}

#[cfg(feature = "bdk")]
fn default_bdk_quote_safety_multiplier() -> f64 {
    1.25
}

#[cfg(feature = "lnbits")]
#[derive(Clone, Serialize, Deserialize)]
pub struct LNbits {
    pub admin_api_key: String,
    pub invoice_api_key: String,
    pub lnbits_api: String,
    #[serde(default = "default_fee_percent")]
    pub fee_percent: f32,
    #[serde(default = "default_reserve_fee_min")]
    pub reserve_fee_min: Amount,
}

#[cfg(feature = "lnbits")]
impl std::fmt::Debug for LNbits {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("LNbits")
            .field("admin_api_key", &"[REDACTED]")
            .field("invoice_api_key", &"[REDACTED]")
            .field("lnbits_api", &self.lnbits_api)
            .field("fee_percent", &self.fee_percent)
            .field("reserve_fee_min", &self.reserve_fee_min)
            .finish()
    }
}

#[cfg(feature = "lnbits")]
impl Default for LNbits {
    fn default() -> Self {
        Self {
            admin_api_key: String::new(),
            invoice_api_key: String::new(),
            lnbits_api: String::new(),
            fee_percent: 0.02,
            reserve_fee_min: 2.into(),
        }
    }
}

#[cfg(feature = "cln")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Cln {
    pub rpc_path: PathBuf,
    #[serde(default = "default_cln_bolt12")]
    pub bolt12: bool,
    #[serde(default)]
    pub expose_private_channels: bool,
    #[serde(default = "default_fee_percent")]
    pub fee_percent: f32,
    #[serde(default = "default_reserve_fee_min")]
    pub reserve_fee_min: Amount,
}

#[cfg(feature = "cln")]
impl Default for Cln {
    fn default() -> Self {
        Self {
            rpc_path: PathBuf::new(),
            bolt12: true,
            expose_private_channels: false,
            fee_percent: 0.02,
            reserve_fee_min: 2.into(),
        }
    }
}

#[cfg(feature = "cln")]
fn default_cln_bolt12() -> bool {
    true
}

#[cfg(feature = "lnd")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Lnd {
    pub address: String,
    pub cert_file: PathBuf,
    pub macaroon_file: PathBuf,
    #[serde(default = "default_fee_percent")]
    pub fee_percent: f32,
    #[serde(default = "default_reserve_fee_min")]
    pub reserve_fee_min: Amount,
}

#[cfg(feature = "lnd")]
impl Default for Lnd {
    fn default() -> Self {
        Self {
            address: String::new(),
            cert_file: PathBuf::new(),
            macaroon_file: PathBuf::new(),
            fee_percent: 0.02,
            reserve_fee_min: 2.into(),
        }
    }
}

#[cfg(feature = "ldk-node")]
#[derive(Clone, Serialize, Deserialize)]
pub struct LdkNode {
    /// Fee percentage (e.g., 0.02 for 2%)
    #[serde(default = "default_ldk_fee_percent")]
    pub fee_percent: f32,
    /// Minimum reserve fee
    #[serde(default = "default_ldk_reserve_fee_min")]
    pub reserve_fee_min: Amount,
    /// Bitcoin network (mainnet, testnet, signet, regtest)
    pub bitcoin_network: Option<String>,
    /// Chain source type (esplora or bitcoinrpc)
    pub chain_source_type: Option<String>,
    /// Esplora URL (when chain_source_type = "esplora")
    pub esplora_url: Option<String>,
    /// Bitcoin RPC configuration (when chain_source_type = "bitcoinrpc")
    pub bitcoind_rpc_host: Option<String>,
    pub bitcoind_rpc_port: Option<u16>,
    pub bitcoind_rpc_user: Option<String>,
    pub bitcoind_rpc_password: Option<String>,
    /// Storage directory path
    pub storage_dir_path: Option<String>,
    /// Log directory path (logging stdout if omitted)
    pub log_dir_path: Option<String>,
    /// LDK node listening host
    pub ldk_node_host: Option<String>,
    /// LDK node listening port
    pub ldk_node_port: Option<u16>,
    /// LDK node announcement addresses
    pub ldk_node_announce_addresses: Option<Vec<String>>,
    /// Gossip source type (p2p or rgs)
    pub gossip_source_type: Option<String>,
    /// Rapid Gossip Sync URL (when gossip_source_type = "rgs")
    pub rgs_url: Option<String>,
    /// Webserver host (defaults to 127.0.0.1)
    #[serde(default = "default_webserver_host")]
    pub webserver_host: Option<String>,
    /// Webserver port
    #[serde(default = "default_webserver_port")]
    pub webserver_port: Option<u16>,
    /// LDK node mnemonic
    /// If not set, LDK node will use its default seed storage mechanism
    pub ldk_node_mnemonic: Option<String>,
}

#[cfg(feature = "ldk-node")]
impl Default for LdkNode {
    fn default() -> Self {
        Self {
            fee_percent: default_ldk_fee_percent(),
            reserve_fee_min: default_ldk_reserve_fee_min(),
            bitcoin_network: None,
            chain_source_type: None,
            esplora_url: None,
            bitcoind_rpc_host: None,
            bitcoind_rpc_port: None,
            bitcoind_rpc_user: None,
            ldk_node_announce_addresses: None,
            bitcoind_rpc_password: None,
            storage_dir_path: None,
            ldk_node_host: None,
            log_dir_path: None,
            ldk_node_port: None,
            gossip_source_type: None,
            rgs_url: None,
            webserver_host: default_webserver_host(),
            webserver_port: default_webserver_port(),
            ldk_node_mnemonic: None,
        }
    }
}

#[cfg(feature = "ldk-node")]
impl std::fmt::Debug for LdkNode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("LdkNode")
            .field("fee_percent", &self.fee_percent)
            .field("reserve_fee_min", &self.reserve_fee_min)
            .field("bitcoin_network", &self.bitcoin_network)
            .field("chain_source_type", &self.chain_source_type)
            .field("esplora_url", &self.esplora_url)
            .field("bitcoind_rpc_host", &self.bitcoind_rpc_host)
            .field("bitcoind_rpc_port", &self.bitcoind_rpc_port)
            .field("bitcoind_rpc_user", &self.bitcoind_rpc_user)
            .field("bitcoind_rpc_password", &"[REDACTED]")
            .field("storage_dir_path", &self.storage_dir_path)
            .field("log_dir_path", &self.log_dir_path)
            .field("ldk_node_host", &self.ldk_node_host)
            .field("ldk_node_port", &self.ldk_node_port)
            .field(
                "ldk_node_announce_addresses",
                &self.ldk_node_announce_addresses,
            )
            .field("gossip_source_type", &self.gossip_source_type)
            .field("rgs_url", &self.rgs_url)
            .field("webserver_host", &self.webserver_host)
            .field("webserver_port", &self.webserver_port)
            .field("ldk_node_mnemonic", &"[REDACTED]")
            .finish()
    }
}

#[cfg(feature = "ldk-node")]
fn default_ldk_fee_percent() -> f32 {
    0.04
}

#[cfg(feature = "ldk-node")]
fn default_ldk_reserve_fee_min() -> Amount {
    4.into()
}

#[cfg(feature = "ldk-node")]
fn default_webserver_host() -> Option<String> {
    Some("127.0.0.1".to_string())
}

#[cfg(feature = "ldk-node")]
fn default_webserver_port() -> Option<u16> {
    Some(8091)
}

#[cfg(feature = "fakewallet")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FakeWalletKeysetRotation {
    /// Currency unit (e.g. "sat", "usd")
    pub unit: CurrencyUnit,
    /// Input fee in parts per thousand
    #[serde(default)]
    pub input_fee_ppk: u64,
    /// Keyset version: "v1" (Version00) or "v2" (Version01)
    #[serde(default = "default_keyset_version")]
    pub version: String,
    /// If true, the keyset will be created with a past expiry (expired)
    #[serde(default)]
    pub expired: bool,
}

#[cfg(feature = "fakewallet")]
fn default_keyset_version() -> String {
    "v1".to_string()
}

#[cfg(feature = "fakewallet")]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(untagged)]
pub enum FakeWalletCustomPaymentMethod {
    /// Custom method available for every supported fake wallet unit
    Method(String),
    /// Custom method available only for one unit
    MethodForUnit {
        /// Payment method name (e.g. "paypal", "venmo")
        method: String,
        /// Currency unit for this method
        unit: CurrencyUnit,
    },
}

#[cfg(feature = "fakewallet")]
impl FakeWalletCustomPaymentMethod {
    pub fn method(&self) -> &str {
        match self {
            Self::Method(method) => method,
            Self::MethodForUnit { method, .. } => method,
        }
    }

    pub fn applies_to_unit(&self, unit: &CurrencyUnit) -> bool {
        match self {
            Self::Method(_) => true,
            Self::MethodForUnit {
                unit: method_unit, ..
            } => method_unit == unit,
        }
    }
}

#[cfg(feature = "fakewallet")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FakeWallet {
    #[serde(default = "default_fake_wallet_supported_units")]
    pub supported_units: Vec<CurrencyUnit>,
    pub fee_percent: f32,
    pub reserve_fee_min: Amount,
    #[serde(default = "default_fake_wallet_custom_payment_methods")]
    pub custom_payment_methods: Vec<FakeWalletCustomPaymentMethod>,
    #[serde(default = "default_min_delay_time")]
    pub min_delay_time: u64,
    #[serde(default = "default_max_delay_time")]
    pub max_delay_time: u64,
    /// Additional keyset rotations to create during mint build
    #[serde(default)]
    pub keyset_rotations: Vec<FakeWalletKeysetRotation>,
}

#[cfg(feature = "fakewallet")]
impl Default for FakeWallet {
    fn default() -> Self {
        Self {
            supported_units: vec![CurrencyUnit::Sat],
            fee_percent: 0.02,
            reserve_fee_min: 2.into(),
            custom_payment_methods: default_fake_wallet_custom_payment_methods(),
            min_delay_time: 1,
            max_delay_time: 3,
            keyset_rotations: Vec::new(),
        }
    }
}

// Helper functions to provide default values
// Common fee defaults for all backends
#[cfg(any(feature = "cln", feature = "lnbits", feature = "lnd"))]
fn default_fee_percent() -> f32 {
    0.02
}

#[cfg(any(feature = "cln", feature = "lnbits", feature = "lnd"))]
fn default_reserve_fee_min() -> Amount {
    2.into()
}

#[cfg(feature = "fakewallet")]
fn default_min_delay_time() -> u64 {
    1
}

#[cfg(feature = "fakewallet")]
fn default_max_delay_time() -> u64 {
    3
}

#[cfg(feature = "fakewallet")]
fn default_fake_wallet_custom_payment_methods() -> Vec<FakeWalletCustomPaymentMethod> {
    vec![FakeWalletCustomPaymentMethod::Method("paypal".to_string())]
}

#[cfg(feature = "fakewallet")]
fn default_fake_wallet_supported_units() -> Vec<CurrencyUnit> {
    vec![CurrencyUnit::Sat]
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct GrpcProcessor {
    #[serde(default)]
    pub supported_units: Vec<CurrencyUnit>,
    #[serde(default = "default_grpc_addr")]
    pub addr: String,
    #[serde(default = "default_grpc_port")]
    pub port: u16,
    #[serde(default)]
    pub tls_dir: Option<PathBuf>,
}

impl Default for GrpcProcessor {
    fn default() -> Self {
        Self {
            supported_units: Vec::new(),
            addr: default_grpc_addr(),
            port: default_grpc_port(),
            tls_dir: None,
        }
    }
}

fn default_grpc_addr() -> String {
    "127.0.0.1".to_string()
}

fn default_grpc_port() -> u16 {
    50051
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
#[serde(rename_all = "lowercase")]
pub enum DatabaseEngine {
    #[default]
    Sqlite,
    Postgres,
}

impl std::str::FromStr for DatabaseEngine {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "sqlite" => Ok(DatabaseEngine::Sqlite),
            "postgres" => Ok(DatabaseEngine::Postgres),
            _ => Err(format!("Unknown database engine: {s}")),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Database {
    pub engine: DatabaseEngine,
    pub postgres: Option<PostgresConfig>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AuthDatabase {
    pub postgres: Option<PostgresAuthConfig>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PostgresAuthConfig {
    pub url: String,
    pub tls_mode: Option<String>,
    pub max_connections: Option<usize>,
    pub connection_timeout_seconds: Option<u64>,
}

impl Default for PostgresAuthConfig {
    fn default() -> Self {
        Self {
            url: String::new(),
            tls_mode: Some("disable".to_string()),
            max_connections: Some(20),
            connection_timeout_seconds: Some(10),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PostgresConfig {
    pub url: String,
    pub tls_mode: Option<String>,
    pub max_connections: Option<usize>,
    pub connection_timeout_seconds: Option<u64>,
}

impl Default for PostgresConfig {
    fn default() -> Self {
        Self {
            url: String::new(),
            tls_mode: Some("disable".to_string()),
            max_connections: Some(20),
            connection_timeout_seconds: Some(10),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum AuthType {
    Clear,
    Blind,
    #[default]
    None,
}

impl std::str::FromStr for AuthType {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "clear" => Ok(AuthType::Clear),
            "blind" => Ok(AuthType::Blind),
            "none" => Ok(AuthType::None),
            _ => Err(format!("Unknown auth type: {s}")),
        }
    }
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Auth {
    #[serde(default)]
    pub auth_enabled: bool,
    pub openid_discovery: String,
    pub openid_client_id: String,
    pub mint_max_bat: u64,
    #[serde(default = "default_blind")]
    pub mint: AuthType,
    #[serde(default)]
    pub get_mint_quote: AuthType,
    #[serde(default)]
    pub check_mint_quote: AuthType,
    #[serde(default)]
    pub melt: AuthType,
    #[serde(default)]
    pub get_melt_quote: AuthType,
    #[serde(default)]
    pub check_melt_quote: AuthType,
    #[serde(default = "default_blind")]
    pub swap: AuthType,
    #[serde(default = "default_blind")]
    pub restore: AuthType,
    #[serde(default)]
    pub check_proof_state: AuthType,
    /// Enable WebSocket authentication support
    #[serde(default = "default_blind")]
    pub websocket_auth: AuthType,
}

fn default_blind() -> AuthType {
    AuthType::Blind
}

/// CDK settings, derived from `config.toml`
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Settings {
    pub info: Info,
    pub mint_info: MintInfo,
    #[serde(default, deserialize_with = "deserialize_ln")]
    pub ln: Vec<Ln>,
    pub onchain: Option<Onchain>,
    /// Transaction limits for DoS protection
    #[serde(default)]
    pub limits: Limits,
    #[cfg(feature = "cln")]
    pub cln: Option<Cln>,
    #[cfg(feature = "lnbits")]
    pub lnbits: Option<LNbits>,
    #[cfg(feature = "lnd")]
    pub lnd: Option<Lnd>,
    #[cfg(feature = "ldk-node")]
    pub ldk_node: Option<LdkNode>,
    #[cfg(feature = "fakewallet")]
    pub fake_wallet: Option<FakeWallet>,
    pub grpc_processor: Option<GrpcProcessor>,
    #[cfg(feature = "bdk")]
    pub bdk: Option<Bdk>,
    pub database: Database,
    pub auth_database: Option<AuthDatabase>,
    #[cfg(feature = "management-rpc")]
    pub mint_management_rpc: Option<MintManagementRpc>,
    pub auth: Option<Auth>,
    #[cfg(feature = "prometheus")]
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub prometheus: Option<Prometheus>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg(feature = "prometheus")]
pub struct Prometheus {
    pub enabled: bool,
    pub address: Option<String>,
    pub port: Option<u16>,
}

/// Transaction limits configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Limits {
    /// Maximum number of inputs allowed per transaction (swap/melt)
    #[serde(default = "default_max_inputs")]
    pub max_inputs: usize,
    /// Maximum number of outputs allowed per transaction (mint/swap/melt)
    #[serde(default = "default_max_outputs")]
    pub max_outputs: usize,
}

impl Default for Limits {
    fn default() -> Self {
        Self {
            max_inputs: 1000,
            max_outputs: 1000,
        }
    }
}

fn default_max_inputs() -> usize {
    1000
}

fn default_max_outputs() -> usize {
    1000
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct MintInfo {
    /// name of the mint and should be recognizable
    pub name: String,
    /// hex pubkey of the mint
    pub pubkey: Option<PublicKey>,
    /// short description of the mint
    pub description: String,
    /// long description
    pub description_long: Option<String>,
    /// url to the mint icon
    pub icon_url: Option<String>,
    /// message of the day that the wallet must display to the user
    pub motd: Option<String>,
    /// Nostr publickey
    pub contact_nostr_public_key: Option<String>,
    /// Contact email
    pub contact_email: Option<String>,
    /// URL to the terms of service
    pub tos_url: Option<String>,
}

#[cfg(feature = "management-rpc")]
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct MintManagementRpc {
    /// When this is set to `true` the mint use the config file for the initial set up on first start.
    /// Changes to the `[mint_info]` after this **MUST** be made via the RPC changes to the config file or env vars will be ignored.
    pub enabled: bool,
    pub address: Option<String>,
    pub port: Option<u16>,
    pub tls_dir_path: Option<PathBuf>,
}

impl Settings {
    /// Validate payment backend combinations after config and env overrides are applied.
    pub fn validate_backend_pairing(&self) -> Result<(), String> {
        #[cfg(feature = "fakewallet")]
        self.validate_fake_wallet_backend_pairing()?;

        Ok(())
    }

    #[cfg(feature = "fakewallet")]
    fn validate_fake_wallet_backend_pairing(&self) -> Result<(), String> {
        let onchain_backend = self
            .onchain
            .as_ref()
            .map(|onchain| &onchain.onchain_backend)
            .unwrap_or(&OnchainBackend::None);

        let has_fake_wallet_ln_backend = self
            .ln
            .iter()
            .any(|ln| ln.ln_backend == LnBackend::FakeWallet);
        let has_real_ln_backend = self
            .ln
            .iter()
            .any(|ln| !matches!(ln.ln_backend, LnBackend::None | LnBackend::FakeWallet));

        // A fake Lightning backend cannot be combined with a real one.
        if has_fake_wallet_ln_backend && has_real_ln_backend {
            return Err(
                "ln_backend = \"fakewallet\" cannot be combined with a real \
                 Lightning backend; use only fakewallet backends or only real backends"
                    .to_string(),
            );
        }

        match onchain_backend {
            #[cfg(feature = "bdk")]
            OnchainBackend::Bdk if has_fake_wallet_ln_backend => {
                return Err("ln_backend = \"fakewallet\" cannot be combined with \
                     onchain_backend = \"bdk\"; use onchain_backend = \
                     \"fakewallet\" or \"none\""
                    .to_string());
            }
            OnchainBackend::FakeWallet if has_real_ln_backend => {
                return Err("onchain_backend = \"fakewallet\" cannot be combined with \
                     a real Lightning backend; use ln_backend = \"fakewallet\" \
                     or \"none\""
                    .to_string());
            }
            _ => {}
        }

        Ok(())
    }

    #[must_use]
    pub fn new<P>(config_file_name: Option<P>) -> Self
    where
        P: Into<PathBuf>,
    {
        let default_settings = Self::default();
        // attempt to construct settings with file
        let from_file = Self::new_from_default(&default_settings, config_file_name);
        match from_file {
            Ok(f) => f,
            Err(e) => {
                tracing::error!(
                    "Error reading config file, falling back to defaults. Error: {e:?}"
                );
                default_settings
            }
        }
    }

    pub fn try_new<P>(config_file_name: Option<P>) -> Result<Self, ConfigError>
    where
        P: Into<PathBuf>,
    {
        Self::new_from_default(&Self::default(), config_file_name)
    }

    fn new_from_default<P>(
        default: &Settings,
        config_file_name: Option<P>,
    ) -> Result<Self, ConfigError>
    where
        P: Into<PathBuf>,
    {
        let mut default_config_file_name = home::home_dir()
            .ok_or(ConfigError::NotFound("Config Path".to_string()))?
            .join("cashu-rs-mint");

        default_config_file_name.push("config.toml");
        let config: String = match config_file_name {
            Some(value) => value.into().to_string_lossy().to_string(),
            None => default_config_file_name.to_string_lossy().to_string(),
        };
        let builder = Config::builder();
        let config: Config = builder
            // use defaults
            .add_source(Config::try_from(default)?)
            // override with file contents
            .add_source(File::with_name(&config))
            .build()?;
        let settings: Settings = config.try_deserialize()?;

        Ok(settings)
    }
}

#[cfg(test)]
mod tests {

    use super::*;

    fn config_env_lock() -> std::sync::MutexGuard<'static, ()> {
        static CONFIG_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());

        CONFIG_ENV_LOCK
            .lock()
            .expect("config env test lock should not be poisoned")
    }

    #[cfg(feature = "bdk")]
    fn clear_bdk_env_vars() {
        std::env::remove_var(crate::env_vars::BDK_MNEMONIC_ENV_VAR);
        std::env::remove_var(crate::env_vars::BDK_NETWORK_ENV_VAR);
        std::env::remove_var(crate::env_vars::BDK_MIN_SEND_AMOUNT_SAT_ENV_VAR);
        std::env::remove_var(crate::env_vars::BDK_TARGET_BLOCK_TIME_SECS_ENV_VAR);
        std::env::remove_var(crate::env_vars::BDK_FEE_OPTIONS_ENV_VAR);
        std::env::remove_var(crate::env_vars::ENV_ONCHAIN_BACKEND);
    }

    #[test]
    fn test_info_debug_impl() {
        // Create a sample Info struct with test data
        let info = Info {
            url: "http://example.com".to_string(),
            listen_host: "127.0.0.1".to_string(),
            listen_port: 8080,
            mnemonic: Some("test secret mnemonic phrase".to_string()),
            input_fee_ppk: Some(100),
            ..Default::default()
        };

        // Convert the Info struct to a debug string
        let debug_output = format!("{info:?}");

        // Verify the debug output contains expected fields
        assert!(debug_output.contains("url: \"http://example.com\""));
        assert!(debug_output.contains("listen_host: \"127.0.0.1\""));
        assert!(debug_output.contains("listen_port: 8080"));

        // The mnemonic should be hashed, not displayed in plaintext
        assert!(!debug_output.contains("test secret mnemonic phrase"));
        assert!(debug_output.contains("<hashed: "));

        assert!(debug_output.contains("input_fee_ppk: Some(100)"));
    }

    #[test]
    fn test_info_debug_with_empty_mnemonic() {
        // Test with an empty mnemonic to ensure it doesn't panic
        let info = Info {
            url: "http://example.com".to_string(),
            listen_host: "127.0.0.1".to_string(),
            listen_port: 8080,
            mnemonic: Some("".to_string()), // Empty mnemonic
            ..Default::default()
        };

        // This should not panic
        let debug_output = format!("{:?}", info);

        // The empty mnemonic should still be hashed
        assert!(debug_output.contains("<hashed: "));
    }

    #[cfg(feature = "bdk")]
    #[test]
    fn test_bdk_default_min_send_amount_sat() {
        assert_eq!(Bdk::default().min_send_amount_sat, 546);
    }

    #[cfg(feature = "bdk")]
    #[test]
    fn test_bdk_config_min_send_amount_sat_override() {
        use std::{env, fs};

        let temp_dir = env::temp_dir().join("cdk_test_bdk_min_send_config");
        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
        let config_path = temp_dir.join("config.toml");

        let config_content = r#"
[bdk]
min_send_amount_sat = 1200
"#;
        fs::write(&config_path, config_content).expect("Failed to write config file");

        let settings = Settings::new(Some(&config_path));

        assert_eq!(
            settings
                .bdk
                .as_ref()
                .expect("bdk config should be present")
                .min_send_amount_sat,
            1200
        );

        let _ = fs::remove_dir_all(&temp_dir);
    }

    #[cfg(feature = "bdk")]
    #[test]
    fn test_bdk_env_min_send_amount_sat_override() {
        let _guard = config_env_lock();
        clear_bdk_env_vars();
        std::env::set_var(crate::env_vars::ENV_ONCHAIN_BACKEND, "bdk");
        std::env::set_var(crate::env_vars::BDK_NETWORK_ENV_VAR, "regtest");
        std::env::set_var(crate::env_vars::BDK_MIN_SEND_AMOUNT_SAT_ENV_VAR, "777");

        let mut settings = Settings::default();
        settings.from_env().expect("Failed to apply env vars");

        assert_eq!(
            settings
                .bdk
                .as_ref()
                .expect("bdk config should be present")
                .min_send_amount_sat,
            777
        );

        clear_bdk_env_vars();
    }

    #[cfg(feature = "bdk")]
    #[test]
    fn test_bdk_default_fee_options_immediate_only() {
        assert_eq!(
            Bdk::default().batch_config.fee_options,
            vec!["immediate".to_string()]
        );
    }

    #[cfg(feature = "bdk")]
    #[test]
    fn test_bdk_default_batch_deadlines_derive_from_target_block_time() {
        let batch_config: cdk_bdk::BatchConfig = Bdk::default().batch_config.into();

        assert_eq!(
            batch_config.target_block_time,
            std::time::Duration::from_secs(600)
        );
        assert_eq!(
            batch_config.standard_deadline,
            std::time::Duration::from_secs(3600)
        );
        assert_eq!(
            batch_config.economy_deadline,
            std::time::Duration::from_secs(86_400)
        );
        assert_eq!(
            batch_config.max_intent_age,
            Some(std::time::Duration::from_secs(86_430))
        );
    }

    #[cfg(feature = "bdk")]
    #[test]
    fn test_bdk_config_fee_options_override() {
        use std::{env, fs};

        let temp_dir = env::temp_dir().join("cdk_test_bdk_fee_options_config");
        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
        let config_path = temp_dir.join("config.toml");

        let config_content = r#"
[bdk.batch_config]
fee_options = ["immediate", "economy"]
"#;
        fs::write(&config_path, config_content).expect("Failed to write config file");

        let settings = Settings::new(Some(&config_path));

        assert_eq!(
            settings
                .bdk
                .as_ref()
                .expect("bdk config should be present")
                .batch_config
                .fee_options,
            vec!["immediate".to_string(), "economy".to_string()]
        );

        let _ = fs::remove_dir_all(&temp_dir);
    }

    #[cfg(feature = "bdk")]
    #[test]
    fn test_bdk_config_target_block_time_derives_deadlines() {
        use std::{env, fs};

        let temp_dir = env::temp_dir().join("cdk_test_bdk_target_block_time_config");
        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
        let config_path = temp_dir.join("config.toml");

        let config_content = r#"
[bdk.batch_config]
target_block_time_secs = 300
"#;
        fs::write(&config_path, config_content).expect("Failed to write config file");

        let settings = Settings::new(Some(&config_path));
        let batch_config: cdk_bdk::BatchConfig = settings
            .bdk
            .as_ref()
            .expect("bdk config should be present")
            .batch_config
            .clone()
            .into();

        assert_eq!(
            batch_config.target_block_time,
            std::time::Duration::from_secs(300)
        );
        assert_eq!(
            batch_config.standard_deadline,
            std::time::Duration::from_secs(1800)
        );
        assert_eq!(
            batch_config.economy_deadline,
            std::time::Duration::from_secs(43_200)
        );
        assert_eq!(
            batch_config.max_intent_age,
            Some(std::time::Duration::from_secs(43_230))
        );

        let _ = fs::remove_dir_all(&temp_dir);
    }

    #[cfg(feature = "bdk")]
    #[test]
    fn test_bdk_env_fee_options_override() {
        let _guard = config_env_lock();
        clear_bdk_env_vars();
        std::env::set_var(crate::env_vars::ENV_ONCHAIN_BACKEND, "bdk");
        std::env::set_var(crate::env_vars::BDK_NETWORK_ENV_VAR, "regtest");
        std::env::set_var(
            crate::env_vars::BDK_FEE_OPTIONS_ENV_VAR,
            "immediate,standard,economy",
        );

        let mut settings = Settings::default();
        settings.from_env().expect("Failed to apply env vars");

        assert_eq!(
            settings
                .bdk
                .as_ref()
                .expect("bdk config should be present")
                .batch_config
                .fee_options,
            vec![
                "immediate".to_string(),
                "standard".to_string(),
                "economy".to_string()
            ]
        );

        clear_bdk_env_vars();
    }

    #[cfg(feature = "bdk")]
    #[test]
    fn test_bdk_env_target_block_time_override() {
        let _guard = config_env_lock();
        clear_bdk_env_vars();
        std::env::set_var(crate::env_vars::ENV_ONCHAIN_BACKEND, "bdk");
        std::env::set_var(crate::env_vars::BDK_NETWORK_ENV_VAR, "regtest");
        std::env::set_var(crate::env_vars::BDK_TARGET_BLOCK_TIME_SECS_ENV_VAR, "120");

        let mut settings = Settings::default();
        settings.from_env().expect("Failed to apply env vars");
        let batch_config: cdk_bdk::BatchConfig = settings
            .bdk
            .as_ref()
            .expect("bdk config should be present")
            .batch_config
            .clone()
            .into();

        assert_eq!(
            batch_config.target_block_time,
            std::time::Duration::from_secs(120)
        );
        assert_eq!(
            batch_config.standard_deadline,
            std::time::Duration::from_secs(720)
        );
        assert_eq!(
            batch_config.economy_deadline,
            std::time::Duration::from_secs(17_280)
        );
        assert_eq!(
            batch_config.max_intent_age,
            Some(std::time::Duration::from_secs(17_310))
        );

        clear_bdk_env_vars();
    }

    #[cfg(feature = "bdk")]
    #[test]
    fn test_bdk_invalid_fee_options_rejected() {
        for fee_options in [
            Vec::new(),
            vec!["immediate".to_string(), "immediate".to_string()],
            vec!["urgent".to_string()],
            vec![
                "immediate".to_string(),
                "standard".to_string(),
                "economy".to_string(),
                "immediate".to_string(),
            ],
        ] {
            let bdk = Bdk {
                batch_config: BatchConfig {
                    fee_options,
                    ..BatchConfig::default()
                },
                ..Default::default()
            };

            let err = bdk.validate().expect_err("invalid fee options should fail");

            assert!(err.contains("fee_options"));
        }
    }

    #[cfg(feature = "bdk")]
    #[test]
    fn test_bdk_target_block_time_zero_rejected() {
        let bdk = Bdk {
            batch_config: BatchConfig {
                target_block_time_secs: 0,
                ..BatchConfig::default()
            },
            ..Default::default()
        };

        let err = bdk
            .validate()
            .expect_err("zero target block time should fail");

        assert!(err.contains("target_block_time_secs"));
    }

    #[cfg(feature = "bdk")]
    #[test]
    fn test_bdk_min_send_amount_sat_zero_rejected() {
        let bdk = Bdk {
            min_send_amount_sat: 0,
            ..Default::default()
        };

        let err = bdk.validate().expect_err("zero send minimum should fail");

        assert!(err.contains("min_send_amount_sat"));
    }

    #[cfg(all(feature = "fakewallet", feature = "bdk"))]
    #[test]
    fn test_fakewallet_ln_with_bdk_onchain_rejected() {
        let settings = Settings {
            ln: vec![Ln {
                ln_backend: LnBackend::FakeWallet,
                ..Default::default()
            }],
            onchain: Some(Onchain {
                onchain_backend: OnchainBackend::Bdk,
                ..Default::default()
            }),
            ..Default::default()
        };

        let err = settings
            .validate_backend_pairing()
            .expect_err("fake LN with BDK onchain should fail");

        assert!(err.contains("fakewallet"));
        assert!(err.contains("bdk"));
    }

    #[cfg(all(feature = "fakewallet", feature = "cln"))]
    #[test]
    fn test_real_ln_with_fakewallet_onchain_rejected() {
        let settings = Settings {
            ln: vec![Ln {
                ln_backend: LnBackend::Cln,
                ..Default::default()
            }],
            onchain: Some(Onchain {
                onchain_backend: OnchainBackend::FakeWallet,
                ..Default::default()
            }),
            ..Default::default()
        };

        let err = settings
            .validate_backend_pairing()
            .expect_err("real LN with fake onchain should fail");

        assert!(err.contains("fakewallet"));
        assert!(err.contains("real Lightning"));
    }

    #[cfg(feature = "fakewallet")]
    #[test]
    fn test_fakewallet_ln_with_fakewallet_onchain_accepted() {
        let settings = Settings {
            ln: vec![Ln {
                ln_backend: LnBackend::FakeWallet,
                ..Default::default()
            }],
            onchain: Some(Onchain {
                onchain_backend: OnchainBackend::FakeWallet,
                ..Default::default()
            }),
            ..Default::default()
        };

        settings
            .validate_backend_pairing()
            .expect("fake-only backend pairing should pass");
    }

    #[cfg(feature = "fakewallet")]
    #[test]
    fn test_fakewallet_ln_with_no_onchain_accepted() {
        let settings = Settings {
            ln: vec![Ln {
                ln_backend: LnBackend::FakeWallet,
                ..Default::default()
            }],
            onchain: Some(Onchain {
                onchain_backend: OnchainBackend::None,
                ..Default::default()
            }),
            ..Default::default()
        };

        settings
            .validate_backend_pairing()
            .expect("fake LN without onchain should pass");
    }

    #[cfg(feature = "fakewallet")]
    #[test]
    fn test_no_ln_with_fakewallet_onchain_accepted() {
        let settings = Settings {
            ln: vec![Ln {
                ln_backend: LnBackend::None,
                ..Default::default()
            }],
            onchain: Some(Onchain {
                onchain_backend: OnchainBackend::FakeWallet,
                ..Default::default()
            }),
            ..Default::default()
        };

        settings
            .validate_backend_pairing()
            .expect("fake onchain-only backend pairing should pass");
    }

    #[cfg(all(feature = "fakewallet", feature = "cln"))]
    #[test]
    fn test_fakewallet_ln_with_real_ln_rejected() {
        let settings = Settings {
            ln: vec![
                Ln {
                    ln_backend: LnBackend::FakeWallet,
                    ..Default::default()
                },
                Ln {
                    ln_backend: LnBackend::Cln,
                    ..Default::default()
                },
            ],
            ..Default::default()
        };

        let err = settings
            .validate_backend_pairing()
            .expect_err("fake LN combined with real LN should fail");

        assert!(err.contains("fakewallet"));
        assert!(err.contains("real Lightning"));
    }

    #[cfg(feature = "fakewallet")]
    #[test]
    fn test_fakewallet_custom_payment_method_unit_matching() {
        let global = FakeWalletCustomPaymentMethod::Method("paypal".to_string());
        let usd_only = FakeWalletCustomPaymentMethod::MethodForUnit {
            method: "venmo".to_string(),
            unit: CurrencyUnit::Usd,
        };

        assert!(global.applies_to_unit(&CurrencyUnit::Sat));
        assert!(global.applies_to_unit(&CurrencyUnit::Usd));
        assert!(!usd_only.applies_to_unit(&CurrencyUnit::Sat));
        assert!(usd_only.applies_to_unit(&CurrencyUnit::Usd));
    }

    #[test]
    fn test_info_debug_with_special_chars() {
        // Test with a mnemonic containing special characters
        let info = Info {
            url: "http://example.com".to_string(),
            listen_host: "127.0.0.1".to_string(),
            listen_port: 8080,
            mnemonic: Some("特殊字符 !@#$%^&*()".to_string()), // Special characters
            ..Default::default()
        };

        // This should not panic
        let debug_output = format!("{:?}", info);

        // The mnemonic with special chars should be hashed
        assert!(!debug_output.contains("特殊字符 !@#$%^&*()"));
        assert!(debug_output.contains("<hashed: "));
    }

    #[cfg(feature = "fakewallet")]
    #[test]
    fn test_multi_backend_config_parses() {
        use std::{env, fs};

        let temp_dir = env::temp_dir().join("cdk_test_multi_backend_config");
        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
        let config_path = temp_dir.join("config.toml");

        let config_content = r#"
[[ln]]
ln_backend = "fakewallet"
unit = "sat"
min_mint = 1
max_mint = 500000
min_melt = 1
max_melt = 500000

[[ln]]
ln_backend = "fakewallet"
unit = "eur"
min_mint = 1
max_mint = 1000
min_melt = 1
max_melt = 1000
"#;
        fs::write(&config_path, config_content).expect("Failed to write config file");

        let settings = Settings::new(Some(&config_path));

        assert_eq!(settings.ln.len(), 2);

        assert_eq!(settings.ln[0].ln_backend, LnBackend::FakeWallet);
        assert_eq!(settings.ln[0].unit, CurrencyUnit::Sat);
        let max_mint_0: u64 = settings.ln[0].max_mint.into();
        assert_eq!(max_mint_0, 500_000);

        assert_eq!(settings.ln[1].ln_backend, LnBackend::FakeWallet);
        assert_eq!(settings.ln[1].unit, CurrencyUnit::Eur);
        let max_mint_1: u64 = settings.ln[1].max_mint.into();
        assert_eq!(max_mint_1, 1_000);

        let _ = fs::remove_dir_all(&temp_dir);
    }

    #[cfg(feature = "fakewallet")]
    #[test]
    fn test_legacy_ln_block_parses() {
        use std::{env, fs};

        let temp_dir = env::temp_dir().join("cdk_test_legacy_ln_block");
        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
        let config_path = temp_dir.join("config.toml");

        let config_content = r#"
[ln]
ln_backend = "fakewallet"
min_mint = 1
max_mint = 500000
min_melt = 1
max_melt = 500000
"#;
        fs::write(&config_path, config_content).expect("Failed to write config file");

        let settings = Settings::new(Some(&config_path));

        assert_eq!(settings.ln.len(), 1);
        assert_eq!(settings.ln[0].ln_backend, LnBackend::FakeWallet);
        assert_eq!(settings.ln[0].unit, CurrencyUnit::Sat);

        let _ = fs::remove_dir_all(&temp_dir);
    }

    #[cfg(feature = "fakewallet")]
    #[test]
    fn test_fakewallet_config_without_supported_units_parses() {
        use std::{env, fs};

        let temp_dir = env::temp_dir().join("cdk_test_fakewallet_without_supported_units");
        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
        let config_path = temp_dir.join("config.toml");

        let config_content = r#"
[[ln]]
ln_backend = "fakewallet"
unit = "sat"
min_mint = 100
max_mint = 1000000
min_melt = 100
max_melt = 1000000

[fake_wallet]
fee_percent = 0.02
reserve_fee_min = 1
min_delay_time = 1
max_delay_time = 3
"#;
        fs::write(&config_path, config_content).expect("Failed to write config file");

        let settings = Settings::try_new(Some(&config_path)).expect("config should parse");

        assert_eq!(settings.ln.len(), 1);
        assert_eq!(settings.ln[0].unit, CurrencyUnit::Sat);
        assert_eq!(
            settings
                .fake_wallet
                .expect("fake wallet section should parse")
                .supported_units,
            vec![CurrencyUnit::Sat]
        );

        let _ = fs::remove_dir_all(&temp_dir);
    }

    /// Test that configuration can be loaded purely from environment variables
    /// without requiring a config.toml file with backend sections.
    ///
    /// This test runs sequentially for all enabled backends to avoid env var interference.
    #[test]
    fn test_env_var_only_config_all_backends() {
        let _guard = config_env_lock();

        // Run each backend test sequentially
        #[cfg(feature = "lnd")]
        test_lnd_env_config();

        #[cfg(feature = "cln")]
        test_cln_env_config();

        #[cfg(feature = "lnbits")]
        test_lnbits_env_config();

        #[cfg(feature = "fakewallet")]
        test_fakewallet_env_config();

        #[cfg(feature = "grpc-processor")]
        test_grpc_processor_env_config();

        #[cfg(feature = "ldk-node")]
        test_ldk_node_env_config();
    }

    #[cfg(all(feature = "prometheus", feature = "fakewallet"))]
    #[test]
    fn test_prometheus_toml_config_survives_env_overlay() {
        use std::{env, fs};

        let _guard = config_env_lock();

        env::remove_var(crate::env_vars::ENV_LN_BACKEND);
        env::remove_var(crate::env_vars::ENV_PROMETHEUS_ENABLED);
        env::remove_var(crate::env_vars::ENV_PROMETHEUS_ADDRESS);
        env::remove_var(crate::env_vars::ENV_PROMETHEUS_PORT);

        let temp_dir =
            env::temp_dir().join(format!("cdk_prometheus_config_{}", std::process::id()));
        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
        let config_path = temp_dir.join("config.toml");

        let config_content = r#"
[info]
url = "http://127.0.0.1:8085"
listen_host = "127.0.0.1"
listen_port = 8085

[ln]
ln_backend = "fakewallet"
min_mint = 1
max_mint = 500000
min_melt = 1
max_melt = 500000

[prometheus]
enabled = true
address = "0.0.0.0"
port = 9090
"#;
        fs::write(&config_path, config_content).expect("Failed to write config file");

        let mut settings = Settings::new(Some(&config_path));
        settings.from_env().expect("Failed to apply env vars");

        let prometheus = settings
            .prometheus
            .as_ref()
            .expect("Prometheus config should be loaded from TOML");
        assert!(prometheus.enabled);
        assert_eq!(prometheus.address.as_deref(), Some("0.0.0.0"));
        assert_eq!(prometheus.port, Some(9090));

        let _ = fs::remove_dir_all(&temp_dir);
    }

    #[cfg(feature = "lnd")]
    fn test_lnd_env_config() {
        use std::path::PathBuf;
        use std::{env, fs};

        // Create a temporary directory for config file
        let temp_dir = env::temp_dir().join("cdk_test_env_vars");
        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
        let config_path = temp_dir.join("config.toml");

        // Create a minimal config.toml with backend set but NO [lnd] section
        let config_content = r#"
[ln]
backend = "lnd"
min_mint = 1
max_mint = 500000
min_melt = 1
max_melt = 500000
"#;
        fs::write(&config_path, config_content).expect("Failed to write config file");

        // Set environment variables for LND configuration
        env::set_var(crate::env_vars::ENV_LN_BACKEND, "lnd");
        env::set_var(crate::env_vars::ENV_LND_ADDRESS, "https://localhost:10009");
        env::set_var(crate::env_vars::ENV_LND_CERT_FILE, "/tmp/test_tls.cert");
        env::set_var(
            crate::env_vars::ENV_LND_MACAROON_FILE,
            "/tmp/test_admin.macaroon",
        );
        env::set_var(crate::env_vars::ENV_LND_FEE_PERCENT, "0.01");
        env::set_var(crate::env_vars::ENV_LND_RESERVE_FEE_MIN, "4");

        // Load settings and apply environment variables (same as production code)
        let mut settings = Settings::new(Some(&config_path));
        settings.from_env().expect("Failed to apply env vars");

        // Verify that settings were populated from env vars
        assert!(settings.lnd.is_some());
        let lnd_config = settings.lnd.as_ref().unwrap();
        assert_eq!(lnd_config.address, "https://localhost:10009");
        assert_eq!(lnd_config.cert_file, PathBuf::from("/tmp/test_tls.cert"));
        assert_eq!(
            lnd_config.macaroon_file,
            PathBuf::from("/tmp/test_admin.macaroon")
        );
        assert_eq!(lnd_config.fee_percent, 0.01);
        let reserve_fee_u64: u64 = lnd_config.reserve_fee_min.into();
        assert_eq!(reserve_fee_u64, 4);

        // Cleanup env vars
        env::remove_var(crate::env_vars::ENV_LN_BACKEND);
        env::remove_var(crate::env_vars::ENV_LND_ADDRESS);
        env::remove_var(crate::env_vars::ENV_LND_CERT_FILE);
        env::remove_var(crate::env_vars::ENV_LND_MACAROON_FILE);
        env::remove_var(crate::env_vars::ENV_LND_FEE_PERCENT);
        env::remove_var(crate::env_vars::ENV_LND_RESERVE_FEE_MIN);

        // Cleanup test file
        let _ = fs::remove_dir_all(&temp_dir);
    }

    #[cfg(feature = "cln")]
    fn test_cln_env_config() {
        use std::path::PathBuf;
        use std::{env, fs};

        // Create a temporary directory for config file
        let temp_dir = env::temp_dir().join("cdk_test_env_vars_cln");
        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
        let config_path = temp_dir.join("config.toml");

        // Create a minimal config.toml with backend set but NO [cln] section
        let config_content = r#"
[ln]
backend = "cln"
min_mint = 1
max_mint = 500000
min_melt = 1
max_melt = 500000
"#;
        fs::write(&config_path, config_content).expect("Failed to write config file");

        // Set environment variables for CLN configuration
        env::set_var(crate::env_vars::ENV_LN_BACKEND, "cln");
        env::set_var(crate::env_vars::ENV_CLN_RPC_PATH, "/tmp/lightning-rpc");
        env::set_var(crate::env_vars::ENV_CLN_BOLT12, "false");
        env::set_var(crate::env_vars::ENV_CLN_FEE_PERCENT, "0.01");
        env::set_var(crate::env_vars::ENV_CLN_RESERVE_FEE_MIN, "4");

        // Load settings and apply environment variables (same as production code)
        let mut settings = Settings::new(Some(&config_path));
        settings.from_env().expect("Failed to apply env vars");

        // Verify that settings were populated from env vars
        assert!(settings.cln.is_some());
        let cln_config = settings.cln.as_ref().unwrap();
        assert_eq!(cln_config.rpc_path, PathBuf::from("/tmp/lightning-rpc"));
        assert!(!cln_config.bolt12);
        assert_eq!(cln_config.fee_percent, 0.01);
        let reserve_fee_u64: u64 = cln_config.reserve_fee_min.into();
        assert_eq!(reserve_fee_u64, 4);

        // Cleanup env vars
        env::remove_var(crate::env_vars::ENV_LN_BACKEND);
        env::remove_var(crate::env_vars::ENV_CLN_RPC_PATH);
        env::remove_var(crate::env_vars::ENV_CLN_BOLT12);
        env::remove_var(crate::env_vars::ENV_CLN_FEE_PERCENT);
        env::remove_var(crate::env_vars::ENV_CLN_RESERVE_FEE_MIN);

        // Cleanup test file
        let _ = fs::remove_dir_all(&temp_dir);
    }

    #[cfg(feature = "lnbits")]
    fn test_lnbits_env_config() {
        use std::{env, fs};

        // Create a temporary directory for config file
        let temp_dir = env::temp_dir().join("cdk_test_env_vars_lnbits");
        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
        let config_path = temp_dir.join("config.toml");

        // Create a minimal config.toml with backend set but NO [lnbits] section
        let config_content = r#"
[ln]
backend = "lnbits"
min_mint = 1
max_mint = 500000
min_melt = 1
max_melt = 500000
"#;
        fs::write(&config_path, config_content).expect("Failed to write config file");

        // Set environment variables for LNbits configuration
        env::set_var(crate::env_vars::ENV_LN_BACKEND, "lnbits");
        env::set_var(crate::env_vars::ENV_LNBITS_ADMIN_API_KEY, "test_admin_key");
        env::set_var(
            crate::env_vars::ENV_LNBITS_INVOICE_API_KEY,
            "test_invoice_key",
        );
        env::set_var(
            crate::env_vars::ENV_LNBITS_API,
            "https://lnbits.example.com",
        );
        env::set_var(crate::env_vars::ENV_LNBITS_FEE_PERCENT, "0.02");
        env::set_var(crate::env_vars::ENV_LNBITS_RESERVE_FEE_MIN, "5");

        // Load settings and apply environment variables (same as production code)
        let mut settings = Settings::new(Some(&config_path));
        settings.from_env().expect("Failed to apply env vars");

        // Verify that settings were populated from env vars
        assert!(settings.lnbits.is_some());
        let lnbits_config = settings.lnbits.as_ref().unwrap();
        assert_eq!(lnbits_config.admin_api_key, "test_admin_key");
        assert_eq!(lnbits_config.invoice_api_key, "test_invoice_key");
        assert_eq!(lnbits_config.lnbits_api, "https://lnbits.example.com");
        assert_eq!(lnbits_config.fee_percent, 0.02);
        let reserve_fee_u64: u64 = lnbits_config.reserve_fee_min.into();
        assert_eq!(reserve_fee_u64, 5);

        // Cleanup env vars
        env::remove_var(crate::env_vars::ENV_LN_BACKEND);
        env::remove_var(crate::env_vars::ENV_LNBITS_ADMIN_API_KEY);
        env::remove_var(crate::env_vars::ENV_LNBITS_INVOICE_API_KEY);
        env::remove_var(crate::env_vars::ENV_LNBITS_API);
        env::remove_var(crate::env_vars::ENV_LNBITS_FEE_PERCENT);
        env::remove_var(crate::env_vars::ENV_LNBITS_RESERVE_FEE_MIN);

        // Cleanup test file
        let _ = fs::remove_dir_all(&temp_dir);
    }

    #[cfg(feature = "fakewallet")]
    fn test_fakewallet_env_config() {
        use std::{env, fs};

        // Create a temporary directory for config file
        let temp_dir = env::temp_dir().join("cdk_test_env_vars_fakewallet");
        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
        let config_path = temp_dir.join("config.toml");

        // Create a minimal config.toml with backend set but NO [fake_wallet] section
        let config_content = r#"
[ln]
backend = "fakewallet"
min_mint = 1
max_mint = 500000
min_melt = 1
max_melt = 500000
"#;
        fs::write(&config_path, config_content).expect("Failed to write config file");

        // Set environment variables for FakeWallet configuration
        env::set_var(crate::env_vars::ENV_LN_BACKEND, "fakewallet");
        env::set_var(crate::env_vars::ENV_FAKE_WALLET_SUPPORTED_UNITS, "sat,msat");
        env::set_var(crate::env_vars::ENV_FAKE_WALLET_FEE_PERCENT, "0.0");
        env::set_var(crate::env_vars::ENV_FAKE_WALLET_RESERVE_FEE_MIN, "0");
        env::set_var(
            crate::env_vars::ENV_FAKE_WALLET_CUSTOM_PAYMENT_METHODS,
            "venmo:msat,cashapp:sat,paypal",
        );
        env::set_var(crate::env_vars::ENV_FAKE_WALLET_MIN_DELAY, "0");
        env::set_var(crate::env_vars::ENV_FAKE_WALLET_MAX_DELAY, "5");

        // Load settings and apply environment variables (same as production code)
        let mut settings = Settings::new(Some(&config_path));
        settings.from_env().expect("Failed to apply env vars");

        // Verify that settings were populated from env vars
        assert!(settings.fake_wallet.is_some());
        let fakewallet_config = settings.fake_wallet.as_ref().unwrap();
        assert_eq!(fakewallet_config.fee_percent, 0.0);
        let reserve_fee_u64: u64 = fakewallet_config.reserve_fee_min.into();
        assert_eq!(reserve_fee_u64, 0);
        assert_eq!(
            fakewallet_config.custom_payment_methods,
            vec![
                FakeWalletCustomPaymentMethod::MethodForUnit {
                    method: "venmo".to_string(),
                    unit: CurrencyUnit::Msat,
                },
                FakeWalletCustomPaymentMethod::MethodForUnit {
                    method: "cashapp".to_string(),
                    unit: CurrencyUnit::Sat,
                },
                FakeWalletCustomPaymentMethod::Method("paypal".to_string()),
            ]
        );
        assert_eq!(fakewallet_config.min_delay_time, 0);
        assert_eq!(fakewallet_config.max_delay_time, 5);
        assert_eq!(
            settings
                .ln
                .iter()
                .map(|ln| ln.unit.clone())
                .collect::<Vec<_>>(),
            vec![CurrencyUnit::Sat, CurrencyUnit::Msat]
        );

        // Cleanup env vars
        env::remove_var(crate::env_vars::ENV_LN_BACKEND);
        env::remove_var(crate::env_vars::ENV_FAKE_WALLET_SUPPORTED_UNITS);
        env::remove_var(crate::env_vars::ENV_FAKE_WALLET_FEE_PERCENT);
        env::remove_var(crate::env_vars::ENV_FAKE_WALLET_RESERVE_FEE_MIN);
        env::remove_var(crate::env_vars::ENV_FAKE_WALLET_CUSTOM_PAYMENT_METHODS);
        env::remove_var(crate::env_vars::ENV_FAKE_WALLET_MIN_DELAY);
        env::remove_var(crate::env_vars::ENV_FAKE_WALLET_MAX_DELAY);

        // Cleanup test file
        let _ = fs::remove_dir_all(&temp_dir);
    }

    #[cfg(feature = "grpc-processor")]
    fn test_grpc_processor_env_config() {
        use std::{env, fs};

        // Create a temporary directory for config file
        let temp_dir = env::temp_dir().join("cdk_test_env_vars_grpc");
        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
        let config_path = temp_dir.join("config.toml");

        // Create a minimal config.toml with backend set but NO [grpc_processor] section
        let config_content = r#"
[ln]
backend = "grpcprocessor"
min_mint = 1
max_mint = 500000
min_melt = 1
max_melt = 500000
"#;
        fs::write(&config_path, config_content).expect("Failed to write config file");

        // Set environment variables for GRPC Processor configuration
        env::set_var(crate::env_vars::ENV_LN_BACKEND, "grpcprocessor");
        env::set_var(
            crate::env_vars::ENV_GRPC_PROCESSOR_SUPPORTED_UNITS,
            "sat,msat",
        );
        env::set_var(crate::env_vars::ENV_GRPC_PROCESSOR_ADDRESS, "localhost");
        env::set_var(crate::env_vars::ENV_GRPC_PROCESSOR_PORT, "50051");

        // Load settings and apply environment variables (same as production code)
        let mut settings = Settings::new(Some(&config_path));
        settings.from_env().expect("Failed to apply env vars");

        // Verify that settings were populated from env vars
        assert!(settings.grpc_processor.is_some());
        let grpc_config = settings.grpc_processor.as_ref().unwrap();
        assert_eq!(grpc_config.addr, "localhost");
        assert_eq!(grpc_config.port, 50051);

        // Cleanup env vars
        env::remove_var(crate::env_vars::ENV_LN_BACKEND);
        env::remove_var(crate::env_vars::ENV_GRPC_PROCESSOR_SUPPORTED_UNITS);
        env::remove_var(crate::env_vars::ENV_GRPC_PROCESSOR_ADDRESS);
        env::remove_var(crate::env_vars::ENV_GRPC_PROCESSOR_PORT);

        // Cleanup test file
        let _ = fs::remove_dir_all(&temp_dir);
    }

    #[cfg(feature = "ldk-node")]
    fn test_ldk_node_env_config() {
        use std::{env, fs};

        // Create a temporary directory for config file
        let temp_dir = env::temp_dir().join("cdk_test_env_vars_ldk");
        fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
        let config_path = temp_dir.join("config.toml");

        // Create a minimal config.toml with backend set but NO [ldk_node] section
        let config_content = r#"
[ln]
backend = "ldknode"
min_mint = 1
max_mint = 500000
min_melt = 1
max_melt = 500000
"#;
        fs::write(&config_path, config_content).expect("Failed to write config file");

        // Set environment variables for LDK Node configuration
        env::set_var(crate::env_vars::ENV_LN_BACKEND, "ldknode");
        env::set_var(crate::env_vars::LDK_NODE_FEE_PERCENT_ENV_VAR, "0.01");
        env::set_var(crate::env_vars::LDK_NODE_RESERVE_FEE_MIN_ENV_VAR, "4");
        env::set_var(crate::env_vars::LDK_NODE_BITCOIN_NETWORK_ENV_VAR, "regtest");
        env::set_var(
            crate::env_vars::LDK_NODE_CHAIN_SOURCE_TYPE_ENV_VAR,
            "esplora",
        );
        env::set_var(
            crate::env_vars::LDK_NODE_ESPLORA_URL_ENV_VAR,
            "http://localhost:3000",
        );
        env::set_var(
            crate::env_vars::LDK_NODE_STORAGE_DIR_PATH_ENV_VAR,
            "/tmp/ldk",
        );

        // Load settings and apply environment variables (same as production code)
        let mut settings = Settings::new(Some(&config_path));
        settings.from_env().expect("Failed to apply env vars");

        // Verify that settings were populated from env vars
        assert!(settings.ldk_node.is_some());
        let ldk_config = settings.ldk_node.as_ref().unwrap();
        assert_eq!(ldk_config.fee_percent, 0.01);
        let reserve_fee_u64: u64 = ldk_config.reserve_fee_min.into();
        assert_eq!(reserve_fee_u64, 4);
        assert_eq!(ldk_config.bitcoin_network, Some("regtest".to_string()));
        assert_eq!(ldk_config.chain_source_type, Some("esplora".to_string()));
        assert_eq!(
            ldk_config.esplora_url,
            Some("http://localhost:3000".to_string())
        );
        assert_eq!(ldk_config.storage_dir_path, Some("/tmp/ldk".to_string()));

        // Cleanup env vars
        env::remove_var(crate::env_vars::ENV_LN_BACKEND);
        env::remove_var(crate::env_vars::LDK_NODE_FEE_PERCENT_ENV_VAR);
        env::remove_var(crate::env_vars::LDK_NODE_RESERVE_FEE_MIN_ENV_VAR);
        env::remove_var(crate::env_vars::LDK_NODE_BITCOIN_NETWORK_ENV_VAR);
        env::remove_var(crate::env_vars::LDK_NODE_CHAIN_SOURCE_TYPE_ENV_VAR);
        env::remove_var(crate::env_vars::LDK_NODE_ESPLORA_URL_ENV_VAR);
        env::remove_var(crate::env_vars::LDK_NODE_STORAGE_DIR_PATH_ENV_VAR);

        // Cleanup test file
        let _ = fs::remove_dir_all(&temp_dir);
    }
}
