use async_trait::async_trait;
use chrono::{DateTime, TimeZone, Utc};
use hmac::{Hmac, Mac};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use sha2::Sha256;

use crate::canonical::{
    CanonicalBillingDocument, CanonicalInvoice, CanonicalMoney, CanonicalPayment,
    CanonicalSubscription,
};
use crate::errors::BillingAdapterError;
use crate::providers::{
    BillingProviderAdapter, BillingProviderConnectionConfigDescriptor, BillingProviderDescriptor,
    BillingProviderRefetchProjection, BillingProviderRefetchRequest, BillingProviderResourceKind,
    BillingWebhookEventSummary, BillingWebhookProjection, config_field,
};
use crate::{
    AthenaInvoiceStatus, AthenaPaymentStatus, AthenaSubscriptionStatus, BillingConnectionMode,
    BillingProvider,
};

const STRIPE_SIGNATURE_HEADER: &str = "stripe-signature";
const STRIPE_SIGNATURE_TOLERANCE_SECONDS: i64 = 300;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StripeCredentialKind {
    SecretKey,
    RestrictedKey,
    OAuthAccessToken,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StripeConnectionConfig {
    pub api_base_url: String,
    pub credential_kind: StripeCredentialKind,
    pub api_key: String,
    pub mode: BillingConnectionMode,
    pub account_id: Option<String>,
    pub webhook_signing_secrets: Vec<String>,
}

impl StripeConnectionConfig {
    pub fn validate(&self) -> Result<(), BillingAdapterError> {
        if self.api_key.trim().is_empty() {
            return Err(BillingAdapterError::InvalidConfig(
                "api_key must not be empty".to_string(),
            ));
        }
        if self.api_base_url.trim().is_empty() {
            return Err(BillingAdapterError::InvalidConfig(
                "api_base_url must not be empty".to_string(),
            ));
        }
        Ok(())
    }
}

pub struct StripeProviderAdapter;

pub const STRIPE_PROVIDER_ADAPTER: StripeProviderAdapter = StripeProviderAdapter;

#[async_trait]
impl BillingProviderAdapter for StripeProviderAdapter {
    fn provider(&self) -> BillingProvider {
        BillingProvider::Stripe
    }

    fn descriptor(&self) -> BillingProviderDescriptor {
        BillingProviderDescriptor {
            provider: self.provider(),
            implementation_status: "implemented".to_string(),
            default_api_base_url: "https://api.stripe.com".to_string(),
            signature_header_name: Some(STRIPE_SIGNATURE_HEADER.to_string()),
            supported_webhook_envelope_kinds: vec!["snapshot".to_string()],
            supported_webhook_event_types: vec![
                "payment_intent.succeeded".to_string(),
                "payment_intent.processing".to_string(),
                "payment_intent.payment_failed".to_string(),
                "payment_intent.canceled".to_string(),
                "charge.refunded".to_string(),
                "invoice.created".to_string(),
                "invoice.finalized".to_string(),
                "invoice.payment_succeeded".to_string(),
                "invoice.paid".to_string(),
                "invoice.payment_failed".to_string(),
                "invoice.voided".to_string(),
                "invoice.marked_uncollectible".to_string(),
                "customer.subscription.created".to_string(),
                "customer.subscription.updated".to_string(),
                "customer.subscription.deleted".to_string(),
                "customer.subscription.paused".to_string(),
                "customer.subscription.resumed".to_string(),
                "customer.subscription.trial_will_end".to_string(),
                "checkout.session.completed".to_string(),
                "checkout.session.async_payment_succeeded".to_string(),
                "checkout.session.async_payment_failed".to_string(),
            ],
            supported_credential_kinds: vec![
                "secret_key".to_string(),
                "restricted_key".to_string(),
                "oauth_access_token".to_string(),
            ],
            supports_webhook_ingestion: true,
            supports_canonical_projection: true,
            supports_reconcile: true,
            supported_reconcile_resource_kinds: vec![
                BillingProviderResourceKind::Payment.as_str().to_string(),
                BillingProviderResourceKind::PaymentLink.as_str().to_string(),
                BillingProviderResourceKind::Subscription.as_str().to_string(),
                BillingProviderResourceKind::Invoice.as_str().to_string(),
            ],
            connection_config: BillingProviderConnectionConfigDescriptor {
                default_credential_kind: "secret_key".to_string(),
                fields: vec![
                    config_field(
                        "apiBaseUrl",
                        "string",
                        "Base URL for the Stripe API.",
                        true,
                    )
                    .with_default(Value::String("https://api.stripe.com".to_string())),
                    config_field(
                        "credentialKind",
                        "string",
                        "Stripe credential mode used for API requests.",
                        true,
                    )
                    .with_allowed_values(vec![
                        "secret_key".to_string(),
                        "restricted_key".to_string(),
                        "oauth_access_token".to_string(),
                    ])
                    .with_default(Value::String("secret_key".to_string()))
                    .with_derived_from("credentialKind"),
                    config_field(
                        "apiKey",
                        "string",
                        "Secret Stripe API key or OAuth access token for the selected credential kind.",
                        true,
                    )
                    .with_secret(true),
                    config_field(
                        "mode",
                        "string",
                        "Billing connection mode used when Athena normalizes provider requests.",
                        true,
                    )
                    .with_allowed_values(vec!["test".to_string(), "live".to_string()])
                    .with_derived_from("mode"),
                    config_field(
                        "accountId",
                        "string",
                        "Optional Stripe account binding used for Connect account scoping.",
                        false,
                    )
                    .with_derived_from("providerProfileId"),
                    config_field(
                        "webhookSigningSecrets",
                        "string_array",
                        "Ordered list of Stripe webhook signing secrets accepted for Stripe-Signature verification.",
                        true,
                    )
                    .with_default(Value::Array(Vec::new())),
                ],
            },
            integration: None,
        }
    }

    fn signature_header_name(&self) -> Option<&'static str> {
        Some(STRIPE_SIGNATURE_HEADER)
    }

    fn normalize_connection_config(
        &self,
        config: &mut serde_json::Map<String, Value>,
        mode: BillingConnectionMode,
        credential_kind: &str,
        provider_profile_id: Option<&str>,
    ) {
        normalize_connection_config(config, mode, credential_kind, provider_profile_id);
    }

    fn validate_connection_config(&self, config: &Value) -> Result<(), BillingAdapterError> {
        let config: StripeConnectionConfig = serde_json::from_value(config.clone())
            .map_err(|error| BillingAdapterError::InvalidConfig(error.to_string()))?;
        config.validate()
    }

    async fn project_webhook(
        &self,
        config: &Value,
        body: &[u8],
        signature_headers: &[String],
    ) -> Result<BillingWebhookProjection, BillingAdapterError> {
        let config: StripeConnectionConfig = serde_json::from_value(config.clone())
            .map_err(|error| BillingAdapterError::InvalidConfig(error.to_string()))?;
        let event = parse_webhook_event(body, signature_headers, &config.webhook_signing_secrets)?;
        let client = StripeClient::new(config)?;
        client.project_webhook(&event).await
    }

    async fn project_refetch(
        &self,
        config: &Value,
        request: &BillingProviderRefetchRequest,
    ) -> Result<BillingProviderRefetchProjection, BillingAdapterError> {
        let config: StripeConnectionConfig = serde_json::from_value(config.clone())
            .map_err(|error| BillingAdapterError::InvalidConfig(error.to_string()))?;
        let client = StripeClient::new(config)?;
        client.project_refetch(request).await
    }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StripeWebhookEventData {
    pub object: Value,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct StripeWebhookEvent {
    pub id: String,
    #[serde(rename = "type")]
    pub event_type: String,
    pub created: i64,
    #[serde(default)]
    pub account: Option<String>,
    pub data: StripeWebhookEventData,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StripePaymentIntent {
    pub id: String,
    #[serde(default)]
    pub customer: Option<String>,
    #[serde(default)]
    pub invoice: Option<String>,
    pub status: String,
    pub amount: i64,
    #[serde(default)]
    pub amount_received: Option<i64>,
    pub currency: String,
    #[serde(default)]
    pub description: Option<String>,
    #[serde(default)]
    pub metadata: Value,
    pub created: i64,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StripeCharge {
    pub id: String,
    #[serde(default)]
    pub payment_intent: Option<String>,
    #[serde(default)]
    pub customer: Option<String>,
    #[serde(default)]
    pub invoice: Option<String>,
    pub amount: i64,
    #[serde(default)]
    pub amount_refunded: Option<i64>,
    pub currency: String,
    #[serde(default)]
    pub description: Option<String>,
    #[serde(default)]
    pub metadata: Value,
    pub created: i64,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StripeInvoiceStatusTransitions {
    #[serde(default)]
    pub finalized_at: Option<i64>,
    #[serde(default)]
    pub paid_at: Option<i64>,
    #[serde(default)]
    pub voided_at: Option<i64>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StripeInvoice {
    pub id: String,
    #[serde(default)]
    pub customer: Option<String>,
    #[serde(default)]
    pub subscription: Option<String>,
    #[serde(default)]
    pub status: Option<String>,
    #[serde(default)]
    pub amount_due: Option<i64>,
    #[serde(default)]
    pub amount_paid: Option<i64>,
    #[serde(default)]
    pub currency: Option<String>,
    #[serde(default)]
    pub description: Option<String>,
    #[serde(default)]
    pub metadata: Value,
    pub created: i64,
    #[serde(default)]
    pub status_transitions: Option<StripeInvoiceStatusTransitions>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StripeRecurringPrice {
    #[serde(default)]
    pub interval: Option<String>,
    #[serde(default)]
    pub interval_count: Option<i64>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StripeSubscriptionPrice {
    #[serde(default)]
    pub unit_amount: Option<i64>,
    #[serde(default)]
    pub unit_amount_decimal: Option<String>,
    #[serde(default)]
    pub currency: Option<String>,
    #[serde(default)]
    pub recurring: Option<StripeRecurringPrice>,
    #[serde(default)]
    pub nickname: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StripeSubscriptionItem {
    #[serde(default)]
    pub price: Option<StripeSubscriptionPrice>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StripeListData<T> {
    #[serde(default)]
    pub data: Vec<T>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StripeSubscriptionItems {
    #[serde(default)]
    pub data: Vec<StripeSubscriptionItem>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StripeSubscription {
    pub id: String,
    pub customer: String,
    pub status: String,
    #[serde(default)]
    pub description: Option<String>,
    #[serde(default)]
    pub metadata: Value,
    pub created: i64,
    #[serde(default)]
    pub canceled_at: Option<i64>,
    #[serde(default)]
    pub current_period_end: Option<i64>,
    #[serde(default)]
    pub items: Option<StripeSubscriptionItems>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StripeCheckoutSession {
    pub id: String,
    #[serde(default)]
    pub mode: Option<String>,
    #[serde(default)]
    pub payment_intent: Option<String>,
    #[serde(default)]
    pub subscription: Option<String>,
    #[serde(default)]
    pub invoice: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
enum StripeCheckoutSessionTarget<'a> {
    PaymentIntent(&'a str),
    Subscription(&'a str),
    Invoice(&'a str),
}

pub struct StripeClient {
    http: Client,
    config: StripeConnectionConfig,
}

impl StripeClient {
    pub fn new(config: StripeConnectionConfig) -> Result<Self, BillingAdapterError> {
        config.validate()?;
        Ok(Self {
            http: Client::new(),
            config,
        })
    }

    async fn get_json<T>(
        &self,
        path: &str,
        query: &[(&str, String)],
    ) -> Result<T, BillingAdapterError>
    where
        T: serde::de::DeserializeOwned,
    {
        let base_url = self.config.api_base_url.trim_end_matches('/');
        let url = format!("{base_url}{path}");
        let mut request = self.http.get(url).bearer_auth(self.config.api_key.as_str());
        if let Some(account_id) = self
            .config
            .account_id
            .as_deref()
            .map(str::trim)
            .filter(|value| !value.is_empty())
        {
            request = request.header("Stripe-Account", account_id);
        }
        if !query.is_empty() {
            request = request.query(query);
        }

        let response = request
            .send()
            .await
            .map_err(|error| BillingAdapterError::Http(error.to_string()))?
            .error_for_status()
            .map_err(|error| BillingAdapterError::Http(error.to_string()))?;

        response
            .json::<T>()
            .await
            .map_err(|error| BillingAdapterError::Parse(error.to_string()))
    }

    pub async fn get_payment_intent(
        &self,
        payment_intent_id: &str,
    ) -> Result<StripePaymentIntent, BillingAdapterError> {
        self.get_json(
            &format!("/v1/payment_intents/{payment_intent_id}"),
            &[("expand[]", "invoice".to_string())],
        )
        .await
    }

    pub async fn get_invoice(
        &self,
        invoice_id: &str,
    ) -> Result<StripeInvoice, BillingAdapterError> {
        self.get_json(&format!("/v1/invoices/{invoice_id}"), &[])
            .await
    }

    pub async fn get_subscription(
        &self,
        subscription_id: &str,
    ) -> Result<StripeSubscription, BillingAdapterError> {
        self.get_json(
            &format!("/v1/subscriptions/{subscription_id}"),
            &[("expand[]", "items.data.price".to_string())],
        )
        .await
    }

    pub async fn get_checkout_session(
        &self,
        session_id: &str,
    ) -> Result<StripeCheckoutSession, BillingAdapterError> {
        self.get_json(
            &format!("/v1/checkout/sessions/{session_id}"),
            &[
                ("expand[]", "payment_intent".to_string()),
                ("expand[]", "subscription".to_string()),
            ],
        )
        .await
    }

    pub async fn project_webhook(
        &self,
        event: &StripeWebhookEvent,
    ) -> Result<BillingWebhookProjection, BillingAdapterError> {
        let payload = serde_json::to_value(event)
            .map_err(|error| BillingAdapterError::Parse(error.to_string()))?;
        let document = self.project_document_for_event(event).await?;

        Ok(BillingWebhookProjection {
            event: webhook_event_summary(event),
            payload,
            document,
        })
    }

    async fn project_document_for_event(
        &self,
        event: &StripeWebhookEvent,
    ) -> Result<Option<CanonicalBillingDocument>, BillingAdapterError> {
        match event.event_type.as_str() {
            "payment_intent.succeeded"
            | "payment_intent.processing"
            | "payment_intent.payment_failed"
            | "payment_intent.canceled" => {
                let payment_intent: StripePaymentIntent =
                    serde_json::from_value(event.data.object.clone())
                        .map_err(|error| BillingAdapterError::Parse(error.to_string()))?;
                let provider_subscription_id = self
                    .provider_subscription_id_for_payment_intent(&payment_intent)
                    .await?;
                let status_override = match event.event_type.as_str() {
                    "payment_intent.payment_failed" => Some(AthenaPaymentStatus::Failed),
                    "payment_intent.canceled" => Some(AthenaPaymentStatus::Canceled),
                    _ => None,
                };
                Ok(Some(CanonicalBillingDocument::Payment(
                    project_payment_intent(
                        &payment_intent,
                        event
                            .account
                            .as_deref()
                            .or(self.config.account_id.as_deref()),
                        None,
                        provider_subscription_id.as_deref(),
                        status_override,
                    ),
                )))
            }
            "charge.refunded" => {
                let charge: StripeCharge = serde_json::from_value(event.data.object.clone())
                    .map_err(|error| BillingAdapterError::Parse(error.to_string()))?;
                let provider_subscription_id =
                    self.provider_subscription_id_for_charge(&charge).await?;
                Ok(Some(CanonicalBillingDocument::Payment(
                    project_refunded_charge(
                        &charge,
                        event
                            .account
                            .as_deref()
                            .or(self.config.account_id.as_deref()),
                        provider_subscription_id.as_deref(),
                    ),
                )))
            }
            "invoice.created"
            | "invoice.finalized"
            | "invoice.payment_succeeded"
            | "invoice.paid"
            | "invoice.payment_failed"
            | "invoice.voided"
            | "invoice.marked_uncollectible" => {
                let invoice: StripeInvoice = serde_json::from_value(event.data.object.clone())
                    .map_err(|error| BillingAdapterError::Parse(error.to_string()))?;
                Ok(Some(CanonicalBillingDocument::Invoice(project_invoice(
                    &invoice,
                    event
                        .account
                        .as_deref()
                        .or(self.config.account_id.as_deref()),
                ))))
            }
            "customer.subscription.created"
            | "customer.subscription.updated"
            | "customer.subscription.deleted"
            | "customer.subscription.paused"
            | "customer.subscription.resumed"
            | "customer.subscription.trial_will_end" => {
                let subscription: StripeSubscription =
                    serde_json::from_value(event.data.object.clone())
                        .map_err(|error| BillingAdapterError::Parse(error.to_string()))?;
                Ok(Some(CanonicalBillingDocument::Subscription(
                    project_subscription(
                        &subscription,
                        event
                            .account
                            .as_deref()
                            .or(self.config.account_id.as_deref()),
                    ),
                )))
            }
            "checkout.session.completed"
            | "checkout.session.async_payment_succeeded"
            | "checkout.session.async_payment_failed" => {
                let session: StripeCheckoutSession =
                    serde_json::from_value(event.data.object.clone())
                        .map_err(|error| BillingAdapterError::Parse(error.to_string()))?;
                self.project_checkout_session(&session, event).await
            }
            _ => Ok(None),
        }
    }

    async fn project_checkout_session(
        &self,
        session: &StripeCheckoutSession,
        event: &StripeWebhookEvent,
    ) -> Result<Option<CanonicalBillingDocument>, BillingAdapterError> {
        let status_override = match event.event_type.as_str() {
            "checkout.session.async_payment_failed" => Some(AthenaPaymentStatus::Failed),
            "checkout.session.completed" | "checkout.session.async_payment_succeeded" => {
                Some(AthenaPaymentStatus::Paid)
            }
            _ => None,
        };

        self.project_checkout_session_target(
            session,
            event
                .account
                .as_deref()
                .or(self.config.account_id.as_deref()),
            status_override,
        )
        .await
    }

    pub async fn project_refetch(
        &self,
        request: &BillingProviderRefetchRequest,
    ) -> Result<BillingProviderRefetchProjection, BillingAdapterError> {
        let provider_ref = request.provider_ref.trim();
        if provider_ref.is_empty() {
            return Err(BillingAdapterError::InvalidConfig(
                "provider_ref must not be empty".to_string(),
            ));
        }

        match request.resource_kind {
            BillingProviderResourceKind::Payment => {
                let payment_intent = self.get_payment_intent(provider_ref).await?;
                let provider_subscription_id = self
                    .provider_subscription_id_for_payment_intent(&payment_intent)
                    .await?;
                Ok(BillingProviderRefetchProjection {
                    resource_kind: request.resource_kind,
                    provider_ref: provider_ref.to_string(),
                    payload: serde_json::to_value(&payment_intent)
                        .map_err(|error| BillingAdapterError::Parse(error.to_string()))?,
                    document: CanonicalBillingDocument::Payment(project_payment_intent(
                        &payment_intent,
                        self.config.account_id.as_deref(),
                        None,
                        provider_subscription_id.as_deref(),
                        None,
                    )),
                })
            }
            BillingProviderResourceKind::PaymentLink => {
                let session = self.get_checkout_session(provider_ref).await?;
                let document = self
                    .project_checkout_session_target(
                        &session,
                        self.config.account_id.as_deref(),
                        None,
                    )
                    .await?
                    .ok_or_else(|| {
                        BillingAdapterError::UnsupportedWebhook(format!(
                            "checkout session '{provider_ref}' does not reference a canonical stripe billing resource"
                        ))
                    })?;

                Ok(BillingProviderRefetchProjection {
                    resource_kind: request.resource_kind,
                    provider_ref: provider_ref.to_string(),
                    payload: serde_json::to_value(&session)
                        .map_err(|error| BillingAdapterError::Parse(error.to_string()))?,
                    document,
                })
            }
            BillingProviderResourceKind::Subscription => {
                let subscription = self.get_subscription(provider_ref).await?;
                Ok(BillingProviderRefetchProjection {
                    resource_kind: request.resource_kind,
                    provider_ref: provider_ref.to_string(),
                    payload: serde_json::to_value(&subscription)
                        .map_err(|error| BillingAdapterError::Parse(error.to_string()))?,
                    document: CanonicalBillingDocument::Subscription(project_subscription(
                        &subscription,
                        self.config.account_id.as_deref(),
                    )),
                })
            }
            BillingProviderResourceKind::Invoice => {
                let invoice = self.get_invoice(provider_ref).await?;
                Ok(BillingProviderRefetchProjection {
                    resource_kind: request.resource_kind,
                    provider_ref: provider_ref.to_string(),
                    payload: serde_json::to_value(&invoice)
                        .map_err(|error| BillingAdapterError::Parse(error.to_string()))?,
                    document: CanonicalBillingDocument::Invoice(project_invoice(
                        &invoice,
                        self.config.account_id.as_deref(),
                    )),
                })
            }
        }
    }

    async fn project_checkout_session_target(
        &self,
        session: &StripeCheckoutSession,
        account_id: Option<&str>,
        status_override: Option<AthenaPaymentStatus>,
    ) -> Result<Option<CanonicalBillingDocument>, BillingAdapterError> {
        let Some(target) = resolve_checkout_session_target(session) else {
            return Ok(None);
        };

        match target {
            StripeCheckoutSessionTarget::PaymentIntent(payment_intent_id) => {
                let payment_intent = self.get_payment_intent(payment_intent_id).await?;
                let provider_subscription_id = self
                    .provider_subscription_id_for_payment_intent(&payment_intent)
                    .await?;
                Ok(Some(CanonicalBillingDocument::Payment(
                    project_payment_intent(
                        &payment_intent,
                        account_id,
                        Some(session.id.as_str()),
                        provider_subscription_id
                            .as_deref()
                            .or(session.subscription.as_deref()),
                        status_override,
                    ),
                )))
            }
            StripeCheckoutSessionTarget::Subscription(subscription_id) => {
                let subscription = self.get_subscription(subscription_id).await?;
                Ok(Some(CanonicalBillingDocument::Subscription(
                    project_subscription(&subscription, account_id),
                )))
            }
            StripeCheckoutSessionTarget::Invoice(invoice_id) => {
                let invoice = self.get_invoice(invoice_id).await?;
                Ok(Some(CanonicalBillingDocument::Invoice(project_invoice(
                    &invoice, account_id,
                ))))
            }
        }
    }

    async fn provider_subscription_id_for_payment_intent(
        &self,
        payment_intent: &StripePaymentIntent,
    ) -> Result<Option<String>, BillingAdapterError> {
        let Some(invoice_id) = payment_intent
            .invoice
            .as_deref()
            .map(str::trim)
            .filter(|value| !value.is_empty())
        else {
            return Ok(None);
        };

        let invoice = self.get_invoice(invoice_id).await?;
        Ok(invoice.subscription)
    }

    async fn provider_subscription_id_for_charge(
        &self,
        charge: &StripeCharge,
    ) -> Result<Option<String>, BillingAdapterError> {
        let Some(invoice_id) = charge
            .invoice
            .as_deref()
            .map(str::trim)
            .filter(|value| !value.is_empty())
        else {
            return Ok(None);
        };

        let invoice = self.get_invoice(invoice_id).await?;
        Ok(invoice.subscription)
    }
}

fn resolve_checkout_session_target(
    session: &StripeCheckoutSession,
) -> Option<StripeCheckoutSessionTarget<'_>> {
    if let Some(payment_intent_id) = session
        .payment_intent
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
    {
        return Some(StripeCheckoutSessionTarget::PaymentIntent(
            payment_intent_id,
        ));
    }

    if let Some(subscription_id) = session
        .subscription
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
    {
        return Some(StripeCheckoutSessionTarget::Subscription(subscription_id));
    }

    session
        .invoice
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(StripeCheckoutSessionTarget::Invoice)
}

pub fn normalize_connection_config(
    config: &mut serde_json::Map<String, Value>,
    mode: BillingConnectionMode,
    credential_kind: &str,
    account_id: Option<&str>,
) {
    config.insert(
        "apiBaseUrl".to_string(),
        config
            .get("apiBaseUrl")
            .cloned()
            .unwrap_or_else(|| Value::String("https://api.stripe.com".to_string())),
    );
    config.insert("mode".to_string(), Value::String(mode.as_str().to_string()));
    config.insert(
        "credentialKind".to_string(),
        Value::String(credential_kind.to_string()),
    );
    if let Some(account_id) = account_id.filter(|value| !value.trim().is_empty()) {
        config.insert(
            "accountId".to_string(),
            Value::String(account_id.to_string()),
        );
    } else {
        config.remove("accountId");
    }
    config
        .entry("webhookSigningSecrets".to_string())
        .or_insert_with(|| Value::Array(Vec::new()));
}

pub fn verify_webhook_signature(
    body: &[u8],
    signature_headers: &[String],
    signing_secrets: &[String],
) -> Result<(), BillingAdapterError> {
    let secret_candidates = signing_secrets
        .iter()
        .map(|value| value.trim())
        .filter(|value| !value.is_empty())
        .collect::<Vec<_>>();
    if secret_candidates.is_empty() {
        return Err(BillingAdapterError::InvalidConfig(
            "stripe webhook signing secrets must not be empty".to_string(),
        ));
    }
    if signature_headers.is_empty() {
        return Err(BillingAdapterError::WebhookVerification(format!(
            "missing {STRIPE_SIGNATURE_HEADER} header"
        )));
    }

    let (timestamp, signatures) = parse_signature_headers(signature_headers)?;
    let age = (Utc::now().timestamp() - timestamp).abs();
    if age > STRIPE_SIGNATURE_TOLERANCE_SECONDS {
        return Err(BillingAdapterError::WebhookVerification(format!(
            "stripe signature timestamp exceeded tolerance of {STRIPE_SIGNATURE_TOLERANCE_SECONDS} seconds"
        )));
    }

    let payload_to_sign = format!("{timestamp}.{}", String::from_utf8_lossy(body));
    for secret in secret_candidates {
        let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes())
            .map_err(|error| BillingAdapterError::InvalidConfig(error.to_string()))?;
        mac.update(payload_to_sign.as_bytes());
        let digest = format!("{:x}", mac.finalize().into_bytes());
        if signatures.iter().any(|candidate| candidate == &digest) {
            return Ok(());
        }
    }

    Err(BillingAdapterError::WebhookVerification(
        "no provided Stripe-Signature matched configured signing secrets".to_string(),
    ))
}

pub fn parse_webhook_event(
    body: &[u8],
    signature_headers: &[String],
    signing_secrets: &[String],
) -> Result<StripeWebhookEvent, BillingAdapterError> {
    verify_webhook_signature(body, signature_headers, signing_secrets)?;
    serde_json::from_slice(body).map_err(|error| BillingAdapterError::Parse(error.to_string()))
}

pub fn webhook_event_summary(event: &StripeWebhookEvent) -> BillingWebhookEventSummary {
    BillingWebhookEventSummary {
        provider: BillingProvider::Stripe,
        envelope_kind: "snapshot".to_string(),
        verification_mode: "stripe_signature".to_string(),
        signature_header_name: Some(STRIPE_SIGNATURE_HEADER.to_string()),
        resource: event
            .data
            .object
            .get("object")
            .and_then(Value::as_str)
            .map(ToString::to_string),
        event_id: Some(event.id.clone()),
        event_type: Some(event.event_type.clone()),
        entity_id: event
            .data
            .object
            .get("id")
            .and_then(Value::as_str)
            .map(ToString::to_string),
        created_at: timestamp_to_datetime(Some(event.created)),
    }
}

fn parse_signature_headers(
    signature_headers: &[String],
) -> Result<(i64, Vec<String>), BillingAdapterError> {
    let mut timestamp = None;
    let mut signatures = Vec::new();

    for header in signature_headers {
        for part in header.split(',') {
            let Some((key, value)) = part.trim().split_once('=') else {
                continue;
            };
            match key.trim() {
                "t" => {
                    let parsed = value.trim().parse::<i64>().map_err(|error| {
                        BillingAdapterError::WebhookVerification(format!(
                            "invalid stripe signature timestamp: {error}"
                        ))
                    })?;
                    timestamp = Some(parsed);
                }
                "v1" => {
                    let signature = value.trim().to_ascii_lowercase();
                    if !signature.is_empty() {
                        signatures.push(signature);
                    }
                }
                _ => {}
            }
        }
    }

    let timestamp = timestamp.ok_or_else(|| {
        BillingAdapterError::WebhookVerification(
            "missing timestamp in Stripe-Signature header".to_string(),
        )
    })?;
    if signatures.is_empty() {
        return Err(BillingAdapterError::WebhookVerification(
            "missing v1 signature in Stripe-Signature header".to_string(),
        ));
    }

    Ok((timestamp, signatures))
}

fn project_payment_intent(
    payment_intent: &StripePaymentIntent,
    provider_profile_id: Option<&str>,
    provider_payment_link_id: Option<&str>,
    provider_subscription_id: Option<&str>,
    status_override: Option<AthenaPaymentStatus>,
) -> CanonicalPayment {
    let amount_value = payment_intent
        .amount_received
        .unwrap_or(payment_intent.amount);
    CanonicalPayment {
        provider: BillingProvider::Stripe,
        provider_payment_id: payment_intent.id.clone(),
        provider_customer_id: payment_intent.customer.clone(),
        provider_profile_id: normalize_optional_text(provider_profile_id),
        provider_subscription_id: normalize_optional_text(provider_subscription_id),
        provider_payment_link_id: normalize_optional_text(provider_payment_link_id),
        status: status_override
            .unwrap_or_else(|| map_payment_intent_status(&payment_intent.status)),
        amount: minor_units_to_money(payment_intent.currency.as_str(), amount_value),
        amount_refunded: None,
        description: payment_intent.description.clone(),
        metadata: normalize_metadata_object(&payment_intent.metadata),
        paid_at: match status_override
            .unwrap_or_else(|| map_payment_intent_status(&payment_intent.status))
        {
            AthenaPaymentStatus::Paid | AthenaPaymentStatus::Refunded => {
                timestamp_to_datetime(Some(payment_intent.created))
            }
            _ => None,
        },
        created_at: timestamp_to_datetime(Some(payment_intent.created)),
        raw: serde_json::to_value(payment_intent).unwrap_or_else(|_| json!({})),
    }
}

fn project_refunded_charge(
    charge: &StripeCharge,
    provider_profile_id: Option<&str>,
    provider_subscription_id: Option<&str>,
) -> CanonicalPayment {
    CanonicalPayment {
        provider: BillingProvider::Stripe,
        provider_payment_id: charge
            .payment_intent
            .clone()
            .unwrap_or_else(|| charge.id.clone()),
        provider_customer_id: charge.customer.clone(),
        provider_profile_id: normalize_optional_text(provider_profile_id),
        provider_subscription_id: normalize_optional_text(provider_subscription_id),
        provider_payment_link_id: None,
        status: AthenaPaymentStatus::Refunded,
        amount: minor_units_to_money(charge.currency.as_str(), charge.amount),
        amount_refunded: Some(minor_units_to_money(
            charge.currency.as_str(),
            charge.amount_refunded.unwrap_or(charge.amount),
        )),
        description: charge.description.clone(),
        metadata: normalize_metadata_object(&charge.metadata),
        paid_at: timestamp_to_datetime(Some(charge.created)),
        created_at: timestamp_to_datetime(Some(charge.created)),
        raw: serde_json::to_value(charge).unwrap_or_else(|_| json!({})),
    }
}

fn project_invoice(invoice: &StripeInvoice, provider_profile_id: Option<&str>) -> CanonicalInvoice {
    let currency = invoice.currency.as_deref().unwrap_or("usd");
    CanonicalInvoice {
        provider: BillingProvider::Stripe,
        provider_invoice_id: invoice.id.clone(),
        provider_profile_id: normalize_optional_text(provider_profile_id),
        provider_customer_id: invoice.customer.clone(),
        status: map_invoice_status(invoice.status.as_deref()),
        amount: invoice
            .amount_due
            .map(|amount| minor_units_to_money(currency, amount)),
        amount_paid: invoice
            .amount_paid
            .map(|amount| minor_units_to_money(currency, amount)),
        description: invoice.description.clone(),
        metadata: normalize_metadata_object(&invoice.metadata),
        issued_at: invoice
            .status_transitions
            .as_ref()
            .and_then(|transitions| timestamp_to_datetime(transitions.finalized_at))
            .or_else(|| timestamp_to_datetime(Some(invoice.created))),
        paid_at: invoice
            .status_transitions
            .as_ref()
            .and_then(|transitions| timestamp_to_datetime(transitions.paid_at)),
        created_at: timestamp_to_datetime(Some(invoice.created)),
        raw: serde_json::to_value(invoice).unwrap_or_else(|_| json!({})),
    }
}

fn project_subscription(
    subscription: &StripeSubscription,
    provider_profile_id: Option<&str>,
) -> CanonicalSubscription {
    let price = subscription
        .items
        .as_ref()
        .and_then(|items| items.data.first())
        .and_then(|item| item.price.as_ref());
    let amount = price.and_then(project_subscription_price_money);
    let interval = price.and_then(project_subscription_interval);
    let currency = price.and_then(|value| value.currency.clone());
    let description = subscription
        .description
        .clone()
        .or_else(|| price.and_then(|value| value.nickname.clone()));

    CanonicalSubscription {
        provider: BillingProvider::Stripe,
        provider_subscription_id: subscription.id.clone(),
        provider_customer_id: subscription.customer.clone(),
        provider_profile_id: normalize_optional_text(provider_profile_id),
        status: map_subscription_status(&subscription.status),
        amount,
        currency,
        interval,
        description,
        metadata: normalize_metadata_object(&subscription.metadata),
        next_payment_date: subscription
            .current_period_end
            .and_then(|timestamp| timestamp_to_datetime(Some(timestamp)))
            .map(|date| date.date_naive().to_string()),
        created_at: timestamp_to_datetime(Some(subscription.created)),
        canceled_at: timestamp_to_datetime(subscription.canceled_at),
        raw: serde_json::to_value(subscription).unwrap_or_else(|_| json!({})),
    }
}

fn project_subscription_price_money(price: &StripeSubscriptionPrice) -> Option<CanonicalMoney> {
    let currency = price.currency.as_deref()?;
    if let Some(decimal) = price.unit_amount_decimal.as_deref() {
        return Some(CanonicalMoney {
            currency: currency.to_ascii_uppercase(),
            value: decimal.to_string(),
        });
    }
    price
        .unit_amount
        .map(|amount| minor_units_to_money(currency, amount))
}

fn project_subscription_interval(price: &StripeSubscriptionPrice) -> Option<String> {
    let recurring = price.recurring.as_ref()?;
    let interval = recurring.interval.as_deref()?.trim();
    if interval.is_empty() {
        return None;
    }
    let count = recurring.interval_count.unwrap_or(1);
    Some(format!("{count} {interval}"))
}

fn map_payment_intent_status(status: &str) -> AthenaPaymentStatus {
    match status.trim() {
        "succeeded" => AthenaPaymentStatus::Paid,
        "requires_capture" => AthenaPaymentStatus::Authorized,
        "canceled" => AthenaPaymentStatus::Canceled,
        "processing" => AthenaPaymentStatus::Pending,
        "requires_payment_method" | "requires_confirmation" | "requires_action" => {
            AthenaPaymentStatus::Pending
        }
        _ => AthenaPaymentStatus::Pending,
    }
}

fn map_subscription_status(status: &str) -> AthenaSubscriptionStatus {
    match status.trim() {
        "trialing" => AthenaSubscriptionStatus::Trialing,
        "active" => AthenaSubscriptionStatus::Active,
        "past_due" | "unpaid" | "incomplete" | "incomplete_expired" => {
            AthenaSubscriptionStatus::PastDue
        }
        "canceled" => AthenaSubscriptionStatus::Canceled,
        "paused" => AthenaSubscriptionStatus::Paused,
        _ => AthenaSubscriptionStatus::Active,
    }
}

fn map_invoice_status(status: Option<&str>) -> AthenaInvoiceStatus {
    match status.unwrap_or_default().trim() {
        "draft" => AthenaInvoiceStatus::Draft,
        "paid" => AthenaInvoiceStatus::Paid,
        "void" | "voided" => AthenaInvoiceStatus::Void,
        "uncollectible" => AthenaInvoiceStatus::Uncollectible,
        _ => AthenaInvoiceStatus::Open,
    }
}

fn minor_units_to_money(currency: &str, amount: i64) -> CanonicalMoney {
    CanonicalMoney {
        currency: currency.trim().to_ascii_uppercase(),
        value: format_minor_units(amount, currency),
    }
}

fn format_minor_units(amount: i64, currency: &str) -> String {
    if is_zero_decimal_currency(currency) {
        return amount.to_string();
    }

    let negative = amount.is_negative();
    let absolute = amount.abs();
    let major = absolute / 100;
    let minor = absolute % 100;
    if negative {
        format!("-{major}.{minor:02}")
    } else {
        format!("{major}.{minor:02}")
    }
}

fn is_zero_decimal_currency(currency: &str) -> bool {
    matches!(
        currency.trim().to_ascii_lowercase().as_str(),
        "bif"
            | "clp"
            | "djf"
            | "gnf"
            | "jpy"
            | "kmf"
            | "krw"
            | "mga"
            | "pyg"
            | "rwf"
            | "ugx"
            | "vnd"
            | "vuv"
            | "xaf"
            | "xof"
            | "xpf"
    )
}

fn normalize_metadata_object(value: &Value) -> Value {
    if value.is_object() {
        value.clone()
    } else {
        json!({})
    }
}

fn normalize_optional_text(value: Option<&str>) -> Option<String> {
    value
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(ToString::to_string)
}

fn timestamp_to_datetime(timestamp: Option<i64>) -> Option<DateTime<Utc>> {
    timestamp.and_then(|value| Utc.timestamp_opt(value, 0).single())
}

#[cfg(test)]
mod tests {
    use super::{
        StripeCharge, StripeCheckoutSession, StripeCheckoutSessionTarget, StripeConnectionConfig,
        StripeCredentialKind, StripeInvoice, StripeInvoiceStatusTransitions, StripePaymentIntent,
        StripeRecurringPrice, StripeSubscription, StripeSubscriptionItem, StripeSubscriptionItems,
        StripeSubscriptionPrice, StripeWebhookEvent, StripeWebhookEventData, format_minor_units,
        map_invoice_status, normalize_connection_config, parse_webhook_event, project_invoice,
        project_payment_intent, project_refunded_charge, project_subscription,
        resolve_checkout_session_target, verify_webhook_signature, webhook_event_summary,
    };
    use crate::{
        AthenaInvoiceStatus, AthenaPaymentStatus, AthenaSubscriptionStatus, BillingConnectionMode,
        BillingProvider,
    };
    use hmac::{Hmac, Mac};
    use serde_json::json;
    use sha2::Sha256;

    fn sign(payload: &[u8], secret: &str, timestamp: i64) -> String {
        let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).unwrap();
        mac.update(format!("{timestamp}.{}", String::from_utf8_lossy(payload)).as_bytes());
        format!("t={timestamp},v1={:x}", mac.finalize().into_bytes())
    }

    #[test]
    fn normalize_connection_config_sets_runtime_defaults() {
        let mut config = serde_json::Map::new();
        config.insert("apiKey".to_string(), json!("sk_test_123"));
        normalize_connection_config(
            &mut config,
            BillingConnectionMode::Test,
            "secret_key",
            Some("acct_123"),
        );

        assert_eq!(config["apiBaseUrl"], "https://api.stripe.com");
        assert_eq!(config["mode"], "test");
        assert_eq!(config["credentialKind"], "secret_key");
        assert_eq!(config["accountId"], "acct_123");
        assert_eq!(config["webhookSigningSecrets"], json!([]));
    }

    #[test]
    fn stripe_connection_config_validates_required_fields() {
        let config = StripeConnectionConfig {
            api_base_url: "https://api.stripe.com".to_string(),
            credential_kind: StripeCredentialKind::SecretKey,
            api_key: "sk_test_123".to_string(),
            mode: BillingConnectionMode::Test,
            account_id: Some("acct_123".to_string()),
            webhook_signing_secrets: vec!["whsec_123".to_string()],
        };
        assert!(config.validate().is_ok());
    }

    #[test]
    fn webhook_signature_verification_accepts_valid_signature() {
        let payload = br#"{"id":"evt_123","type":"payment_intent.succeeded","created":1734364624,"data":{"object":{"id":"pi_123","object":"payment_intent","status":"succeeded","amount":2495,"amount_received":2495,"currency":"eur","metadata":{},"created":1734364624}}}"#;
        let timestamp = chrono::Utc::now().timestamp();
        let header = sign(payload, "whsec_test", timestamp);
        verify_webhook_signature(payload, &[header], &["whsec_test".to_string()])
            .expect("signature should verify");
    }

    #[test]
    fn parse_webhook_event_returns_structured_event() {
        let payload = br#"{"id":"evt_123","type":"payment_intent.succeeded","created":1734364624,"account":"acct_123","data":{"object":{"id":"pi_123","object":"payment_intent","status":"succeeded","amount":2495,"amount_received":2495,"currency":"eur","metadata":{},"created":1734364624}}}"#;
        let timestamp = chrono::Utc::now().timestamp();
        let header = sign(payload, "whsec_test", timestamp);
        let event = parse_webhook_event(payload, &[header], &["whsec_test".to_string()])
            .expect("event should parse");

        assert_eq!(event.id, "evt_123");
        assert_eq!(event.event_type, "payment_intent.succeeded");
        assert_eq!(event.account.as_deref(), Some("acct_123"));
    }

    #[test]
    fn payment_intent_projection_maps_paid_state() {
        let payment_intent = StripePaymentIntent {
            id: "pi_123".to_string(),
            customer: Some("cus_123".to_string()),
            invoice: Some("in_123".to_string()),
            status: "succeeded".to_string(),
            amount: 2495,
            amount_received: Some(2495),
            currency: "eur".to_string(),
            description: Some("Bike tires".to_string()),
            metadata: json!({"campaign":"spring"}),
            created: 1734364624,
        };

        let payment = project_payment_intent(
            &payment_intent,
            Some("acct_123"),
            Some("cs_123"),
            Some("sub_123"),
            None,
        );
        assert_eq!(payment.provider, BillingProvider::Stripe);
        assert_eq!(payment.status, AthenaPaymentStatus::Paid);
        assert_eq!(payment.amount.value, "24.95");
        assert_eq!(payment.provider_payment_link_id.as_deref(), Some("cs_123"));
        assert_eq!(payment.provider_subscription_id.as_deref(), Some("sub_123"));
    }

    #[test]
    fn refunded_charge_projection_sets_refund_amount() {
        let charge = StripeCharge {
            id: "ch_123".to_string(),
            payment_intent: Some("pi_123".to_string()),
            customer: Some("cus_123".to_string()),
            invoice: Some("in_123".to_string()),
            amount: 2495,
            amount_refunded: Some(2495),
            currency: "eur".to_string(),
            description: Some("Refund".to_string()),
            metadata: json!({}),
            created: 1734364624,
        };

        let payment = project_refunded_charge(&charge, Some("acct_123"), Some("sub_123"));
        assert_eq!(payment.provider_payment_id, "pi_123");
        assert_eq!(payment.status, AthenaPaymentStatus::Refunded);
        assert_eq!(payment.provider_subscription_id.as_deref(), Some("sub_123"));
        assert_eq!(
            payment
                .amount_refunded
                .as_ref()
                .map(|value| value.value.as_str()),
            Some("24.95")
        );
    }

    #[test]
    fn invoice_projection_maps_paid_state() {
        let invoice = StripeInvoice {
            id: "in_123".to_string(),
            customer: Some("cus_123".to_string()),
            subscription: Some("sub_123".to_string()),
            status: Some("paid".to_string()),
            amount_due: Some(2495),
            amount_paid: Some(2495),
            currency: Some("eur".to_string()),
            description: Some("Starter invoice".to_string()),
            metadata: json!({}),
            created: 1734364624,
            status_transitions: Some(StripeInvoiceStatusTransitions {
                finalized_at: Some(1734364624),
                paid_at: Some(1734364684),
                voided_at: None,
            }),
        };

        let projected = project_invoice(&invoice, Some("acct_123"));
        assert_eq!(projected.status, AthenaInvoiceStatus::Paid);
        assert_eq!(projected.amount_paid.unwrap().value, "24.95");
    }

    #[test]
    fn subscription_projection_maps_interval_and_amount() {
        let subscription = StripeSubscription {
            id: "sub_123".to_string(),
            customer: "cus_123".to_string(),
            status: "active".to_string(),
            description: None,
            metadata: json!({}),
            created: 1734364624,
            canceled_at: None,
            current_period_end: Some(1736956624),
            items: Some(StripeSubscriptionItems {
                data: vec![StripeSubscriptionItem {
                    price: Some(StripeSubscriptionPrice {
                        unit_amount: Some(1499),
                        unit_amount_decimal: None,
                        currency: Some("eur".to_string()),
                        recurring: Some(StripeRecurringPrice {
                            interval: Some("month".to_string()),
                            interval_count: Some(1),
                        }),
                        nickname: Some("Starter".to_string()),
                    }),
                }],
            }),
        };

        let projected = project_subscription(&subscription, Some("acct_123"));
        assert_eq!(projected.status, AthenaSubscriptionStatus::Active);
        assert_eq!(
            projected.amount.as_ref().map(|value| value.value.as_str()),
            Some("14.99")
        );
        assert_eq!(projected.interval.as_deref(), Some("1 month"));
        assert_eq!(projected.description.as_deref(), Some("Starter"));
    }

    #[test]
    fn webhook_summary_uses_stripe_signature_mode() {
        let event = StripeWebhookEvent {
            id: "evt_123".to_string(),
            event_type: "invoice.paid".to_string(),
            created: 1734364624,
            account: Some("acct_123".to_string()),
            data: StripeWebhookEventData {
                object: json!({
                    "id": "in_123",
                    "object": "invoice"
                }),
            },
        };

        let summary = webhook_event_summary(&event);
        assert_eq!(summary.provider, BillingProvider::Stripe);
        assert_eq!(summary.verification_mode, "stripe_signature");
        assert_eq!(summary.entity_id.as_deref(), Some("in_123"));
    }

    #[test]
    fn zero_decimal_currency_formatting_stays_integral() {
        assert_eq!(format_minor_units(1500, "JPY"), "1500");
        assert_eq!(format_minor_units(2495, "eur"), "24.95");
    }

    #[test]
    fn invoice_status_mapping_handles_voided_alias() {
        assert_eq!(
            map_invoice_status(Some("voided")),
            AthenaInvoiceStatus::Void
        );
    }

    #[test]
    fn checkout_session_target_resolution_prefers_payment_intent_then_subscription_then_invoice() {
        let session = StripeCheckoutSession {
            id: "cs_123".to_string(),
            mode: Some("payment".to_string()),
            payment_intent: Some("pi_123".to_string()),
            subscription: Some("sub_123".to_string()),
            invoice: Some("in_123".to_string()),
        };

        assert_eq!(
            resolve_checkout_session_target(&session),
            Some(StripeCheckoutSessionTarget::PaymentIntent("pi_123"))
        );

        let session = StripeCheckoutSession {
            payment_intent: Some("  ".to_string()),
            ..session
        };
        assert_eq!(
            resolve_checkout_session_target(&session),
            Some(StripeCheckoutSessionTarget::Subscription("sub_123"))
        );

        let session = StripeCheckoutSession {
            subscription: None,
            ..session
        };
        assert_eq!(
            resolve_checkout_session_target(&session),
            Some(StripeCheckoutSessionTarget::Invoice("in_123"))
        );
    }

    #[test]
    fn checkout_session_target_resolution_ignores_mode_when_invoice_is_the_only_link() {
        let session = StripeCheckoutSession {
            id: "cs_invoice_only".to_string(),
            mode: Some("setup".to_string()),
            payment_intent: None,
            subscription: None,
            invoice: Some("in_456".to_string()),
        };

        assert_eq!(
            resolve_checkout_session_target(&session),
            Some(StripeCheckoutSessionTarget::Invoice("in_456"))
        );
    }

    #[test]
    fn checkout_session_target_resolution_returns_none_when_no_billable_resource_is_linked() {
        let session = StripeCheckoutSession {
            id: "cs_setup".to_string(),
            mode: Some("setup".to_string()),
            payment_intent: None,
            subscription: None,
            invoice: None,
        };

        assert_eq!(resolve_checkout_session_target(&session), None);
    }
}
