//! Module for Feig's payment terminals.
//!
//! The module is an EVerest wrapper around the [ZVT](https://github.com/qwello/zvt)
//! crate. It talks directly to the underlying hardware and instructs it to read
//! cards.
//!
//! ## NFC card handling
//!
//! In case a NFC card is presented, the module will issue a token on the
//! `auth_token_provider` interface. The token-id will be generated from the
//! NFC card metadata.
//!
//! ## Bank card handling
//!
//! In case a bank card is presented, the module will also issue a token on the
//! `auth_token_provider` interface. The token-id must be provided by the
//! `bank_session_token` interface. In order to block the
//! `pre_authorization_amount`, the user must call the `auth_token_validator`
//! interface of the module. The modile commits (and releases the surplus from
//! the pre-authorized amount) once it receives the same a token-id on its
//! `session_cost` interface.
//!
//! ## Implementation details
//!
//! For more details checkout the
//! * [ZVT Rust implementation](https://github.com/qwello/zvt)
//! * [ZVT documentation](https://www.terminalhersteller.de/downloads.aspx)
//! * [Feig homepage](https://www.feig-payment.de/)
//!
#![allow(non_snake_case, non_camel_case_types, clippy::all)]
include!(concat!(env!("OUT_DIR"), "/generated.rs"));

use anyhow::Result;
use generated::errors::payment_terminal::{Error as PTError, PaymentTerminalError};
use generated::types::{
    authorization::{
        AuthorizationStatus, AuthorizationType, IdToken, IdTokenType, ProvidedIdToken,
        ValidationResult,
    },
    money::MoneyAmount,
    payment_terminal::{BankSessionToken, BankTransactionSummary},
    session_cost::{SessionCost, SessionStatus, TariffMessage},
};
use generated::{
    AuthTokenProviderServiceSubscriber, AuthTokenValidatorServiceSubscriber,
    BankSessionTokenProviderClientSubscriber, Context, Module, ModulePublisher, OnReadySubscriber,
    PaymentTerminalServiceSubscriber, SessionCostClientSubscriber,
};
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::sync::{mpsc::channel, mpsc::Sender, Arc, Mutex};
use std::time::Duration;
use std::{net::Ipv4Addr, str::FromStr};
use zvt::constants::ErrorMessages;
use zvt_feig_terminal::config::{Config, FeigConfig};
use zvt_feig_terminal::feig::{CardInfo, Error};

const INVALID_BANK_TOKEN: &str = "PAYMENT_TERMINAL_INVALID";

mod backoff {
    use std::cmp::min;
    use std::time::{Duration, Instant};

    pub struct Backoff {
        next_retry: Instant,
        backoff_secs: u64,
        max_backoff_secs: u64,
    }

    impl Backoff {
        pub fn from_secs(max_backoff_secs: u64) -> Self {
            Self {
                next_retry: Instant::now(),
                backoff_secs: 1,
                max_backoff_secs,
            }
        }

        pub fn is_ready(&self) -> bool {
            Instant::now() >= self.next_retry
        }

        pub fn record_failure(&mut self) {
            self.backoff_secs = min(self.backoff_secs * 2, self.max_backoff_secs);
            log::info!(
                "Recorded failure: next retry will be in {}",
                self.backoff_secs
            );
            self.next_retry = Instant::now() + Duration::from_secs(self.backoff_secs);
        }

        pub fn record_success(&mut self) {
            self.backoff_secs = 1;
            log::debug!("Next retry will be in {}", self.backoff_secs);
            self.next_retry = Instant::now() + Duration::from_secs(self.backoff_secs);
        }
    }

    #[cfg(test)]
    mod tests {
        use super::*;

        #[test]
        fn ready_immediately_after_creation() {
            let b = Backoff::from_secs(60);
            assert!(b.is_ready());
        }

        #[test]
        fn not_ready_after_failure() {
            let mut b = Backoff::from_secs(60);
            b.record_failure();
            assert!(!b.is_ready());
            assert_eq!(b.backoff_secs, 2);
        }

        #[test]
        fn exponential_increase() {
            let mut b = Backoff::from_secs(60);
            b.record_failure();
            assert_eq!(b.backoff_secs, 2);
            b.record_failure();
            assert_eq!(b.backoff_secs, 4);
            b.record_failure();
            assert_eq!(b.backoff_secs, 8);
        }

        #[test]
        fn caps_at_max() {
            let mut b = Backoff::from_secs(8);
            for _ in 0..10 {
                b.record_failure();
            }
            assert_eq!(b.backoff_secs, 8);
        }

        #[test]
        fn success_resets() {
            let mut b = Backoff::from_secs(60);
            b.record_failure();
            b.record_failure();
            b.record_success();
            assert_eq!(b.backoff_secs, 1);
        }
    }
}

mod sync_feig {
    use anyhow::Result;
    use std::sync::Mutex;
    use zvt_feig_terminal::{
        config::Config,
        feig::{CardInfo, Feig, TransactionSummary},
    };

    pub struct SyncFeig {
        /// Tokio runtime to call the Feig functions.
        rt: tokio::runtime::Runtime,

        /// The async impl of the Feig.
        inner: Mutex<Feig>,
    }

    /// Sync interface for the Feig.
    ///
    /// The `Feig` implements an async interface, which we can't use in EVerest.
    /// Here we wrap the async functions and expose the sync version of them.
    ///
    /// Below we allow `dead_code` so we just wrap all async functions even
    /// though they may be unused.
    #[allow(dead_code)]
    #[cfg_attr(test, mockall::automock)]
    impl SyncFeig {
        pub fn new(config: Config) -> Self {
            // Create a runtime for the Feig terminal.
            let rt = tokio::runtime::Builder::new_multi_thread()
                .max_blocking_threads(1)
                .enable_all()
                .build()
                .unwrap();

            // Create the Feig terminal itself.
            let feig = rt.block_on(async {
                loop {
                    let response = Feig::new(config.clone()).await;
                    match response {
                        Ok(inner) => {
                            log::info!("Payment terminal initialized.");
                            return inner;
                        }
                        Err(e) => {
                            log::warn!("Payment terminal not initialized {:?}", e);
                        }
                    }
                }
            });

            Self {
                rt,
                inner: Mutex::new(feig),
            }
        }

        pub fn read_card(&self) -> Result<CardInfo> {
            let mut inner = self.inner.lock().unwrap();
            self.rt.block_on(inner.read_card())
        }

        pub fn begin_transaction(&self, token: &str, amount: usize) -> Result<()> {
            let mut inner = self.inner.lock().unwrap();
            self.rt.block_on(inner.begin_transaction(token, amount))
        }

        pub fn cancel_transaction(&self, token: &str) -> Result<()> {
            let mut inner = self.inner.lock().unwrap();
            self.rt.block_on(inner.cancel_transaction(token))
        }

        pub fn commit_transaction(&self, token: &str, amount: u64) -> Result<TransactionSummary> {
            let mut inner = self.inner.lock().unwrap();
            self.rt.block_on(inner.commit_transaction(token, amount))
        }

        pub fn configure(&self) -> Result<()> {
            let mut inner = self.inner.lock().unwrap();
            self.rt.block_on(inner.configure())
        }
    }
}

#[mockall_double::double]
use sync_feig::SyncFeig;

impl ProvidedIdToken {
    fn new(
        id_token: String,
        authorization_type: AuthorizationType,
        connectors: Option<Vec<i64>>,
    ) -> Self {
        Self {
            parent_id_token: None,
            id_token: IdToken {
                value: id_token,
                r#type: IdTokenType::Local,
                additional_info: None,
            },
            authorization_type,
            certificate: None,
            connectors,
            iso_15118_certificate_hash_data: None,
            prevalidated: None,
            request_id: None,
        }
    }
}

impl From<anyhow::Error> for PTError {
    fn from(value: anyhow::Error) -> Self {
        match value.downcast_ref::<Error>() {
            Some(inner) => match inner {
                Error::TidMismatch => {
                    PTError::PaymentTerminal(PaymentTerminalError::TerminalIdNotSet)
                }
                Error::IncorrectDeviceId { .. } => {
                    PTError::PaymentTerminal(PaymentTerminalError::IncorrectDeviceId)
                }
                _ => PTError::PaymentTerminal(PaymentTerminalError::GenericPaymentTerminalError),
            },
            None => PTError::PaymentTerminal(PaymentTerminalError::GenericPaymentTerminalError),
        }
    }
}

impl From<ErrorMessages> for AuthorizationStatus {
    fn from(code: ErrorMessages) -> Self {
        match code {
            #[cfg(feature = "with_lavego_error_codes")]
            ErrorMessages::ContactlessTransactionCountExceeded => AuthorizationStatus::PinRequired,
            #[cfg(feature = "with_lavego_error_codes")]
            ErrorMessages::PinEntryRequiredx33 => AuthorizationStatus::PinRequired,
            #[cfg(feature = "with_lavego_error_codes")]
            ErrorMessages::PinEntryRequiredx3d => AuthorizationStatus::PinRequired,
            #[cfg(feature = "with_lavego_error_codes")]
            ErrorMessages::PinEntryRequiredx41 => AuthorizationStatus::PinRequired,
            ErrorMessages::PinProcessingNotPossible
                | ErrorMessages::NecessaryDeviceNotPresentOrDefective => AuthorizationStatus::PinRequired,
            ErrorMessages::CreditNotSufficient => AuthorizationStatus::NoCredit,
            ErrorMessages::PaymentMethodNotSupported => AuthorizationStatus::Blocked,
            ErrorMessages::AbortViaTimeoutOrAbortKey => AuthorizationStatus::Invalid,
            #[cfg(feature = "with_lavego_error_codes")]
            ErrorMessages::Declined => AuthorizationStatus::Timeout,
            ErrorMessages::ReceiverNotReady
                | ErrorMessages::SystemError
                | ErrorMessages::ErrorFromDialUp   // error from dial-up/communication fault
              => AuthorizationStatus::Timeout,
            _ => AuthorizationStatus::Unknown,
        }
    }
}

impl Hash for AuthorizationType {
    fn hash<H: Hasher>(&self, state: &mut H) {
        std::mem::discriminant(self).hash(state);
    }
}

impl Eq for AuthorizationType {}

impl From<AuthorizationStatus> for ValidationResult {
    fn from(value: AuthorizationStatus) -> Self {
        ValidationResult {
            authorization_status: value,
            tariff_messages: Vec::new(),
            allowed_energy_transfer_modes: None,
            certificate_status: None,
            evse_ids: None,
            expiry_time: None,
            parent_id_token: None,
            reservation_id: None,
        }
    }
}

/// Returns the default card type to connector mapping where all card types
/// are enabled for all connectors.
fn default_card_type_to_connector() -> HashMap<AuthorizationType, Option<Vec<i64>>> {
    HashMap::from_iter([
        (AuthorizationType::BankCard, None),
        (AuthorizationType::RFID, None),
    ])
}

/// Main struct for this module.
pub struct PaymentTerminalModule {
    /// Sender for the `ModulePublisher` -> to get the publisher from `on_ready`
    /// into the main thread.
    tx: Sender<ModulePublisher>,

    /// The Feig interface.
    feig: SyncFeig,

    /// The mapping of supported connectors for every card type. None means
    /// no restrictions, Some(vec![]) means no connector.
    card_type_to_connector: Mutex<HashMap<AuthorizationType, Option<Vec<i64>>>>,

    /// The configurable pre-auth.
    pre_authorization_amount: Mutex<usize>,
}

impl PaymentTerminalModule {
    /// Waits for a card and generates an auth token.
    ///
    /// Regardless of the card type we don't flag the token as pre-validated to
    /// allow the consumers to add custom validation steps on top. For bank
    /// cards use the `auth_token_validator` to pre-authorize money.
    fn read_card(&self, publishers: &ModulePublisher) -> Result<()> {
        let mut token: Option<String> = None;

        // Wait for the card.
        let mut read_card_loop = || -> CardInfo {
            // Long backoff to the payment provider backend to not overload it.
            let mut configure_backoff = backoff::Backoff::from_secs(3600);
            let mut bank_token_backoff = backoff::Backoff::from_secs(60);
            loop {
                if configure_backoff.is_ready() {
                    if let Err(inner) = self.feig.configure() {
                        log::warn!("Failed to configure: {inner:?}");
                        let inner: PTError = inner.into();
                        publishers.payment_terminal.raise_error(inner.into());
                        configure_backoff.record_failure();
                    } else {
                        configure_backoff.record_success();
                        publishers.payment_terminal.clear_all_errors();
                    }
                }

                let bank_cards_enabled = {
                    let mut map_guard = self.card_type_to_connector.lock().unwrap();
                    map_guard
                        .entry(AuthorizationType::BankCard)
                        .or_default()
                        .as_ref()
                        .map_or(true, |v| !v.is_empty())
                };

                // Attempting to get an invoice token
                if token.is_none() && bank_cards_enabled {
                    if let Some(publisher) = publishers.bank_session_token_slots.get(0) {
                        if bank_token_backoff.is_ready() {
                            token = publisher.get_bank_session_token().map_or(None, |v| v.token);

                            if token.is_none() {
                                bank_token_backoff.record_failure();
                            } else {
                                log::info!("Received the invoice token {token:?}");
                            }
                        }
                    }
                }

                match self.feig.read_card() {
                    Ok(card_info) => return card_info,
                    Err(e) => {
                        if let Some(Error::NoCardPresented) = e.downcast_ref::<Error>() {
                            log::debug!("No card presented");
                        } else {
                            log::warn!("Failed to read a card {e:?}");
                            // Cleared in the next loop if we can configure the
                            // feig.
                            let inner: PTError = e.into();
                            publishers.payment_terminal.raise_error(inner.into());
                        }
                    }
                };
            }
        };
        let card_info = read_card_loop();
        let mut map_guard = self.card_type_to_connector.lock().unwrap();

        let provided_token = match card_info {
            CardInfo::Bank => ProvidedIdToken::new(
                // If we don't have a valid token, still issue the token but
                // reject it in the validation - so e.x. display can still
                // react to cards being read.
                token.unwrap_or(INVALID_BANK_TOKEN.to_string()),
                AuthorizationType::BankCard,
                map_guard
                    .entry(AuthorizationType::BankCard)
                    .or_default()
                    .clone(),
            ),
            CardInfo::MembershipCard(id_token) => ProvidedIdToken::new(
                id_token,
                AuthorizationType::RFID,
                map_guard
                    .entry(AuthorizationType::RFID)
                    .or_default()
                    .clone(),
            ),
        };
        publishers.token_provider.provided_token(provided_token)?;
        Ok(())
    }

    /// The implementation of the `SessionCostClientSubscriber::on_session_cost`,
    /// but here we can return errors.
    fn on_session_cost_impl(&self, context: &Context, value: SessionCost) -> Result<()> {
        let Some(id_tag) = value.id_tag else {
            return Ok(());
        };

        // We only care about bank cards.
        match id_tag.authorization_type {
            AuthorizationType::BankCard => (),
            _ => return Ok(()),
        }

        if let SessionStatus::Running = value.status {
            log::info!("The session is still running");
            return Ok(());
        }

        let total_cost = value
            .cost_chunks
            .unwrap_or_default()
            .into_iter()
            .fold(0, |acc, chunk| {
                acc + chunk.cost.unwrap_or(MoneyAmount { value: 0 }).value
            });

        let res = self
            .feig
            .commit_transaction(&id_tag.id_token.value, total_cost as u64)?;

        context
            .publisher
            .payment_terminal
            .bank_transaction_summary(BankTransactionSummary {
                session_token: Some(BankSessionToken {
                    token: Some(id_tag.id_token.value.clone()),
                }),
                transaction_data: Some(format!("{:06}", res.trace_number.unwrap_or_default())),
            })?;
        Ok(())
    }
}

impl AuthTokenProviderServiceSubscriber for PaymentTerminalModule {}

impl BankSessionTokenProviderClientSubscriber for PaymentTerminalModule {}

impl OnReadySubscriber for PaymentTerminalModule {
    fn on_ready(&self, publishers: &ModulePublisher) {
        // Send the publishers to the main thread.
        self.tx.send(publishers.clone()).unwrap();
    }
}

impl SessionCostClientSubscriber for PaymentTerminalModule {
    fn on_session_cost(&self, context: &Context, value: SessionCost) {
        let res = self.on_session_cost_impl(context, value);
        match res {
            Ok(_) => log::debug!("Transaction successful"),
            Err(err) => log::error!("Transaction failed {err:}"),
        }
    }

    fn on_tariff_message(&self, _context: &Context, value: TariffMessage) {
        if let Some(pre_authorization_amount) = value.pre_authorized_amount {
            log::info!(
                "Updating pre_authorization_amount to {}",
                pre_authorization_amount.value
            );
            *self.pre_authorization_amount.lock().unwrap() =
                pre_authorization_amount.value as usize;
        }
    }
}

impl AuthTokenValidatorServiceSubscriber for PaymentTerminalModule {
    fn validate_token(
        &self,
        _context: &Context,
        provided_token: ProvidedIdToken,
    ) -> ::everestrs::Result<ValidationResult> {
        if provided_token.authorization_type != AuthorizationType::BankCard {
            log::warn!(
                "{:?} not supported: can only validate `AuthorizationType::BankCard`",
                provided_token.authorization_type
            );
            return Ok(AuthorizationStatus::Invalid.into());
        }

        if &provided_token.id_token.value == INVALID_BANK_TOKEN {
            log::warn!("Validating a `BankCard` without an invoice token");
            return Ok(AuthorizationStatus::Invalid.into());
        }

        if let Err(err) = self.feig.begin_transaction(
            &provided_token.id_token.value,
            *self.pre_authorization_amount.lock().unwrap(),
        ) {
            log::warn!("Failed to start a transaction: {err:?}");
            match err.downcast_ref::<ErrorMessages>() {
                Some(rejection_reason) => {
                    log::info!("Recieved rejection reason {}", rejection_reason);
                    let status: AuthorizationStatus = (*rejection_reason).into();
                    return Ok(status.into());
                }
                None => {
                    log::info!("No error code provided");
                    return Ok(AuthorizationStatus::Invalid.into());
                }
            };
        }

        Ok(AuthorizationStatus::Accepted.into())
    }
}

impl PaymentTerminalServiceSubscriber for PaymentTerminalModule {
    fn enable_card_reading(
        &self,
        _context: &Context,
        connector_id: i64,
        supported_cards: Vec<AuthorizationType>,
    ) -> ::everestrs::Result<()> {
        let mut map_guard = self.card_type_to_connector.lock().unwrap();

        for (card_type, connector_ids) in map_guard.iter_mut() {
            match supported_cards.iter().find(|card| *card == card_type) {
                Some(_) => {
                    // Card is allowed -> Add the connector under the card type
                    let connector_ids = connector_ids.get_or_insert_default();
                    if let Err(idx) = connector_ids.binary_search(&connector_id) {
                        connector_ids.insert(idx, connector_id);
                    }
                }
                None => {
                    // Card is forbidden -> remove the connector from the card type
                    let connector_ids = connector_ids.get_or_insert_default();
                    if let Ok(idx) = connector_ids.binary_search(&connector_id) {
                        connector_ids.remove(idx);
                    }
                }
            }
        }
        Ok(())
    }

    fn allow_all_cards_for_every_connector(&self, _context: &Context) -> ::everestrs::Result<()> {
        let mut map_guard = self.card_type_to_connector.lock().unwrap();
        *map_guard = default_card_type_to_connector();
        Ok(())
    }
}

#[everestrs::main]
fn main(module: &Module) -> Result<()> {
    let config = module.get_config();
    log::info!("Received the config {config:?}");

    let pt_config = Config {
        terminal_id: config.terminal_id,
        feig_serial: config.feig_serial,
        ip_address: Ipv4Addr::from_str(&config.ip)?,
        feig_config: FeigConfig {
            currency: config.currency as usize,
            read_card_timeout: config.read_card_timeout as u8,
            password: config.password as usize,
            end_of_day_max_interval: config.end_of_day_max_interval as u64,
        },
        transactions_max_num: config.transactions_max_num as usize,
    };

    let (tx, rx) = channel();

    let pt_module = Arc::new(PaymentTerminalModule {
        tx,
        feig: SyncFeig::new(pt_config),
        card_type_to_connector: Mutex::new(default_card_type_to_connector()),
        pre_authorization_amount: Mutex::new(config.pre_authorization_amount as usize),
    });

    let _publishers = module.start(
        pt_module.clone(),
        pt_module.clone(),
        pt_module.clone(),
        pt_module.clone(),
        |_index| pt_module.clone(),
        |_index| pt_module.clone(),
    );

    let read_card_debounce = Duration::from_secs(config.read_card_debounce as u64);
    let publishers = rx.recv()?;
    loop {
        log::debug!("Waiting for transactions...");
        let res = pt_module.read_card(&publishers);
        match res {
            Ok(()) => {
                log::info!("Started a transaction");
                std::thread::sleep(read_card_debounce);
            }
            Err(err) => log::error!("Failed to start a transaction {err:?}"),
        }
    }
}

#[cfg(test)]
mod tests {
    use self::generated::types::money::Currency;
    use self::generated::types::money::CurrencyCode;
    use self::generated::types::session_cost::SessionCostChunk;
    use self::generated::BankSessionTokenProviderClientPublisher;

    use super::*;
    use mockall::predicate::eq;
    use zvt_feig_terminal::feig::TransactionSummary;

    impl From<SyncFeig> for PaymentTerminalModule {
        fn from(feig_mock: SyncFeig) -> Self {
            let (tx, _) = channel();

            PaymentTerminalModule {
                tx,
                feig: feig_mock,
                card_type_to_connector: Mutex::new(default_card_type_to_connector()),
                pre_authorization_amount: Mutex::new(11),
            }
        }
    }

    impl<'a> From<&'a ModulePublisher> for Context<'a> {
        fn from(everest_mock: &'a ModulePublisher) -> Context<'a> {
            Context {
                name: "foo",
                publisher: &everest_mock,
                index: 0,
            }
        }
    }

    #[test]
    fn payment_terminal__read_card__get_bank_session_token_failed() {
        let PARAMETERS = [
            (
                CardInfo::Bank,
                AuthorizationType::BankCard,
                INVALID_BANK_TOKEN,
            ),
            (
                CardInfo::MembershipCard("some id".to_string()),
                AuthorizationType::RFID,
                "some id",
            ),
        ];

        for (card_info, authorization_type, id_token) in PARAMETERS {
            let mut feig_mock = SyncFeig::default();
            feig_mock.expect_configure().times(1).return_once(|| Ok(()));
            feig_mock
                .expect_read_card()
                .times(1)
                .return_once(|| Ok(card_info));

            let mut everest_mock = ModulePublisher::default();
            everest_mock
                .bank_session_token_slots
                .push(BankSessionTokenProviderClientPublisher::default());

            everest_mock.bank_session_token_slots[0]
                .expect_get_bank_session_token()
                .times(1)
                .return_once(|| Err(::everestrs::Error::HandlerException("oh no".to_string())));

            everest_mock
                .token_provider
                .expect_provided_token()
                .times(1)
                .withf(move |arg| {
                    &arg.id_token.value == id_token && arg.authorization_type == authorization_type
                })
                .return_once(|_| Ok(()));

            everest_mock
                .payment_terminal
                .expect_clear_all_errors()
                .times(1)
                .return_once(|| ());

            let pt_module: PaymentTerminalModule = feig_mock.into();
            assert!(pt_module.read_card(&everest_mock).is_ok());
        }
    }

    #[test]
    fn payment_terminal__read_card__no_bank_session_token() {
        let PARAMETERS = [
            (
                CardInfo::Bank,
                AuthorizationType::BankCard,
                INVALID_BANK_TOKEN,
            ),
            (
                CardInfo::MembershipCard("some id".to_string()),
                AuthorizationType::RFID,
                "some id",
            ),
        ];
        // Here we don't have a bank session provider defined.
        for (card_info, authorization_type, id_token) in PARAMETERS {
            let mut feig_mock = SyncFeig::default();
            feig_mock.expect_configure().times(1).return_once(|| Ok(()));
            feig_mock
                .expect_read_card()
                .times(1)
                .return_once(|| Ok(card_info));

            let mut everest_mock = ModulePublisher::default();

            everest_mock
                .token_provider
                .expect_provided_token()
                .times(1)
                .withf(move |arg| {
                    &arg.id_token.value == id_token && arg.authorization_type == authorization_type
                })
                .return_once(|_| Ok(()));

            everest_mock
                .payment_terminal
                .expect_clear_all_errors()
                .times(1)
                .return_once(|| ());

            let pt_module: PaymentTerminalModule = feig_mock.into();
            assert!(pt_module.read_card(&everest_mock).is_ok());
        }
    }

    #[test]
    /// Test that bank cards are not processed when bank cards are disabled
    fn payment_terminal__read_card__bank_cards_disabled() {
        let mut feig_mock = SyncFeig::default();
        feig_mock.expect_configure().times(1).return_once(|| Ok(()));
        feig_mock
            .expect_read_card()
            .times(1)
            .return_once(|| Ok(CardInfo::Bank));

        let mut everest_mock = ModulePublisher::default();
        everest_mock
            .bank_session_token_slots
            .push(BankSessionTokenProviderClientPublisher::default());

        // get_bank_session_token should NOT be called when bank cards are disabled
        everest_mock.bank_session_token_slots[0]
            .expect_get_bank_session_token()
            .times(0);

        everest_mock
            .token_provider
            .expect_provided_token()
            .times(1)
            .withf(|arg| {
                // When bank cards are disabled, we should get INVALID_BANK_TOKEN
                &arg.id_token.value == INVALID_BANK_TOKEN
                    && arg.authorization_type == AuthorizationType::BankCard
                    // Connectors should be empty (bank cards disabled)
                    && arg.connectors == Some(vec![])
            })
            .return_once(|_| Ok(()));

        everest_mock
            .payment_terminal
            .expect_clear_all_errors()
            .times(1)
            .return_once(|| ());

        let (tx, _) = channel();
        // Set up module with bank cards disabled (empty connector list)
        let pt_module = PaymentTerminalModule {
            tx,
            feig: feig_mock,
            card_type_to_connector: Mutex::new(HashMap::from_iter([
                (AuthorizationType::BankCard, Some(vec![])), // Empty = disabled
                (AuthorizationType::RFID, None),
            ])),
            pre_authorization_amount: Mutex::new(11),
        };

        assert!(pt_module.read_card(&everest_mock).is_ok());
    }

    #[test]
    fn payment_terminal__read_card__success() {
        let PARAMETERS = [
            (
                CardInfo::Bank,
                AuthorizationType::BankCard,
                "some bank token",
                "some bank token",
            ),
            (
                CardInfo::MembershipCard("some id".to_string()),
                AuthorizationType::RFID,
                "some bank token",
                "some id",
            ),
        ];

        for (card_info, authorization_type, bank_token, id_token) in PARAMETERS {
            // When the token is None, we still expect membership card to work
            let mut feig_mock = SyncFeig::default();

            feig_mock
                .expect_read_card()
                .times(1)
                .return_once(|| Ok(card_info));

            let mut everest_mock = ModulePublisher::default();
            everest_mock
                .bank_session_token_slots
                .push(BankSessionTokenProviderClientPublisher::default());

            everest_mock.bank_session_token_slots[0]
                .expect_get_bank_session_token()
                .times(1)
                .return_once(|| {
                    Ok(BankSessionToken {
                        token: Some(bank_token.to_owned()),
                    })
                });

            everest_mock
                .token_provider
                .expect_provided_token()
                .times(1)
                .withf(move |arg| {
                    &arg.id_token.value == id_token && arg.authorization_type == authorization_type
                })
                .return_once(|_| Ok(()));

            everest_mock
                .payment_terminal
                .expect_clear_all_errors()
                .times(1)
                .return_once(|| ());

            feig_mock.expect_configure().times(1).return_once(|| Ok(()));

            let pt_module: PaymentTerminalModule = feig_mock.into();

            assert!(pt_module.read_card(&everest_mock).is_ok());
        }
    }

    #[test]
    /// We test that we don't commit anything for inputs which should be ignored.
    fn payment_terminal__on_session_cost_impl__noop() {
        let parameters = [
            (AuthorizationType::OCPP, SessionStatus::Finished),
            (AuthorizationType::RFID, SessionStatus::Finished),
            (AuthorizationType::BankCard, SessionStatus::Running),
        ];

        for (auth_type, status) in parameters {
            let session_cost = SessionCost {
                cost_chunks: None,
                currency: Currency {
                    code: Some(CurrencyCode::EUR),
                    decimals: None,
                },
                id_tag: Some(ProvidedIdToken::new(String::new(), auth_type, None)),
                status,
                session_id: String::new(),
                idle_price: None,
                charging_price: None,
                next_period: None,
                message: None,
                qr_code: None,
            };

            let everest_mock = ModulePublisher::default();
            let feig = SyncFeig::default();
            let pt_module: PaymentTerminalModule = feig.into();

            assert!(pt_module
                .on_session_cost_impl(&(&everest_mock).into(), session_cost)
                .is_ok());
        }
    }

    #[test]
    /// We test that we commit the right amount for transactions which are for
    /// us.
    fn payment_terminal__on_session_cost_impl() {
        let parameters = [
            (None, 0),
            (Some(vec![]), 0),
            (Some(vec![None]), 0),
            (Some(vec![Some(1), Some(2)]), 3),
        ];

        for (cost_chunks, amount) in parameters {
            let session_cost = SessionCost {
                cost_chunks: cost_chunks.map(|chunks| {
                    chunks
                        .into_iter()
                        .map(|cost| SessionCostChunk {
                            category: None,
                            cost: cost.map(|value| MoneyAmount { value }),
                            timestamp_from: None,
                            timestamp_to: None,
                            metervalue_from: None,
                            metervalue_to: None,
                        })
                        .collect()
                }),
                currency: Currency {
                    code: Some(CurrencyCode::EUR),
                    decimals: None,
                },
                id_tag: Some(ProvidedIdToken::new(
                    "token".to_string(),
                    AuthorizationType::BankCard,
                    None,
                )),
                status: SessionStatus::Finished,
                session_id: String::new(),
                idle_price: None,
                charging_price: None,
                next_period: None,
                message: None,
                qr_code: None,
            };
            let mut everest_mock = ModulePublisher::default();
            everest_mock
                .payment_terminal
                .expect_bank_transaction_summary()
                .times(1)
                .returning(|_| Ok(()));

            let mut feig = SyncFeig::default();
            feig.expect_commit_transaction()
                .times(1)
                .with(eq("token"), eq(amount))
                .returning(|_, _| {
                    Ok(TransactionSummary {
                        terminal_id: None,
                        amount: None,
                        trace_number: None,
                        date: None,
                        time: None,
                    })
                });
            let pt_module: PaymentTerminalModule = feig.into();

            assert!(pt_module
                .on_session_cost_impl(&(&everest_mock).into(), session_cost)
                .is_ok());
        }
    }

    #[test]
    /// Test validate_token with non-BankCard authorization type
    fn payment_terminal__validate_token__invalid_authorization_type() {
        let parameters = [AuthorizationType::OCPP, AuthorizationType::RFID];

        for auth_type in parameters {
            let feig_mock = SyncFeig::default();
            let pt_module: PaymentTerminalModule = feig_mock.into();

            let provided_token = ProvidedIdToken::new("some_token".to_string(), auth_type, None);
            let everest_mock = ModulePublisher::default();

            let result = pt_module.validate_token(&(&everest_mock).into(), provided_token);
            assert!(result.is_ok());
            let validation_result = result.unwrap();
            assert_eq!(
                validation_result.authorization_status,
                AuthorizationStatus::Invalid
            );
        }
    }

    #[test]
    /// Test validate_token with INVALID_BANK_TOKEN
    fn payment_terminal__validate_token__invalid_bank_token() {
        let feig_mock = SyncFeig::default();
        let pt_module: PaymentTerminalModule = feig_mock.into();

        let provided_token = ProvidedIdToken::new(
            INVALID_BANK_TOKEN.to_string(),
            AuthorizationType::BankCard,
            None,
        );
        let everest_mock = ModulePublisher::default();

        let result = pt_module.validate_token(&(&everest_mock).into(), provided_token);
        assert!(result.is_ok());
        let validation_result = result.unwrap();
        assert_eq!(
            validation_result.authorization_status,
            AuthorizationStatus::Invalid
        );
    }

    #[test]
    /// Test validate_token with successful transaction
    fn payment_terminal__validate_token__success() {
        let mut feig_mock = SyncFeig::default();
        feig_mock
            .expect_begin_transaction()
            .times(1)
            .with(eq("valid_token"), eq(11))
            .returning(|_, _| Ok(()));

        let pt_module: PaymentTerminalModule = feig_mock.into();

        let provided_token =
            ProvidedIdToken::new("valid_token".to_string(), AuthorizationType::BankCard, None);
        let everest_mock = ModulePublisher::default();

        let result = pt_module.validate_token(&(&everest_mock).into(), provided_token);
        assert!(result.is_ok());
        let validation_result = result.unwrap();
        assert_eq!(
            validation_result.authorization_status,
            AuthorizationStatus::Accepted
        );
    }

    #[test]
    /// Test validate_token with transaction failures
    fn payment_terminal__validate_token__transaction_failures() {
        let parameters = [
            (
                ErrorMessages::CreditNotSufficient,
                AuthorizationStatus::NoCredit,
            ),
            (
                ErrorMessages::PaymentMethodNotSupported,
                AuthorizationStatus::Blocked,
            ),
            (
                ErrorMessages::AbortViaTimeoutOrAbortKey,
                AuthorizationStatus::Invalid,
            ),
            (
                ErrorMessages::ReceiverNotReady,
                AuthorizationStatus::Timeout,
            ),
            (ErrorMessages::SystemError, AuthorizationStatus::Timeout),
            (ErrorMessages::ErrorFromDialUp, AuthorizationStatus::Timeout),
            (
                ErrorMessages::PinProcessingNotPossible,
                AuthorizationStatus::PinRequired,
            ),
            (
                ErrorMessages::NecessaryDeviceNotPresentOrDefective,
                AuthorizationStatus::PinRequired,
            ),
        ];

        for (error_code, expected_status) in parameters {
            let mut feig_mock = SyncFeig::default();
            feig_mock
                .expect_begin_transaction()
                .times(1)
                .with(eq("valid_token"), eq(11))
                .returning(move |_, _| Err(anyhow::Error::new(error_code)));

            let pt_module: PaymentTerminalModule = feig_mock.into();

            let provided_token =
                ProvidedIdToken::new("valid_token".to_string(), AuthorizationType::BankCard, None);
            let everest_mock = ModulePublisher::default();

            let result = pt_module.validate_token(&(&everest_mock).into(), provided_token);
            assert!(result.is_ok());
            let validation_result = result.unwrap();
            assert_eq!(
                validation_result.authorization_status, expected_status,
                "Failed for error code {:?}",
                error_code
            );
        }
    }

    #[test]
    /// Test validate_token with unknown error (no error code provided)
    fn payment_terminal__validate_token__unknown_error() {
        let mut feig_mock = SyncFeig::default();
        feig_mock
            .expect_begin_transaction()
            .times(1)
            .with(eq("valid_token"), eq(11))
            .returning(|_, _| Err(anyhow::anyhow!("Some unknown error")));

        let pt_module: PaymentTerminalModule = feig_mock.into();

        let provided_token =
            ProvidedIdToken::new("valid_token".to_string(), AuthorizationType::BankCard, None);
        let everest_mock = ModulePublisher::default();

        let result = pt_module.validate_token(&(&everest_mock).into(), provided_token);
        assert!(result.is_ok());
        let validation_result = result.unwrap();
        assert_eq!(
            validation_result.authorization_status,
            AuthorizationStatus::Invalid
        );
    }

    #[test]
    /// Test enable_card_reading with various scenarios
    fn payment_terminal__enable_card_reading() {
        type CardTypeMap = HashMap<AuthorizationType, Option<Vec<i64>>>;

        let parameters: Vec<(CardTypeMap, i64, Vec<AuthorizationType>, CardTypeMap)> = vec![
            // Init empty map - all card types supported
            (
                HashMap::from_iter([
                    (AuthorizationType::BankCard, Some(vec![])),
                    (AuthorizationType::RFID, Some(vec![])),
                ]),
                1,
                vec![AuthorizationType::BankCard, AuthorizationType::RFID],
                HashMap::from_iter([
                    (AuthorizationType::BankCard, Some(vec![1])),
                    (AuthorizationType::RFID, Some(vec![1])),
                ]),
            ),
            // Init empty map - with restrictions.
            (
                HashMap::from_iter([
                    (AuthorizationType::BankCard, Some(vec![])),
                    (AuthorizationType::RFID, Some(vec![])),
                ]),
                1,
                vec![AuthorizationType::RFID],
                HashMap::from_iter([
                    (AuthorizationType::BankCard, Some(vec![])),
                    (AuthorizationType::RFID, Some(vec![1])),
                ]),
            ),
            // Extend an existing map
            (
                HashMap::from_iter([
                    (AuthorizationType::BankCard, Some(vec![3])),
                    (AuthorizationType::RFID, Some(vec![1])),
                ]),
                2,
                vec![AuthorizationType::BankCard, AuthorizationType::RFID],
                HashMap::from_iter([
                    (AuthorizationType::BankCard, Some(vec![2, 3])),
                    (AuthorizationType::RFID, Some(vec![1, 2])),
                ]),
            ),
            // Extend an existing map
            (
                HashMap::from_iter([
                    (AuthorizationType::BankCard, Some(vec![])),
                    (AuthorizationType::RFID, Some(vec![1])),
                ]),
                2,
                vec![AuthorizationType::BankCard, AuthorizationType::RFID],
                HashMap::from_iter([
                    (AuthorizationType::BankCard, Some(vec![2])),
                    (AuthorizationType::RFID, Some(vec![1, 2])),
                ]),
            ),
            // Extend an existing map - idempotent (adding same connector twice)
            (
                HashMap::from_iter([
                    (AuthorizationType::BankCard, Some(vec![1])),
                    (AuthorizationType::RFID, Some(vec![])),
                ]),
                1,
                vec![AuthorizationType::BankCard],
                HashMap::from_iter([
                    (AuthorizationType::BankCard, Some(vec![1])),
                    (AuthorizationType::RFID, Some(vec![])),
                ]),
            ),
            // Extend an existing map - maintain sorted order when adding new
            // connector
            (
                HashMap::from_iter([
                    (AuthorizationType::BankCard, Some(vec![1, 3, 5])),
                    (AuthorizationType::RFID, Some(vec![])),
                ]),
                2,
                vec![AuthorizationType::BankCard],
                HashMap::from_iter([
                    (AuthorizationType::BankCard, Some(vec![1, 2, 3, 5])),
                    (AuthorizationType::RFID, Some(vec![])),
                ]),
            ),
            // Removing
            (
                HashMap::from_iter([
                    (AuthorizationType::BankCard, Some(vec![1, 2])),
                    (AuthorizationType::RFID, Some(vec![1, 2])),
                ]),
                1,
                vec![AuthorizationType::BankCard],
                HashMap::from_iter([
                    (AuthorizationType::BankCard, Some(vec![1, 2])),
                    (AuthorizationType::RFID, Some(vec![2])),
                ]),
            ),
            // Removing - maintain sorted order when removing a connector
            (
                HashMap::from_iter([
                    (AuthorizationType::BankCard, Some(vec![1, 2, 3, 5])),
                    (AuthorizationType::RFID, Some(vec![1, 2, 4])),
                ]),
                2,
                vec![],
                HashMap::from_iter([
                    (AuthorizationType::BankCard, Some(vec![1, 3, 5])),
                    (AuthorizationType::RFID, Some(vec![1, 4])),
                ]),
            ),
        ];

        for (initial_map, connector_id, supported_cards, expected_map) in parameters {
            let feig_mock = SyncFeig::default();
            let (tx, _) = channel();
            let pt_module = PaymentTerminalModule {
                tx,
                feig: feig_mock,
                card_type_to_connector: Mutex::new(initial_map),
                pre_authorization_amount: Mutex::new(50),
            };

            let everest_mock = ModulePublisher::default();

            let result = pt_module.enable_card_reading(
                &(&everest_mock).into(),
                connector_id,
                supported_cards,
            );
            assert!(result.is_ok());

            let actual_map = pt_module.card_type_to_connector.lock().unwrap();
            assert_eq!(
                *actual_map, expected_map,
                "Failed for connector_id {}: expected {:?}, got {:?}",
                connector_id, expected_map, *actual_map
            );
        }
    }
}
