//! CDK Fake LN Backend
//!
//! Used for testing where quotes are auto filled.
//!
//! The fake wallet now includes a secondary repayment system that continuously repays any-amount
//! invoices (amount = 0) at random intervals between 30 seconds and 3 minutes to simulate
//! real-world behavior where invoices might get multiple payments. Payments continue to be
//! processed until they are evicted from the queue when the queue reaches its maximum size
//! (default 100 items). This is in addition to the original immediate payment processing
//! which is maintained for all invoice types.

#![doc = include_str!("../README.md")]

use std::cmp::max;
use std::collections::{HashMap, HashSet, VecDeque};
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};

use async_trait::async_trait;
use bitcoin::hashes::{sha256, Hash};
use bitcoin::secp256k1::{Secp256k1, SecretKey};
use bitcoin::{Address, CompressedPublicKey, Network};
use cdk_common::amount::Amount;
use cdk_common::common::FeeReserve;
use cdk_common::ensure_cdk;
use cdk_common::nuts::nut30::MeltQuoteOnchainFeeOption;
use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
use cdk_common::payment::{
    self, CreateIncomingPaymentResponse, Event, IncomingPaymentOptions, MakePaymentResponse,
    MintPayment, OutgoingPaymentOptions, PaymentIdentifier, PaymentQuoteResponse, SettingsResponse,
    WaitPaymentResponse,
};
use error::Error;
use futures::stream::StreamExt;
use futures::Stream;
use lightning::offers::offer::OfferBuilder;
use lightning_invoice::{Bolt11Invoice, Currency, InvoiceBuilder, PaymentSecret};
use serde::{Deserialize, Serialize};
use tokio::sync::{Mutex, RwLock};
use tokio::time;
use tokio_stream::wrappers::ReceiverStream;
use tokio_util::sync::CancellationToken;
use tracing::instrument;
use uuid::Uuid;

pub mod error;

/// Default maximum size for the secondary repayment queue
const DEFAULT_REPAY_QUEUE_MAX_SIZE: usize = 100;
/// Default amount used for custom outgoing payments in tests
const DEFAULT_CUSTOM_OUTGOING_AMOUNT: u64 = 1000;
/// Default amount reported for fake onchain deposits.
const DEFAULT_ONCHAIN_INCOMING_AMOUNT: u64 = 1000;
/// Default confirmation target for fake onchain sends.
const DEFAULT_ONCHAIN_ESTIMATED_BLOCKS: u32 = 6;

/// Payment state entry containing the melt quote state and amount spent
type PaymentStateEntry = (MeltQuoteState, Amount<CurrencyUnit>);

/// Cache duration for exchange rate (5 minutes)
const RATE_CACHE_DURATION: Duration = Duration::from_secs(300);

/// Mempool.space prices API response structure
#[derive(Debug, Deserialize)]
struct MempoolPricesResponse {
    #[serde(rename = "USD")]
    usd: f64,
    #[serde(rename = "EUR")]
    eur: f64,
}

/// Exchange rate cache with built-in fallback rates
#[derive(Debug, Clone)]
struct ExchangeRateCache {
    rates: Arc<Mutex<Option<(MempoolPricesResponse, Instant)>>>,
}

impl ExchangeRateCache {
    fn new() -> Self {
        Self {
            rates: Arc::new(Mutex::new(None)),
        }
    }

    /// Get current BTC rate for the specified currency with caching and fallback
    async fn get_btc_rate(&self, currency: &CurrencyUnit) -> Result<f64, Error> {
        // Return cached rate if still valid
        {
            let cached_rates = self.rates.lock().await;
            if let Some((rates, timestamp)) = &*cached_rates {
                if timestamp.elapsed() < RATE_CACHE_DURATION {
                    return Self::rate_for_currency(rates, currency);
                }
            }
        }

        // Try to fetch fresh rates, fallback on error
        match self.fetch_fresh_rate(currency).await {
            Ok(rate) => Ok(rate),
            Err(e) => {
                tracing::warn!(
                    "Failed to fetch exchange rates, using fallback for {:?}: {}",
                    currency,
                    e
                );
                Self::fallback_rate(currency)
            }
        }
    }

    /// Fetch fresh rate and update cache
    async fn fetch_fresh_rate(&self, currency: &CurrencyUnit) -> Result<f64, Error> {
        let url = "https://mempool.space/api/v1/prices";
        let response: MempoolPricesResponse = cdk_common::fetch(url)
            .await
            .map_err(|_| Error::UnknownInvoiceAmount)?;

        let rate = Self::rate_for_currency(&response, currency)?;
        *self.rates.lock().await = Some((response, Instant::now()));
        Ok(rate)
    }

    fn rate_for_currency(
        rates: &MempoolPricesResponse,
        currency: &CurrencyUnit,
    ) -> Result<f64, Error> {
        match currency {
            CurrencyUnit::Usd => Ok(rates.usd),
            CurrencyUnit::Eur => Ok(rates.eur),
            _ => Err(Error::UnknownInvoiceAmount),
        }
    }

    fn fallback_rate(currency: &CurrencyUnit) -> Result<f64, Error> {
        match currency {
            CurrencyUnit::Usd => Ok(110_000.0), // $110k per BTC
            CurrencyUnit::Eur => Ok(95_000.0),  // €95k per BTC
            _ => Err(Error::UnknownInvoiceAmount),
        }
    }
}

async fn convert_currency_amount(
    amount: u64,
    from_unit: &CurrencyUnit,
    target_unit: &CurrencyUnit,
    rate_cache: &ExchangeRateCache,
) -> Result<Amount<CurrencyUnit>, Error> {
    use CurrencyUnit::*;

    // Try basic unit conversion first (handles SAT/MSAT and same-unit conversions)
    if let Ok(converted) = Amount::new(amount, from_unit.clone()).convert_to(target_unit) {
        return Ok(converted);
    }

    // Handle fiat <-> bitcoin conversions that require exchange rates
    match (from_unit, target_unit) {
        // Fiat to Bitcoin conversions
        (Usd | Eur, Sat) => {
            let rate = rate_cache.get_btc_rate(from_unit).await?;
            let fiat_amount = amount as f64 / 100.0; // cents to dollars/euros
            Ok(Amount::new(
                (fiat_amount / rate * 100_000_000.0).round() as u64,
                target_unit.clone(),
            )) // to sats
        }
        (Usd | Eur, Msat) => {
            let rate = rate_cache.get_btc_rate(from_unit).await?;
            let fiat_amount = amount as f64 / 100.0; // cents to dollars/euros
            Ok(Amount::new(
                (fiat_amount / rate * 100_000_000_000.0).round() as u64,
                target_unit.clone(),
            )) // to msats
        }

        // Bitcoin to fiat conversions
        (Sat, Usd | Eur) => {
            let rate = rate_cache.get_btc_rate(target_unit).await?;
            let btc_amount = amount as f64 / 100_000_000.0; // sats to BTC
            Ok(Amount::new(
                (btc_amount * rate * 100.0).round() as u64,
                target_unit.clone(),
            )) // to cents
        }
        (Msat, Usd | Eur) => {
            let rate = rate_cache.get_btc_rate(target_unit).await?;
            let btc_amount = amount as f64 / 100_000_000_000.0; // msats to BTC
            Ok(Amount::new(
                (btc_amount * rate * 100.0).round() as u64,
                target_unit.clone(),
            )) // to cents
        }

        _ => Err(Error::UnknownInvoiceAmount), // Unsupported conversion
    }
}

/// Secondary repayment queue manager for any-amount invoices
#[derive(Debug, Clone)]
struct SecondaryRepaymentQueue {
    queue: Arc<Mutex<VecDeque<PaymentIdentifier>>>,
    max_size: usize,
    sender: tokio::sync::mpsc::Sender<WaitPaymentResponse>,
    unit: CurrencyUnit,
}

impl SecondaryRepaymentQueue {
    fn new(
        max_size: usize,
        sender: tokio::sync::mpsc::Sender<WaitPaymentResponse>,
        unit: CurrencyUnit,
    ) -> Self {
        let queue = Arc::new(Mutex::new(VecDeque::new()));
        let repayment_queue = Self {
            queue: queue.clone(),
            max_size,
            sender,
            unit,
        };

        // Start the background secondary repayment processor
        repayment_queue.start_secondary_repayment_processor();

        repayment_queue
    }

    /// Add a payment to the secondary repayment queue
    async fn enqueue_for_repayment(&self, payment: PaymentIdentifier) {
        let mut queue = self.queue.lock().await;

        // If queue is at max capacity, remove the oldest item
        if queue.len() >= self.max_size {
            if let Some(dropped) = queue.pop_front() {
                tracing::debug!(
                    "Secondary repayment queue at capacity, dropping oldest payment: {:?}",
                    dropped
                );
            }
        }

        queue.push_back(payment);
        tracing::debug!(
            "Added payment to secondary repayment queue, current size: {}",
            queue.len()
        );
    }

    /// Start the background task that randomly processes secondary repayments from the queue
    fn start_secondary_repayment_processor(&self) {
        let queue = self.queue.clone();
        let sender = self.sender.clone();
        let unit = self.unit.clone();

        tokio::spawn(async move {
            use bitcoin::secp256k1::rand::rngs::OsRng;
            use bitcoin::secp256k1::rand::Rng;
            let mut rng = OsRng;

            loop {
                // Wait for a random interval between 30 seconds and 3 minutes (180 seconds)
                let delay_secs = rng.gen_range(1..=3);
                time::sleep(time::Duration::from_secs(delay_secs)).await;

                // Try to process a random payment from the queue without removing it
                let payment_to_process = {
                    let q = queue.lock().await;
                    if q.is_empty() {
                        None
                    } else {
                        // Pick a random index from the queue but don't remove it
                        let index = rng.gen_range(0..q.len());
                        q.get(index).cloned()
                    }
                };

                if let Some(payment) = payment_to_process {
                    // Generate a random amount for this secondary payment (same range as initial payment: 1-1000)
                    let random_amount: u64 = rng.gen_range(1..=1000);

                    // Create amount based on unit, ensuring minimum of 1 sat worth
                    let secondary_amount = match &unit {
                        CurrencyUnit::Sat => Amount::new(random_amount, unit.clone()),
                        CurrencyUnit::Msat => {
                            Amount::new(u64::max(random_amount * 1000, 1000), unit.clone())
                        }
                        _ => Amount::new(u64::max(random_amount, 1), unit.clone()), // fallback
                    };

                    // Generate a unique payment identifier for this secondary payment
                    // We'll create a new payment hash by appending a timestamp and random bytes
                    use bitcoin::hashes::{sha256, Hash};
                    let mut random_bytes = [0u8; 16];
                    rng.fill(&mut random_bytes);
                    let timestamp = web_time::SystemTime::now()
                        .duration_since(web_time::UNIX_EPOCH)
                        .expect("System time before UNIX_EPOCH")
                        .as_nanos() as u64;

                    // Create a unique hash combining the original payment identifier, timestamp, and random bytes
                    let mut hasher_input = Vec::new();
                    hasher_input.extend_from_slice(payment.to_string().as_bytes());
                    hasher_input.extend_from_slice(&timestamp.to_le_bytes());
                    hasher_input.extend_from_slice(&random_bytes);

                    let unique_hash = sha256::Hash::hash(&hasher_input);
                    let unique_payment_id = PaymentIdentifier::PaymentHash(*unique_hash.as_ref());

                    tracing::info!(
                        "Processing secondary repayment: original={:?}, new_id={:?}, amount={}",
                        payment,
                        unique_payment_id,
                        secondary_amount
                    );

                    // Send the payment notification using the original payment identifier
                    // The mint will process this through the normal payment stream
                    let secondary_response = WaitPaymentResponse {
                        payment_identifier: payment.clone(),
                        payment_amount: secondary_amount,
                        payment_id: unique_payment_id.to_string(),
                    };

                    if let Err(e) = sender.send(secondary_response).await {
                        tracing::error!(
                            "Failed to send secondary repayment notification for {:?}: {}",
                            unique_payment_id,
                            e
                        );
                    }
                }
            }
        });
    }
}

/// Fake Wallet
#[derive(Clone, Debug)]
pub struct FakeWallet {
    fee_reserve: FeeReserve,
    sender: tokio::sync::mpsc::Sender<WaitPaymentResponse>,
    receiver: Arc<Mutex<Option<tokio::sync::mpsc::Receiver<WaitPaymentResponse>>>>,
    payment_states: Arc<Mutex<HashMap<String, PaymentStateEntry>>>,
    failed_payment_check: Arc<Mutex<HashSet<String>>>,
    payment_delay: u64,
    wait_invoice_cancel_token: CancellationToken,
    wait_invoice_is_active: Arc<AtomicBool>,
    incoming_payments: Arc<RwLock<HashMap<PaymentIdentifier, Vec<WaitPaymentResponse>>>>,
    unit: CurrencyUnit,
    secondary_repayment_queue: SecondaryRepaymentQueue,
    exchange_rate_cache: ExchangeRateCache,
    custom_payment_methods: HashMap<String, String>,
}

impl FakeWallet {
    /// Create new [`FakeWallet`]
    pub fn new(
        fee_reserve: FeeReserve,
        payment_states: HashMap<String, PaymentStateEntry>,
        fail_payment_check: HashSet<String>,
        payment_delay: u64,
        unit: CurrencyUnit,
    ) -> Self {
        Self::new_with_repay_queue_size(
            fee_reserve,
            payment_states,
            fail_payment_check,
            payment_delay,
            unit,
            DEFAULT_REPAY_QUEUE_MAX_SIZE,
        )
    }

    /// Create new [`FakeWallet`] with custom secondary repayment queue size
    pub fn new_with_repay_queue_size(
        fee_reserve: FeeReserve,
        payment_states: HashMap<String, PaymentStateEntry>,
        fail_payment_check: HashSet<String>,
        payment_delay: u64,
        unit: CurrencyUnit,
        repay_queue_max_size: usize,
    ) -> Self {
        let (sender, receiver) = tokio::sync::mpsc::channel(8);
        let incoming_payments = Arc::new(RwLock::new(HashMap::new()));

        let secondary_repayment_queue =
            SecondaryRepaymentQueue::new(repay_queue_max_size, sender.clone(), unit.clone());

        Self {
            fee_reserve,
            sender,
            receiver: Arc::new(Mutex::new(Some(receiver))),
            payment_states: Arc::new(Mutex::new(payment_states)),
            failed_payment_check: Arc::new(Mutex::new(fail_payment_check)),
            payment_delay,
            wait_invoice_cancel_token: CancellationToken::new(),
            wait_invoice_is_active: Arc::new(AtomicBool::new(false)),
            incoming_payments,
            unit,
            secondary_repayment_queue,
            exchange_rate_cache: ExchangeRateCache::new(),
            custom_payment_methods: HashMap::new(),
        }
    }

    /// Configure custom payment methods advertised by this fake wallet.
    pub fn with_custom_payment_methods(
        mut self,
        custom_payment_methods: HashMap<String, String>,
    ) -> Self {
        self.custom_payment_methods = custom_payment_methods
            .into_iter()
            .map(|(method, settings)| (method.to_lowercase(), settings))
            .collect();
        self
    }

    fn ensure_custom_method_supported(&self, method: &str) -> Result<(), payment::Error> {
        ensure_cdk!(
            self.custom_payment_methods
                .contains_key(&method.to_lowercase()),
            payment::Error::UnsupportedPaymentOption
        );

        Ok(())
    }

    fn fee_for_amount(&self, amount: &Amount<CurrencyUnit>) -> Amount<CurrencyUnit> {
        let relative_fee_reserve =
            (self.fee_reserve.percent_fee_reserve * amount.value() as f32) as u64;

        let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();

        Amount::new(
            max(relative_fee_reserve, absolute_fee_reserve),
            amount.unit().clone(),
        )
    }

    async fn custom_outgoing_amount(
        &self,
        unit: &CurrencyUnit,
        extra_json: &Option<String>,
    ) -> Result<Amount<CurrencyUnit>, Error> {
        #[derive(Deserialize)]
        struct CustomAmountField {
            amount: Option<u64>,
        }

        let amount = match extra_json {
            Some(extra_json) => serde_json::from_str::<CustomAmountField>(extra_json)
                .ok()
                .and_then(|extra| extra.amount)
                .unwrap_or(DEFAULT_CUSTOM_OUTGOING_AMOUNT),
            None => DEFAULT_CUSTOM_OUTGOING_AMOUNT,
        };

        convert_currency_amount(amount, &self.unit, unit, &self.exchange_rate_cache).await
    }
}

/// Struct for signaling what methods should respond via invoice description
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct FakeInvoiceDescription {
    /// State to be returned from pay invoice state
    pub pay_invoice_state: MeltQuoteState,
    /// State to be returned by check payment state
    pub check_payment_state: MeltQuoteState,
    /// Should pay invoice error
    pub pay_err: bool,
    /// Should check failure
    pub check_err: bool,
}

impl Default for FakeInvoiceDescription {
    fn default() -> Self {
        Self {
            pay_invoice_state: MeltQuoteState::Paid,
            check_payment_state: MeltQuoteState::Paid,
            pay_err: false,
            check_err: false,
        }
    }
}

#[async_trait]
impl MintPayment for FakeWallet {
    type Err = payment::Error;

    #[instrument(skip_all)]
    async fn get_settings(&self) -> Result<SettingsResponse, Self::Err> {
        Ok(SettingsResponse {
            unit: self.unit.to_string(),
            bolt11: Some(payment::Bolt11Settings {
                mpp: true,
                amountless: false,
                invoice_description: true,
            }),
            bolt12: Some(payment::Bolt12Settings { amountless: false }),
            onchain: Some(payment::OnchainSettings {
                confirmations: 1,
                min_receive_amount_sat: 1,
                min_send_amount_sat: 1,
            }),
            custom: self.custom_payment_methods.clone(),
        })
    }

    #[instrument(skip_all)]
    fn is_payment_event_stream_active(&self) -> bool {
        self.wait_invoice_is_active.load(Ordering::SeqCst)
    }

    #[instrument(skip_all)]
    fn cancel_payment_event_stream(&self) {
        self.wait_invoice_cancel_token.cancel()
    }

    #[instrument(skip_all)]
    async fn wait_payment_event(
        &self,
    ) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err> {
        tracing::info!("Starting stream for fake invoices");
        let receiver = self.receiver.lock().await.take().ok_or(Error::NoReceiver)?;
        let receiver_stream = ReceiverStream::new(receiver);
        Ok(Box::pin(receiver_stream.map(move |wait_response| {
            Event::PaymentReceived(wait_response)
        })))
    }

    #[instrument(skip_all)]
    async fn get_payment_quote(
        &self,
        unit: &CurrencyUnit,
        options: OutgoingPaymentOptions,
    ) -> Result<PaymentQuoteResponse, Self::Err> {
        let (amount_msat, request_lookup_id) = match options {
            OutgoingPaymentOptions::Bolt11(bolt11_options) => {
                // If we have specific amount options, use those
                let amount_msat: u64 = if let Some(melt_options) = bolt11_options.melt_options {
                    let msats = match melt_options {
                        MeltOptions::Amountless { amountless } => {
                            let amount_msat = amountless.amount_msat;

                            if let Some(invoice_amount) =
                                bolt11_options.bolt11.amount_milli_satoshis()
                            {
                                ensure_cdk!(
                                    invoice_amount == u64::from(amount_msat),
                                    payment::Error::AmountMismatch
                                );
                            }
                            amount_msat
                        }
                        MeltOptions::Mpp { mpp } => mpp.amount,
                    };

                    u64::from(msats)
                } else {
                    // Fall back to invoice amount
                    bolt11_options
                        .bolt11
                        .amount_milli_satoshis()
                        .ok_or(Error::UnknownInvoiceAmount)?
                };
                let payment_id =
                    PaymentIdentifier::PaymentHash(*bolt11_options.bolt11.payment_hash().as_ref());
                (amount_msat, Some(payment_id))
            }
            OutgoingPaymentOptions::Bolt12(bolt12_options) => {
                let offer = bolt12_options.offer;

                let amount_msat: u64 = if let Some(amount) = bolt12_options.melt_options {
                    amount.amount_msat().into()
                } else {
                    // Fall back to offer amount
                    let amount = offer.amount().ok_or(Error::UnknownInvoiceAmount)?;
                    match amount {
                        lightning::offers::offer::Amount::Bitcoin { amount_msats } => amount_msats,
                        _ => return Err(Error::UnknownInvoiceAmount.into()),
                    }
                };
                (amount_msat, None)
            }
            OutgoingPaymentOptions::Custom(custom_options) => {
                self.ensure_custom_method_supported(&custom_options.method)?;

                let amount = self
                    .custom_outgoing_amount(unit, &custom_options.extra_json)
                    .await?;

                return Ok(PaymentQuoteResponse {
                    request_lookup_id: Some(PaymentIdentifier::CustomId(
                        Uuid::new_v4().to_string(),
                    )),
                    amount: amount.clone(),
                    fee: self.fee_for_amount(&amount),
                    state: MeltQuoteState::Unpaid,
                    extra_json: custom_options
                        .extra_json
                        .as_ref()
                        .and_then(|extra_json| serde_json::from_str(extra_json).ok()),
                    estimated_blocks: None,
                    fee_options: None,
                });
            }
            OutgoingPaymentOptions::Onchain(onchain_options) => {
                let fee = self.fee_for_amount(&onchain_options.amount);

                return Ok(PaymentQuoteResponse {
                    request_lookup_id: Some(PaymentIdentifier::QuoteId(
                        onchain_options.quote_id.clone(),
                    )),
                    amount: onchain_options.amount,
                    fee: fee.clone(),
                    state: MeltQuoteState::Unpaid,
                    extra_json: None,
                    estimated_blocks: Some(DEFAULT_ONCHAIN_ESTIMATED_BLOCKS),
                    fee_options: Some(vec![MeltQuoteOnchainFeeOption {
                        fee_index: 0,
                        fee_reserve: Amount::from(fee.value()),
                        estimated_blocks: DEFAULT_ONCHAIN_ESTIMATED_BLOCKS,
                    }]),
                });
            }
        };

        let amount = convert_currency_amount(
            amount_msat,
            &CurrencyUnit::Msat,
            unit,
            &self.exchange_rate_cache,
        )
        .await?;

        Ok(PaymentQuoteResponse {
            request_lookup_id,
            fee: self.fee_for_amount(&amount),
            amount,
            state: MeltQuoteState::Unpaid,
            extra_json: None,
            estimated_blocks: None,
            fee_options: None,
        })
    }

    #[instrument(skip_all)]
    async fn make_payment(
        &self,
        unit: &CurrencyUnit,
        options: OutgoingPaymentOptions,
    ) -> Result<MakePaymentResponse, Self::Err> {
        match options {
            OutgoingPaymentOptions::Bolt11(bolt11_options) => {
                let bolt11 = bolt11_options.bolt11;
                let payment_hash = bolt11.payment_hash().to_string();

                let description = bolt11.description().to_string();

                let status: Option<FakeInvoiceDescription> =
                    serde_json::from_str(&description).ok();

                let mut payment_states = self.payment_states.lock().await;
                let payment_status = status
                    .clone()
                    .map(|s| s.pay_invoice_state)
                    .unwrap_or(MeltQuoteState::Paid);

                let checkout_going_status = status
                    .clone()
                    .map(|s| s.check_payment_state)
                    .unwrap_or(MeltQuoteState::Paid);

                let amount_msat: u64 = if let Some(melt_options) = bolt11_options.melt_options {
                    let msats = match melt_options {
                        MeltOptions::Amountless { amountless } => {
                            let amount_msat = amountless.amount_msat;

                            if let Some(invoice_amount) = bolt11.amount_milli_satoshis() {
                                ensure_cdk!(
                                    invoice_amount == u64::from(amount_msat),
                                    payment::Error::AmountMismatch
                                );
                            }

                            amount_msat
                        }
                        MeltOptions::Mpp { mpp } => mpp.amount,
                    };

                    u64::from(msats)
                } else {
                    // Fall back to invoice amount
                    bolt11
                        .amount_milli_satoshis()
                        .ok_or(Error::UnknownInvoiceAmount)?
                };

                let amount_spent = if checkout_going_status == MeltQuoteState::Paid {
                    Amount::new(amount_msat, CurrencyUnit::Msat)
                } else {
                    Amount::new(0, CurrencyUnit::Msat)
                };

                payment_states.insert(payment_hash.clone(), (checkout_going_status, amount_spent));

                if let Some(description) = status {
                    if description.check_err {
                        let mut fail = self.failed_payment_check.lock().await;
                        fail.insert(payment_hash.clone());
                    }

                    ensure_cdk!(!description.pay_err, Error::UnknownInvoice.into());
                }

                let total_spent = convert_currency_amount(
                    amount_msat,
                    &CurrencyUnit::Msat,
                    unit,
                    &self.exchange_rate_cache,
                )
                .await?;

                Ok(MakePaymentResponse {
                    payment_lookup_id: PaymentIdentifier::PaymentHash(
                        *bolt11.payment_hash().as_ref(),
                    ),
                    payment_proof: Some("".to_string()),
                    status: payment_status,
                    total_spent: Amount::new(total_spent.value() + 1, unit.clone()),
                })
            }
            OutgoingPaymentOptions::Bolt12(bolt12_options) => {
                let bolt12 = bolt12_options.offer;
                let amount_msat: u64 = if let Some(amount) = bolt12_options.melt_options {
                    amount.amount_msat().into()
                } else {
                    // Fall back to offer amount
                    let amount = bolt12.amount().ok_or(Error::UnknownInvoiceAmount)?;
                    match amount {
                        lightning::offers::offer::Amount::Bitcoin { amount_msats } => amount_msats,
                        _ => return Err(Error::UnknownInvoiceAmount.into()),
                    }
                };

                let total_spent = convert_currency_amount(
                    amount_msat,
                    &CurrencyUnit::Msat,
                    unit,
                    &self.exchange_rate_cache,
                )
                .await?;

                Ok(MakePaymentResponse {
                    payment_lookup_id: PaymentIdentifier::CustomId(Uuid::new_v4().to_string()),
                    payment_proof: Some("".to_string()),
                    status: MeltQuoteState::Paid,
                    total_spent: Amount::new(total_spent.value() + 1, unit.clone()),
                })
            }
            OutgoingPaymentOptions::Custom(custom_options) => {
                self.ensure_custom_method_supported(&custom_options.method)?;

                let total_spent = self
                    .custom_outgoing_amount(unit, &custom_options.extra_json)
                    .await?;

                Ok(MakePaymentResponse {
                    payment_lookup_id: PaymentIdentifier::CustomId(Uuid::new_v4().to_string()),
                    payment_proof: Some(custom_options.request),
                    status: MeltQuoteState::Paid,
                    total_spent: Amount::new(total_spent.value() + 1, unit.clone()),
                })
            }
            OutgoingPaymentOptions::Onchain(onchain_options) => {
                let amount = onchain_options.amount;
                let quote_id = onchain_options.quote_id;
                let fee = self.fee_for_amount(&amount);

                let total_spent = Amount::new(amount.value() + fee.value(), amount.unit().clone());
                let payment_proof = fake_onchain_outpoint(&quote_id.to_string());

                self.payment_states.lock().await.insert(
                    quote_id.to_string(),
                    (MeltQuoteState::Paid, total_spent.clone()),
                );

                Ok(MakePaymentResponse {
                    payment_lookup_id: PaymentIdentifier::QuoteId(quote_id),
                    payment_proof: Some(payment_proof),
                    status: MeltQuoteState::Paid,
                    total_spent,
                })
            }
        }
    }

    #[instrument(skip_all)]
    async fn create_incoming_payment_request(
        &self,
        options: IncomingPaymentOptions,
    ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
        let (payment_hash, request, amount, expiry) = match options {
            IncomingPaymentOptions::Bolt12(bolt12_options) => {
                let description = bolt12_options.description.unwrap_or_default();
                let amount = bolt12_options.amount;
                let expiry = bolt12_options.unix_expiry;

                let secret_key = SecretKey::new(&mut bitcoin::secp256k1::rand::rngs::OsRng);
                let secp_ctx = Secp256k1::new();

                let offer_builder = OfferBuilder::new(secret_key.public_key(&secp_ctx))
                    .description(description.clone());

                let (offer_builder, final_amount) = match amount {
                    Some(ref amt) => {
                        let amount_msat = convert_currency_amount(
                            amt.value(),
                            amt.unit(),
                            &CurrencyUnit::Msat,
                            &self.exchange_rate_cache,
                        )
                        .await?;
                        (offer_builder.amount_msats(amount_msat.value()), amt.clone())
                    }
                    None => (offer_builder, Amount::new(0, CurrencyUnit::Sat)),
                };

                let offer = offer_builder.build().expect("Failed to build BOLT12 offer");

                (
                    PaymentIdentifier::OfferId(offer.id().to_string()),
                    offer.to_string(),
                    final_amount,
                    expiry,
                )
            }
            IncomingPaymentOptions::Bolt11(bolt11_options) => {
                let description = bolt11_options.description.unwrap_or_default();
                let amount = bolt11_options.amount;
                let expiry = bolt11_options.unix_expiry;

                let amount_msat = convert_currency_amount(
                    amount.value(),
                    amount.unit(),
                    &CurrencyUnit::Msat,
                    &self.exchange_rate_cache,
                )
                .await?;

                let invoice = create_fake_invoice(amount_msat.value(), description.clone());
                let payment_hash = invoice.payment_hash();

                (
                    PaymentIdentifier::PaymentHash(*payment_hash.as_ref()),
                    invoice.to_string(),
                    amount,
                    expiry,
                )
            }
            IncomingPaymentOptions::Onchain(onchain_options) => {
                let request = fake_onchain_address(&onchain_options.quote_id.to_string());

                (
                    PaymentIdentifier::QuoteId(onchain_options.quote_id),
                    request,
                    Amount::new(DEFAULT_ONCHAIN_INCOMING_AMOUNT, self.unit.clone()),
                    None,
                )
            }
            IncomingPaymentOptions::Custom(custom_options) => {
                self.ensure_custom_method_supported(&custom_options.method)?;

                let custom_id = Uuid::new_v4().to_string();
                let request = format!("{}:{}", custom_options.method.to_lowercase(), custom_id);

                (
                    PaymentIdentifier::CustomId(custom_id),
                    request,
                    custom_options.amount,
                    custom_options.unix_expiry,
                )
            }
        };

        // ALL invoices get immediate payment processing (original behavior)
        let sender = self.sender.clone();
        let duration = time::Duration::from_secs(self.payment_delay);
        let payment_hash_clone = payment_hash.clone();
        let incoming_payment = self.incoming_payments.clone();

        let final_amount = if amount.value() == 0 {
            // For any-amount invoices, generate a random amount for the initial payment
            use bitcoin::secp256k1::rand::rngs::OsRng;
            use bitcoin::secp256k1::rand::Rng;
            let mut rng = OsRng;
            let random_amount: u64 = rng.gen_range(1000..=10000);
            // Use the same unit as the invoice for any-amount invoices
            Amount::new(random_amount, amount.unit().clone())
        } else {
            amount
        };

        // Check if this is an any-amount invoice before moving final_amount
        let is_any_amount = final_amount.value() == 0;

        // Schedule the immediate payment (original behavior maintained)
        tokio::spawn(async move {
            // Wait for the random delay to elapse
            time::sleep(duration).await;

            let response = WaitPaymentResponse {
                payment_identifier: payment_hash_clone.clone(),
                payment_amount: final_amount,
                payment_id: payment_hash_clone.to_string(),
            };
            let mut incoming = incoming_payment.write().await;
            incoming
                .entry(payment_hash_clone.clone())
                .or_insert_with(Vec::new)
                .push(response.clone());

            // Send the message after waiting for the specified duration
            if sender.send(response.clone()).await.is_err() {
                tracing::error!("Failed to send label: {:?}", payment_hash_clone);
            }
        });

        // For any-amount invoices ONLY, also add to the secondary repayment queue
        if is_any_amount {
            tracing::info!(
                "Adding any-amount invoice to secondary repayment queue: {:?}",
                payment_hash
            );

            self.secondary_repayment_queue
                .enqueue_for_repayment(payment_hash.clone())
                .await;
        }

        Ok(CreateIncomingPaymentResponse {
            request_lookup_id: payment_hash,
            request,
            expiry,
            extra_json: None,
        })
    }

    #[instrument(skip_all)]
    async fn check_incoming_payment_status(
        &self,
        request_lookup_id: &PaymentIdentifier,
    ) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
        Ok(self
            .incoming_payments
            .read()
            .await
            .get(request_lookup_id)
            .cloned()
            .unwrap_or(vec![]))
    }

    #[instrument(skip_all)]
    async fn check_outgoing_payment(
        &self,
        request_lookup_id: &PaymentIdentifier,
    ) -> Result<MakePaymentResponse, Self::Err> {
        // For fake wallet if the state is not explicitly set default to paid
        let states = self.payment_states.lock().await;
        let status = states.get(&request_lookup_id.to_string()).cloned();

        let (status, total_spent) =
            status.unwrap_or((MeltQuoteState::Unknown, Amount::new(0, CurrencyUnit::Msat)));

        let fail_payments = self.failed_payment_check.lock().await;

        if fail_payments.contains(&request_lookup_id.to_string()) {
            return Err(payment::Error::InvoicePaymentPending);
        }

        let payment_proof = match request_lookup_id {
            PaymentIdentifier::QuoteId(quote_id) => {
                Some(fake_onchain_outpoint(&quote_id.to_string()))
            }
            _ => Some("".to_string()),
        };

        Ok(MakePaymentResponse {
            payment_lookup_id: request_lookup_id.clone(),
            payment_proof,
            status,
            total_spent,
        })
    }
}

/// Create fake invoice
///
/// # Panics
///
/// Panics if the hardcoded secret key or payment hash bytes are invalid.
#[instrument]
pub fn create_fake_invoice(amount_msat: u64, description: String) -> Bolt11Invoice {
    let private_key = SecretKey::from_slice(
        &[
            0xe1, 0x26, 0xf6, 0x8f, 0x7e, 0xaf, 0xcc, 0x8b, 0x74, 0xf5, 0x4d, 0x26, 0x9f, 0xe2,
            0x06, 0xbe, 0x71, 0x50, 0x00, 0xf9, 0x4d, 0xac, 0x06, 0x7d, 0x1c, 0x04, 0xa8, 0xca,
            0x3b, 0x2d, 0xb7, 0x34,
        ][..],
    )
    .expect("Valid 32-byte secret key");

    use bitcoin::secp256k1::rand::rngs::OsRng;
    use bitcoin::secp256k1::rand::Rng;
    let mut rng = OsRng;
    let mut random_bytes = [0u8; 32];
    rng.fill(&mut random_bytes);

    let payment_hash = sha256::Hash::from_slice(&random_bytes).expect("Valid 32-byte hash input");
    let payment_secret = PaymentSecret([42u8; 32]);

    InvoiceBuilder::new(Currency::Bitcoin)
        .description(description)
        .payment_hash(payment_hash)
        .payment_secret(payment_secret)
        .amount_milli_satoshis(amount_msat)
        .current_timestamp()
        .min_final_cltv_expiry_delta(144)
        .build_signed(|hash| Secp256k1::new().sign_ecdsa_recoverable(hash, &private_key))
        .expect("Failed to build fake invoice")
}

fn fake_onchain_outpoint(seed: &str) -> String {
    let txid = sha256::Hash::hash(seed.as_bytes());
    format!("{txid}:0")
}

fn fake_onchain_address(seed: &str) -> String {
    let secp_ctx = Secp256k1::new();
    let secret_key = fake_secret_key(seed);
    let public_key = bitcoin::secp256k1::PublicKey::from_secret_key(&secp_ctx, &secret_key);

    Address::p2wpkh(&CompressedPublicKey(public_key), Network::Regtest).to_string()
}

fn fake_secret_key(seed: &str) -> SecretKey {
    let mut attempt = 0u64;

    loop {
        let mut key_material = seed.as_bytes().to_vec();
        key_material.extend_from_slice(&attempt.to_le_bytes());

        let candidate = sha256::Hash::hash(&key_material);
        if let Ok(secret_key) = SecretKey::from_slice(candidate.as_ref()) {
            return secret_key;
        }

        attempt += 1;
    }
}

#[cfg(test)]
mod tests {
    use cdk_common::payment::{
        CustomIncomingPaymentOptions, IncomingPaymentOptions, MintPayment, PaymentIdentifier,
    };

    use super::*;

    fn test_wallet() -> FakeWallet {
        FakeWallet::new(
            FeeReserve {
                min_fee_reserve: 0.into(),
                percent_fee_reserve: 0.0,
            },
            HashMap::new(),
            HashSet::new(),
            0,
            CurrencyUnit::Sat,
        )
    }

    #[tokio::test]
    async fn custom_payment_methods_are_configurable() {
        let settings = test_wallet()
            .get_settings()
            .await
            .expect("fake wallet settings should load");
        assert!(settings.custom.is_empty());

        let wallet = test_wallet()
            .with_custom_payment_methods(HashMap::from([("Venmo".to_string(), "{}".to_string())]));

        let settings = wallet
            .get_settings()
            .await
            .expect("fake wallet settings should load");
        assert!(settings.custom.contains_key("venmo"));
        assert!(!settings.custom.contains_key("paypal"));
    }

    #[tokio::test]
    async fn configured_custom_incoming_payment_is_paid() {
        let wallet = test_wallet()
            .with_custom_payment_methods(HashMap::from([("venmo".to_string(), "{}".to_string())]));

        let response = wallet
            .create_incoming_payment_request(IncomingPaymentOptions::Custom(Box::new(
                CustomIncomingPaymentOptions {
                    method: "venmo".to_string(),
                    description: None,
                    amount: Amount::new(10, CurrencyUnit::Sat),
                    unix_expiry: None,
                    extra_json: None,
                },
            )))
            .await
            .expect("configured custom method should create a request");

        assert!(response.request.starts_with("venmo:"));
        assert!(matches!(
            response.request_lookup_id,
            PaymentIdentifier::CustomId(_)
        ));
    }
}
