use std::{fmt::Display, str::FromStr};

use bytes::Bytes;
use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, TimeZone};
use isocountry::CountryCode;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use serde_with::base64::Base64;
use uuid::Uuid;

#[cfg(feature = "uniffi")]
uniffi::setup_scaffolding!();

#[cfg(feature = "uniffi")]
uniffi::custom_type!(Bytes, Vec<u8>, {
    remote,
    try_lift: |val| Ok(val.into()),
    lower: |obj| obj.into(),
});

#[cfg(feature = "uniffi")]
uniffi::custom_type!(ConnectionId, String, {
    try_lift: |val| Ok(val.parse()?),
    lower: |obj| obj.to_string(),
});

#[cfg(feature = "uniffi")]
uniffi::custom_type!(Decimal, String, {
    remote,
    try_lift: |val| Ok(val.parse()?),
    lower: |obj| obj.to_string(),
});

#[cfg(feature = "uniffi")]
uniffi::custom_type!(CountryCode, String, {
    remote,
    try_lift: |val| Ok(Self::for_alpha2(&val)?),
    lower: |obj| obj.alpha2().to_string(),
});

/// Identifier of a specific service connection.
///
/// Supports serialization, comparison, and hashing for use e.g. in a `HashMap`.
#[derive(Serialize, Eq, PartialEq, Clone, Hash, Debug)]
pub struct ConnectionId(String);

impl Display for ConnectionId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        self.0.fmt(f)
    }
}

impl<'de> Deserialize<'de> for ConnectionId {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        String::deserialize(deserializer)?
            .parse()
            .map_err(serde::de::Error::custom)
    }
}

impl From<Uuid> for ConnectionId {
    fn from(value: Uuid) -> Self {
        Self(format!("connection-{value}"))
    }
}

impl From<&ConnectionId> for Uuid {
    fn from(value: &ConnectionId) -> Self {
        (value.0[11..]).parse().unwrap()
    }
}

impl FromStr for ConnectionId {
    type Err = uuid::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        s.trim_start_matches("connection-")
            .parse::<Uuid>()
            .map(Into::into)
    }
}

/// Requirements for user identifier and password.
#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Clone, Copy, Debug)]
#[non_exhaustive]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
#[serde(rename_all = "camelCase")]
pub struct CredentialsModel {
    /// A full set of credentials may be provided to support fully embedded authentication (including scraped redirects).
    pub full: bool,

    /// Only a user identifier without a password may be provided.
    /// This is typically the case for decoupled authentication where the user e.g. authorizes access in a mobile application.
    /// Note that if password-less authentication fails (e.g. as no device for decoupled authentication is set up for the user and
    /// a redirect is not supported), an error is returned and the transaction has to get restarted with a full set of credentials.
    pub user_id: bool,

    /// Credentials are not required. The user will provide them to the service provider during a redirect.
    pub none: bool,
}

#[cfg(feature = "kitx")]
impl CredentialsModel {
    pub const FULL: CredentialsModel = CredentialsModel {
        full: true,
        user_id: false,
        none: false,
    };

    pub const USER_ID: CredentialsModel = CredentialsModel {
        full: false,
        user_id: true,
        none: false,
    };

    pub const NONE: CredentialsModel = CredentialsModel {
        full: false,
        user_id: false,
        none: true,
    };

    pub const OPT_USER: CredentialsModel = CredentialsModel {
        full: false,
        user_id: true,
        none: true,
    };

    pub const OPT_FULL: CredentialsModel = CredentialsModel {
        full: true,
        user_id: false,
        none: true,
    };
}

#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
#[non_exhaustive]
pub enum PaymentErrorCode {
    LimitExceeded,
    InsufficientFunds,
}

#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
#[non_exhaustive]
pub enum ProviderErrorCode {
    Maintenance,
}

#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
#[non_exhaustive]
pub enum ServiceBlockedCode {
    /// Something is not set up for the user, e.g., there are no TAN methods.
    MissingSetup,
    /// User attention is required via another channel. Typically the user needs to log into the Online Banking.
    ActionRequired,
}

#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
#[non_exhaustive]
pub enum UnsupportedProductReason {
    /// The amount is not allowed for the payment product.
    Limit,
    /// The recipient is not capable to receive the payment product.
    Recipient,
    /// Scheduled payments are not supported.
    Scheduled,
}

#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Default, Debug)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Account {
    /// ISO 20022 IBAN2007Identifier.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub iban: Option<String>,

    /// Account number that is not an IBAN, e.g. ISO 20022 BBANIdentifier or primary account number (PAN) of a card account.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub number: Option<String>,

    /// ISO 20022 BICFIIdentifier.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub bic: Option<String>,

    /// National bank code.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub bank_code: Option<String>,

    /// ISO 4217 Alpha 3 currency code.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub currency: Option<String>,

    /// Name of account, assigned by ASPSP.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,

    /// Display name of account, assigned by PSU.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub display_name: Option<String>,

    /// Legal account owner.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub owner_name: Option<String>,

    /// Product name.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub product_name: Option<String>,

    /// Account Status.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub status: Option<AccountStatus>,

    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(rename = "type")]
    pub type_: Option<AccountType>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub capabilities: Option<Vec<Capability>>,
}

#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
#[non_exhaustive]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
pub enum AccountStatus {
    Available,
    Terminated,
    Blocked,
}

#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
#[non_exhaustive]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
pub enum AccountType {
    /// Account used to post debits and credits.
    /// ISO 20022 ExternalCashAccountType1Code CACC.
    Current,
    /// Account used for credit card payments.
    /// ISO 20022 ExternalCashAccountType1Code CARD.
    Card,
    /// Account used for savings.
    /// ISO 20022 ExternalCashAccountType1Code SVGS.
    Savings,
    /// Account used for call money.
    /// No dedicated ISO 20022 code (falls into SVGS).
    CallMoney,
    /// Account used for time deposits.
    /// No dedicated ISO 20022 code (falls into SVGS).
    TimeDeposit,
    /// Account used for loans.
    /// ISO 20022 ExternalCashAccountType1Code LOAN.
    Loan,
    Securities,
    Insurance,
    Commerce,
    Rewards,
}

#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
pub struct Amount {
    /// ISO 4217 Alpha 3 currency code.
    pub currency: String,

    pub amount: Decimal,
}

impl Amount {
    pub fn new(amount: impl Into<Decimal>, currency: impl Into<String>) -> Self {
        Self {
            amount: amount.into(),
            currency: currency.into(),
        }
    }
}

#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash, Debug)]
#[non_exhaustive]
pub enum Capability {
    Balances,
    Documents,
    Securities,
    Transactions,
    SinglePayment,
    BulkPayment,
    StandingOrders,
    ScheduledTransfers,
}

#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
#[non_exhaustive]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
pub enum TransactionStatus {
    /// The transaction is expected / planned.
    Pending,
    /// The transaction is booked to the account. This is typically the final state for most accounts.
    Booked,
    /// The credit card transaction is booked and invoiced but not yet paid.
    Invoiced,
    /// The credit card transaction is paid. This is typically the final state for card accounts.
    Paid,
    /// The transaction has been canceled in some way.
    Canceled,
}

#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
pub struct Fee {
    /// Amount of the fee.
    pub amount: Amount,

    /// ISO 20022 `ExternalChargeType1Code` for the fee.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[cfg_attr(feature = "uniffi", uniffi(default))]
    pub kind: Option<String>,

    /// ISO 20022 `BICFIIdentifier` of the agent to whom the charges are due.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[cfg_attr(feature = "uniffi", uniffi(default))]
    pub bic: Option<String>,
}

impl Fee {
    pub fn new(amount: impl Into<Amount>) -> Self {
        Self {
            amount: amount.into(),
            kind: None,
            bic: None,
        }
    }
}

/// User dialog.
///
/// This is meant to be displayed as a dialog in some User Interface and consists of:
///
/// - A way to cancel the dialog (typically an X symbol and / or a "Cancel" button).
/// - The display part:
///   - The `message`.
///   - An optional `image`.
/// - The interactive part defined by `input`.
///
/// The [`DialogInput`] contains a context for continuing the
/// process at the service that issued the dialog object.
#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Dialog<ConfirmationContext, InputContext> {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub context: Option<DialogContext>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub message: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub image: Option<Image>,
    #[serde(bound(
        serialize = "ConfirmationContext: AsRef<[u8]>, InputContext: AsRef<[u8]>",
        deserialize = "ConfirmationContext: From<Vec<u8>>, InputContext: From<Vec<u8>>"
    ))]
    pub input: DialogInput<ConfirmationContext, InputContext>,
}

impl<ConCtx, InpCtx> Dialog<ConCtx, InpCtx> {
    pub fn new(input: DialogInput<ConCtx, InpCtx>) -> Self {
        Self {
            context: None,
            message: None,
            image: None,
            input,
        }
    }
}

/// Context of a user dialog.
#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
#[non_exhaustive]
pub enum DialogContext {
    /// SCA or TAN process.
    ///
    /// There are multiple cases, distinguishable by the [`DialogInput`]:
    /// - [`DialogInput::Confirmation`]: Decoupled process (e.g. confirmation in a SCA app).
    /// - [`DialogInput::Selection`]: TAN method selection.
    /// - [`DialogInput::Field`]: TAN entry.
    Sca,

    /// Account selection.
    ///
    /// A [`DialogInput::Selection`] gets returned with this context when an account has to be selected.
    /// Note that there might be just a single option that may be chosen automatically without user interaction.
    Accounts,

    /// Pending redirect confirmation.
    ///
    /// A [`DialogInput::Confirmation`] gets returned with this context when a redirect got confirmed but no result is known yet.
    Redirect,

    /// Pending SCT Inst payment.
    ///
    /// A [`DialogInput::Confirmation`] gets returned with this context when an SCT Inst payment has been initialized and not reached the final status yet.
    PaymentStatus,

    /// Verification of Payee confirmation.
    ///
    /// A [`DialogInput::Confirmation`] gets returned with this context when an explicit confirmation of the creditor is required due to a name mismatch.
    /// Note that this confirmation has legal implications, releasing the bank from liabilities in case of the transfer to an unintended receiver due to incorrect creditor data.
    VopConfirmation,

    /// Pending Verification of Payee check.
    ///
    /// A [`DialogInput::Confirmation`] gets returned with this context when a Verification of Payee check for a bulk payment is still pending.
    VopCheck,
}

/// Data defining the interactive part of a user dialog.
#[serde_with::serde_as]
#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
#[serde(rename_all_fields = "camelCase")]
pub enum DialogInput<ConfirmationContext, InputContext> {
    /// Just a primary action to confirm the dialog.
    Confirmation {
        /// Context object that can be used to confirm the dialog.
        #[serde(bound(
            serialize = "ConfirmationContext: AsRef<[u8]>",
            deserialize = "ConfirmationContext: From<Vec<u8>>"
        ))]
        #[serde_as(as = "Base64")]
        context: ConfirmationContext,

        /// If polling is acceptable, a delay in seconds is specified for which the client has to wait before automatically confirming.
        #[serde(skip_serializing_if = "Option::is_none")]
        polling_delay_secs: Option<u32>,
    },

    /// A selection of options the user can choose from.
    Selection {
        /// Options are meant to be rendered e.g. as radio buttons where the user must select exactly
        /// one to for a confirmation button to get enabled. Another example for an implementation is
        /// one button per option that immediately confirms the selection.
        options: Vec<DialogOption>,

        /// Context object that can be used to respond to the dialog.
        #[serde(bound(
            serialize = "ConfirmationContext: AsRef<[u8]>",
            deserialize = "ConfirmationContext: From<Vec<u8>>"
        ))]
        #[serde_as(as = "Base64")]
        context: InputContext,
    },

    /// An input field.
    Field {
        /// Type that may be used for showing hints or dedicated keyboard layouts and for applying input restrictions or validation.
        #[serde(rename = "type")]
        type_: InputType,

        /// Indicates if the input should be masked.
        secrecy_level: SecrecyLevel,

        /// Minimal length to allow.
        #[serde(skip_serializing_if = "Option::is_none")]
        min_length: Option<u32>,

        /// Maximum length to allow.
        #[serde(skip_serializing_if = "Option::is_none")]
        max_length: Option<u32>,
        #[serde(bound(
            serialize = "InputContext: AsRef<[u8]>",
            deserialize = "InputContext: From<Vec<u8>>"
        ))]

        /// Context object that can be used to respond to the dialog.
        #[serde_as(as = "Base64")]
        context: InputContext,
    },
}

/// A dialog option.
#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
pub struct DialogOption {
    pub key: String,
    pub label: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[cfg_attr(feature = "uniffi", uniffi(default))]
    pub explanation: Option<String>,
}

impl DialogOption {
    pub fn new(key: impl Into<String>, label: impl Into<String>) -> Self {
        Self {
            key: key.into(),
            label: label.into(),
            explanation: None,
        }
    }
}

/// Image data for a dialog.
#[serde_with::serde_as]
#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
pub struct Image {
    pub mime_type: String,
    /// Binary data in the format defined by `mime_type`.
    #[serde_as(as = "Base64")]
    pub data: Bytes,
    #[allow(clippy::doc_markdown)]
    /// HHD_UC data block
    ///
    /// In cases where the ASPSP provides HHD_UC data for optical coupling with a HandHeld-Device
    /// for the generation of an OTP, especially for an HHD_OPT animated graphic, the raw HHD_UC
    /// data stream is provided here.
    ///
    /// The publicly available document "HandHeld-Device (HHD) for the generation of an OTP HHD
    /// enhancement for optical interfaces" describes how to implement the animated graphic for
    /// HHD_OPT in section C. `data` provides a pre-rendered animated GIF
    /// to be presented with a width of 62.5 mm.
    #[serde_as(as = "Option<Base64>")]
    #[serde(skip_serializing_if = "Option::is_none")]
    #[cfg_attr(feature = "uniffi", uniffi(default))]
    pub hhd_uc_data: Option<Bytes>,
}

impl Image {
    pub fn new(mime_type: impl Into<String>, data: impl Into<Bytes>) -> Self {
        Self {
            mime_type: mime_type.into(),
            data: data.into(),
            hhd_uc_data: None,
        }
    }
}

/// Level of secrecy for an input field.
#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
pub enum SecrecyLevel {
    /// The data is not a secret.
    Plain,
    /// The data is a one-time password. This can usually be treated as
    /// no secret but the implementer might still choose to mask the input.
    Otp,
    /// The data is a secret password. Input must be masked.
    Password,
}

/// Type of an input field.
#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
pub enum InputType {
    Date,
    Email,
    Number,
    Phone,
    Text,
}

#[derive(Serialize, Deserialize, Clone, Copy, Eq, PartialEq, Debug)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
#[non_exhaustive]
pub enum PaymentProduct {
    /// SEPA Credit Transfer (SCT) in EUR
    SepaCreditTransfer,

    /// SEPA Instant Credit Transfer (SCT Inst) in EUR
    SepaInstantCreditTransfer,

    /// Default SEPA Credit Transfer in EUR
    ///
    /// Tries SCT Inst with a fallback to SCT if this is supported.
    /// Otherwise, SCT is used.
    DefaultSepaCreditTransfer,

    /// International credit transfer outside of SEPA (typically SWIFT)
    CrossBorderCreditTransfer,

    /// Domestic credit transfer in the domestic, non-EUR currency
    DomesticCreditTransfer,

    /// Instant domestic credit transfer in the domestic, non-EUR currency
    DomesticInstantCreditTransfer,
}

#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash, Debug)]
#[serde(untagged)]
pub enum ISODateTimeOrDate {
    Date(NaiveDate),
    NaiveDateTime(NaiveDateTime),
    OffsetDateTime(DateTime<FixedOffset>),
}

impl ISODateTimeOrDate {
    pub fn date(&self, tz: &impl TimeZone) -> NaiveDate {
        match self {
            Self::Date(d) => *d,
            Self::NaiveDateTime(dt) => dt.date(),
            Self::OffsetDateTime(dt) => dt.with_timezone(tz).date_naive(),
        }
    }
}

#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
pub enum ChargeBearer {
    #[serde(rename = "DEBT")]
    BorneByDebtor,
    #[serde(rename = "CRED")]
    BorneByCreditor,
    #[serde(rename = "SHAR")]
    Shared,
    #[serde(rename = "SLEV")]
    FollowingServiceLevel,
}

impl Display for ChargeBearer {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        self.serialize(f)
    }
}

#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
pub struct CreditorAddress {
    pub town_name: String,
    pub country: CountryCode,
}

impl CreditorAddress {
    #[must_use]
    pub fn new(town_name: String, country: CountryCode) -> Self {
        Self { town_name, country }
    }
}

#[cfg(test)]
mod tests {
    use std::str::FromStr;

    use uuid::Uuid;

    use crate::ConnectionId;

    #[test]
    fn uuid_conversion() {
        let uuid = Uuid::new_v4();
        assert_eq!(uuid, Uuid::from(&ConnectionId::from(uuid)));
    }

    #[test]
    fn str_conversion() {
        let uuid = Uuid::new_v4();
        let s = format!("connection-{uuid}");

        assert_eq!(s, ConnectionId::from_str(&s).unwrap().to_string());
        assert_eq!(
            s,
            ConnectionId::from_str(&uuid.to_string())
                .unwrap()
                .to_string()
        );
    }

    #[test]
    fn deserialize_prefixed_connection_id() {
        let _ = Uuid::from(
            &serde_json::from_value::<super::ConnectionId>(serde_json::Value::String(
                "connection-00000000-0000-0000-0000-000000000000".to_string(),
            ))
            .unwrap(),
        );
    }

    #[test]
    fn deserialize_stripped_connection_id() {
        let _ = Uuid::from(
            &serde_json::from_value::<super::ConnectionId>(serde_json::Value::String(
                "00000000-0000-0000-0000-000000000000".to_string(),
            ))
            .unwrap(),
        );
    }

    #[test]
    fn deserialize_invalid_connection_id() {
        serde_json::from_value::<super::ConnectionId>(serde_json::Value::String(String::new()))
            .unwrap_err();
    }

    #[test]
    fn deserialize_invalid_prefixed_connection_id() {
        serde_json::from_value::<super::ConnectionId>(serde_json::Value::String(
            "connection-0000".to_string(),
        ))
        .unwrap_err();
    }

    #[cfg(feature = "uniffi")]
    fn try_lift(val: impl Into<String>) -> anyhow::Result<ConnectionId> {
        use uniffi::FfiConverter;

        <ConnectionId as FfiConverter<()>>::try_lift(<String as FfiConverter<()>>::lower(
            val.into(),
        ))
    }

    #[cfg(feature = "uniffi")]
    #[test]
    fn convert_uuid_like_connection_id() {
        let uuid = Uuid::new_v4();

        assert_eq!(
            try_lift(uuid.to_string()).unwrap(),
            ConnectionId::from(uuid),
        );
    }

    #[cfg(feature = "uniffi")]
    #[test]
    fn convert_prefixed_connection_id() {
        let uuid = Uuid::new_v4();

        assert_eq!(
            try_lift(format!("connection-{uuid}")).unwrap(),
            ConnectionId::from(uuid),
        );
    }

    #[cfg(feature = "uniffi")]
    #[test]
    fn convert_connection_id() {
        let connection_id = ConnectionId::from(Uuid::new_v4());

        assert_eq!(try_lift(connection_id.to_string()).unwrap(), connection_id);
    }

    #[cfg(feature = "uniffi")]
    #[test]
    fn convert_invalid_string() {
        assert_eq!(
            try_lift(String::new()).unwrap_err().to_string(),
            "Lifting custom type `routex_models::ConnectionId` from FFI type `alloc::string::String` failed at routex-models/src/lib.rs:22"
        );
    }
}
