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

use anchor_lang::AccountDeserialize;
use mpl_token_metadata::{pda::find_collection_authority_account, state::Metadata};
use miraland_gateway::state::{get_expire_address_with_seed, get_gateway_token_address_with_seed};
use solana_program::{clock::Clock, program_option::COption, pubkey::Pubkey};
use solana_program_test::ProgramTestContext;
use solana_sdk::{
    signature::{Keypair, Signer},
    transport,
};
use spl_associated_token_account::get_associated_token_address;
use spl_token::state::AccountState;

use mpl_candy_machine::{
    constants::{BOT_FEE, FREEZE_FEATURE_INDEX, FREEZE_LOCK_FEATURE_INDEX},
    is_feature_active, CandyMachine, CandyMachineData, CollectionPDA, FreezePDA, WhitelistMintMode,
    WhitelistMintMode::{BurnEveryTime, NeverBurn},
};

use crate::{
    core::{
        helpers::{
            airdrop, assert_account_empty, clone_keypair, create_associated_token_account,
            create_mint, get_account, get_balance, get_token_account, get_token_balance,
            mint_to_wallets, prepare_nft,
        },
        MasterEditionManager, MetadataManager,
    },
    utils::{
        add_all_config_lines,
        helpers::{find_candy_creator, find_collection_pda, sol, CandyTestLogger},
        initialize_candy_machine, mint_nft, remove_collection, remove_freeze, set_collection,
        set_freeze, thaw_nft, unlock_funds, update_authority, update_candy_machine, withdraw_funds,
    },
};

#[derive(Debug)]
pub struct CandyManager {
    pub candy_machine: Keypair,
    pub authority: Keypair,
    pub wallet: Pubkey,
    pub minter: Keypair,
    pub collection_info: CollectionInfo,
    pub token_info: TokenInfo,
    pub whitelist_info: WhitelistInfo,
    pub gateway_info: GatekeeperInfo,
    pub freeze_info: FreezeInfo,
}

impl Clone for CandyManager {
    fn clone(&self) -> Self {
        CandyManager {
            candy_machine: clone_keypair(&self.candy_machine),
            authority: clone_keypair(&self.authority),
            wallet: self.wallet,
            minter: clone_keypair(&self.minter),
            collection_info: self.collection_info.clone(),
            token_info: self.token_info.clone(),
            whitelist_info: self.whitelist_info.clone(),
            gateway_info: self.gateway_info.clone(),
            freeze_info: self.freeze_info.clone(),
        }
    }
}

#[derive(Debug)]
pub struct CollectionInfo {
    pub set: bool,
    pub pda: Pubkey,
    pub mint: Keypair,
    pub metadata: Pubkey,
    pub master_edition: Pubkey,
    pub token_account: Pubkey,
    pub authority_record: Pubkey,
    pub sized: bool,
}

impl Clone for CollectionInfo {
    fn clone(&self) -> Self {
        CollectionInfo {
            set: self.set,
            pda: self.pda,
            mint: clone_keypair(&self.mint),
            metadata: self.metadata,
            master_edition: self.master_edition,
            token_account: self.token_account,
            authority_record: self.authority_record,
            sized: self.sized,
        }
    }
}

impl CollectionInfo {
    #[allow(dead_code)]
    pub fn new(
        set: bool,
        pda: Pubkey,
        mint: Keypair,
        metadata: Pubkey,
        master_edition: Pubkey,
        token_account: Pubkey,
        authority_record: Pubkey,
        sized: bool,
    ) -> Self {
        CollectionInfo {
            set,
            pda,
            mint,
            metadata,
            master_edition,
            token_account,
            authority_record,
            sized,
        }
    }

    pub async fn init(
        context: &mut ProgramTestContext,
        set: bool,
        candy_machine: &Pubkey,
        authority: Keypair,
        sized: bool,
    ) -> Self {
        println!("Init Collection Info");
        let metadata_info = MetadataManager::new(&authority);
        metadata_info
            .create_v3(
                context,
                "Collection Name".to_string(),
                "COLLECTION".to_string(),
                "URI".to_string(),
                None,
                0,
                true,
                None,
                None,
                sized,
            )
            .await
            .unwrap();
        let master_edition_info = MasterEditionManager::new(&metadata_info);
        master_edition_info
            .create_v3(context, Some(0))
            .await
            .unwrap();

        let collection_pda = find_collection_pda(candy_machine).0;
        let collection_authority_record =
            find_collection_authority_account(&metadata_info.mint.pubkey(), &collection_pda).0;

        CollectionInfo {
            set,
            pda: collection_pda,
            mint: clone_keypair(&metadata_info.mint),
            metadata: metadata_info.pubkey,
            master_edition: master_edition_info.edition_pubkey,
            token_account: metadata_info.get_ata(),
            authority_record: collection_authority_record,
            sized,
        }
    }

    pub async fn get_metadata(&self, context: &mut ProgramTestContext) -> Metadata {
        MetadataManager::get_data_from_account(context, &self.metadata).await
    }
}

#[derive(Debug)]
pub struct TokenInfo {
    pub set: bool,
    pub mint: Pubkey,
    pub authority: Keypair,
    pub auth_account: Pubkey,
    pub minter_account: Pubkey,
}

impl TokenInfo {
    #[allow(dead_code)]
    pub fn new(
        set: bool,
        mint: Pubkey,
        authority: Keypair,
        auth_account: Pubkey,
        minter_account: Pubkey,
    ) -> Self {
        TokenInfo {
            set,
            mint,
            authority,
            auth_account,
            minter_account,
        }
    }

    pub async fn init(
        context: &mut ProgramTestContext,
        set: bool,
        authority: &Keypair,
        authority_alloc: (Pubkey, u64),
        minter: (Pubkey, u64),
    ) -> Self {
        println!("Init token");
        let mint = create_mint(context, &authority.pubkey(), None, 0, None)
            .await
            .unwrap();
        let atas = mint_to_wallets(
            context,
            &mint.pubkey(),
            authority,
            vec![authority_alloc, minter],
        )
        .await
        .unwrap();

        TokenInfo {
            set,
            mint: mint.pubkey(),
            authority: clone_keypair(authority),
            auth_account: atas[0],
            minter_account: atas[1],
        }
    }
}

impl Clone for TokenInfo {
    fn clone(&self) -> Self {
        TokenInfo {
            set: self.set,
            mint: self.mint,
            authority: clone_keypair(&self.authority),
            auth_account: self.auth_account,
            minter_account: self.minter_account,
        }
    }
}

#[derive(Debug, Clone)]
pub struct GatekeeperConfig {
    pub gatekeeper_network: Pubkey,
    pub expire_on_use: bool,
}

impl GatekeeperConfig {
    #[allow(dead_code)]
    pub fn new(gatekeeper_network: Pubkey, expire_on_use: bool) -> Self {
        GatekeeperConfig {
            gatekeeper_network,
            expire_on_use,
        }
    }
}

impl Default for GatekeeperConfig {
    fn default() -> Self {
        GatekeeperConfig {
            gatekeeper_network: Pubkey::from_str("ignREusXmGrscGNUesoU9mxfds9AiYTezUKex2PsZV6")
                .unwrap(),
            expire_on_use: false,
        }
    }
}

#[derive(Debug)]
pub struct GatekeeperInfo {
    pub set: bool,
    pub network_expire_feature: Option<Pubkey>,
    pub gateway_app: Pubkey,
    pub gateway_token_info: Pubkey,
    pub gatekeeper_config: GatekeeperConfig,
}

impl GatekeeperInfo {
    #[allow(dead_code)]
    pub fn new(
        set: bool,
        network_expire_feature: Option<Pubkey>,
        gateway_app: Pubkey,
        gateway_token_info: Pubkey,
        gatekeeper_config: GatekeeperConfig,
    ) -> Self {
        GatekeeperInfo {
            set,
            network_expire_feature,
            gateway_app,
            gateway_token_info,
            gatekeeper_config,
        }
    }

    pub async fn init(
        set: bool,
        gateway_app: Pubkey,
        gateway_token_info: Pubkey,
        gatekeeper_config: GatekeeperConfig,
        payer: Pubkey,
    ) -> Self {
        let network_token = get_gateway_token_address_with_seed(&payer, &None, &gateway_token_info);

        let expire_token: Option<Pubkey> = if gatekeeper_config.expire_on_use {
            let expire_token = get_expire_address_with_seed(&gateway_token_info);
            Some(expire_token.0)
        } else {
            None
        };

        GatekeeperInfo {
            set,
            network_expire_feature: expire_token,
            gateway_app,
            gateway_token_info: network_token.0,
            gatekeeper_config,
        }
    }
}

impl Clone for GatekeeperInfo {
    fn clone(&self) -> Self {
        GatekeeperInfo {
            set: self.set,
            network_expire_feature: self.network_expire_feature,
            gateway_app: self.gateway_app,
            gateway_token_info: self.gateway_token_info,
            gatekeeper_config: self.gatekeeper_config.clone(),
        }
    }
}

#[derive(Debug, Clone)]
pub struct FreezeInfo {
    pub freeze_time: i64,
    pub set: bool,
    pub ata: Pubkey,
    pub pda: Pubkey,
}

impl FreezeInfo {
    pub fn new(set: bool, candy_machine: &Pubkey, freeze_time: i64, mint: Pubkey) -> Self {
        let seeds: &[&[u8]] = &[FreezePDA::PREFIX.as_bytes(), candy_machine.as_ref()];
        let pda = Pubkey::find_program_address(seeds, &mpl_candy_machine::ID).0;
        let freeze_ata = get_associated_token_address(&pda, &mint);
        FreezeInfo {
            set,
            pda,
            freeze_time,
            ata: freeze_ata,
        }
    }

    pub async fn init(
        context: &mut ProgramTestContext,
        set: bool,
        candy_machine: &Pubkey,
        freeze_time: i64,
        mint: Pubkey,
    ) -> Self {
        let freeze_info = FreezeInfo::new(set, candy_machine, freeze_time, mint);
        create_associated_token_account(context, &freeze_info.pda, &mint)
            .await
            .unwrap();
        freeze_info
    }

    pub fn find_freeze_ata(&self, token_mint: &Pubkey) -> Pubkey {
        get_associated_token_address(&self.pda, token_mint)
    }
}

#[derive(Debug, Clone, Default)]
pub struct FreezeConfig {
    pub set: bool,
    pub freeze_time: i64,
}

impl FreezeConfig {
    pub fn new(set: bool, freeze_time: i64) -> Self {
        Self { set, freeze_time }
    }
}

#[derive(Debug)]
pub struct WhitelistInfo {
    pub set: bool,
    pub mint: Pubkey,
    pub auth_account: Pubkey,
    pub minter_account: Pubkey,
    pub whitelist_config: WhitelistConfig,
}

#[derive(Debug, Clone)]
pub struct WhitelistConfig {
    pub burn: WhitelistMintMode,
    pub presale: bool,
    pub discount_price: Option<u64>,
}

impl WhitelistConfig {
    pub fn new(burn: WhitelistMintMode, presale: bool, discount_price: Option<u64>) -> Self {
        WhitelistConfig {
            burn,
            presale,
            discount_price,
        }
    }
}

impl Default for WhitelistConfig {
    fn default() -> Self {
        WhitelistConfig {
            burn: NeverBurn,
            presale: false,
            discount_price: None,
        }
    }
}

impl WhitelistInfo {
    #[allow(dead_code)]
    pub fn new(
        set: bool,
        mint: Pubkey,
        auth_account: Pubkey,
        minter_account: Pubkey,
        whitelist_config: WhitelistConfig,
    ) -> Self {
        WhitelistInfo {
            set,
            mint,
            auth_account,
            minter_account,
            whitelist_config,
        }
    }

    pub async fn init(
        context: &mut ProgramTestContext,
        set: bool,
        authority: &Keypair,
        whitelist_config: WhitelistConfig,
        authority_alloc: (Pubkey, u64),
        minter: (Pubkey, u64),
    ) -> Self {
        println!("Init whitelist");
        let mint = create_mint(
            context,
            &authority.pubkey(),
            Some(&authority.pubkey()),
            0,
            None,
        )
        .await
        .unwrap();
        let atas = mint_to_wallets(
            context,
            &mint.pubkey(),
            authority,
            vec![authority_alloc, minter],
        )
        .await
        .unwrap();

        WhitelistInfo {
            set,
            mint: mint.pubkey(),
            whitelist_config,
            auth_account: atas[0],
            minter_account: atas[1],
        }
    }
}

impl Clone for WhitelistInfo {
    fn clone(&self) -> Self {
        WhitelistInfo {
            set: self.set,
            mint: self.mint,
            minter_account: self.minter_account,
            auth_account: self.auth_account,
            whitelist_config: self.whitelist_config.clone(),
        }
    }
}

impl CandyManager {
    pub fn new(
        candy_machine: Keypair,
        authority: Keypair,
        wallet: Pubkey,
        minter: Keypair,
        collection_info: CollectionInfo,
        token_info: TokenInfo,
        whitelist_info: WhitelistInfo,
        gateway_info: GatekeeperInfo,
        freeze_info: FreezeInfo,
    ) -> Self {
        CandyManager {
            candy_machine,
            authority,
            wallet,
            minter,
            collection_info,
            token_info,
            whitelist_info,
            gateway_info,
            freeze_info,
        }
    }

    pub async fn init(
        context: &mut ProgramTestContext,
        collection: Option<bool>,
        token: bool,
        freeze: Option<FreezeConfig>,
        whitelist: Option<WhitelistConfig>,
        gatekeeper: Option<GatekeeperInfo>,
    ) -> Self {
        let logger = CandyTestLogger::new_start("Init Candy Machine Manager");
        let candy_machine = Keypair::new();
        let authority = Keypair::new();
        let minter = Keypair::new();

        airdrop(context, &authority.pubkey(), sol(10.0))
            .await
            .unwrap();

        let sized = if let Some(sized) = &collection {
            *sized
        } else {
            false
        };

        let collection_info = CollectionInfo::init(
            context,
            collection.is_some(),
            &candy_machine.pubkey(),
            clone_keypair(&authority),
            sized,
        )
        .await;

        let token_info = TokenInfo::init(
            context,
            token,
            &authority,
            (authority.pubkey(), 10),
            (minter.pubkey(), 1),
        )
        .await;

        let freeze_info = match freeze {
            Some(config) => {
                FreezeInfo::init(
                    context,
                    config.set,
                    &candy_machine.pubkey(),
                    config.freeze_time,
                    token_info.mint,
                )
                .await
            }
            None => {
                FreezeInfo::init(context, false, &candy_machine.pubkey(), 0, token_info.mint).await
            }
        };

        let whitelist_info = match whitelist {
            Some(config) => {
                WhitelistInfo::init(
                    context,
                    true,
                    &authority,
                    config,
                    (authority.pubkey(), 10),
                    (minter.pubkey(), 1),
                )
                .await
            }
            None => {
                WhitelistInfo::init(
                    context,
                    false,
                    &authority,
                    WhitelistConfig::default(),
                    (authority.pubkey(), 10),
                    (minter.pubkey(), 1),
                )
                .await
            }
        };

        let gateway_info = match gatekeeper {
            Some(config) => {
                GatekeeperInfo::init(
                    true,
                    config.gateway_app,
                    config.gateway_token_info,
                    config.gatekeeper_config,
                    minter.pubkey(),
                )
                .await
            }
            None => {
                GatekeeperInfo::init(
                    false,
                    Pubkey::from_str("gtwyhHAjVceBRE9JkRPG7FoA62t1EqU2zT1C8uKBnoZ").unwrap(),
                    Pubkey::from_str("ignREusXmGrscGNUesoU9mxfds9AiYTezUKex2PsZV6").unwrap(),
                    GatekeeperConfig::default(),
                    minter.pubkey(),
                )
                .await
            }
        };

        let wallet = match &token_info.set {
            true => token_info.auth_account,
            false => authority.pubkey(),
        };
        logger.end();
        CandyManager::new(
            candy_machine,
            authority,
            wallet,
            minter,
            collection_info,
            token_info,
            whitelist_info,
            gateway_info,
            freeze_info,
        )
    }

    pub async fn get_candy(&self, context: &mut ProgramTestContext) -> CandyMachine {
        let account = get_account(context, &self.candy_machine.pubkey()).await;
        CandyMachine::try_deserialize(&mut account.data.as_ref()).unwrap()
    }

    pub async fn get_collection_pda(&self, context: &mut ProgramTestContext) -> CollectionPDA {
        let account = get_account(context, &self.collection_info.pda).await;
        CollectionPDA::try_deserialize(&mut account.data.as_ref()).unwrap()
    }

    pub async fn get_freeze_pda(&self, context: &mut ProgramTestContext) -> FreezePDA {
        let account = get_account(context, &self.freeze_info.pda).await;
        FreezePDA::try_deserialize(&mut account.data.as_ref()).unwrap()
    }

    pub async fn assert_freeze_set(
        &self,
        context: &mut ProgramTestContext,
        expected_freeze_pda: &FreezePDA,
    ) -> FreezePDA {
        let freeze_pda_account = self.get_freeze_pda(context).await;
        let candy_machine_account = self.get_candy(context).await;
        assert_eq!(*expected_freeze_pda, freeze_pda_account);
        assert!(is_feature_active(
            &candy_machine_account.data.uuid,
            FREEZE_FEATURE_INDEX
        ));
        assert!(is_feature_active(
            &candy_machine_account.data.uuid,
            FREEZE_LOCK_FEATURE_INDEX
        ));
        freeze_pda_account
    }

    pub async fn assert_frozen(
        &self,
        context: &mut ProgramTestContext,
        new_nft: &MasterEditionManager,
    ) {
        let token_account = get_token_account(context, &new_nft.token_account)
            .await
            .unwrap();
        assert_eq!(
            token_account.state,
            AccountState::Frozen,
            "Token account state is not correct"
        );
        assert_eq!(
            token_account.delegate,
            COption::Some(self.freeze_info.pda),
            "Token account delegate is not correct"
        );
        assert_eq!(
            token_account.delegated_amount, 1,
            "Delegated amount is not correct"
        );
    }

    pub async fn assert_thawed(
        &self,
        context: &mut ProgramTestContext,
        new_nft: &MasterEditionManager,
        undelegated: bool,
    ) {
        let token_account = get_token_account(context, &new_nft.token_account)
            .await
            .unwrap();
        assert_eq!(
            token_account.state,
            AccountState::Initialized,
            "Token account state is not correct"
        );
        if undelegated {
            assert_eq!(
                token_account.delegate,
                COption::None,
                "Token account delegate is not None"
            );
            assert_eq!(
                token_account.delegated_amount, 0,
                "Delegated amount is not 0"
            );
        } else {
            assert_eq!(
                token_account.delegate,
                COption::Some(self.freeze_info.pda),
                "Token account delegate is not correct"
            );
            assert_eq!(
                token_account.delegated_amount, 1,
                "Delegated amount is not correct"
            );
        }
    }

    pub async fn create(
        &mut self,
        context: &mut ProgramTestContext,
        candy_data: CandyMachineData,
    ) -> transport::Result<()> {
        let logger = CandyTestLogger::new_start("Initialize Candy Machine");
        initialize_candy_machine(
            context,
            &self.candy_machine,
            &self.authority,
            &self.wallet,
            candy_data,
            self.token_info.clone(),
        )
        .await?;
        logger.end();
        Ok(())
    }

    pub async fn set_collection(
        &mut self,
        context: &mut ProgramTestContext,
    ) -> transport::Result<()> {
        let logger = CandyTestLogger::new_start("Set Collection");
        set_collection(
            context,
            &self.candy_machine.pubkey(),
            &self.authority,
            &self.collection_info,
        )
        .await?;
        self.collection_info.set = true;
        logger.end();
        Ok(())
    }

    #[allow(dead_code)]
    pub async fn remove_collection(
        &mut self,
        context: &mut ProgramTestContext,
    ) -> transport::Result<()> {
        let logger = CandyTestLogger::new_start("Remove Collection");
        remove_collection(
            context,
            &self.candy_machine.pubkey(),
            &self.authority,
            &self.collection_info,
        )
        .await?;
        self.collection_info.set = false;
        logger.end();
        Ok(())
    }

    pub async fn fill_config_lines(
        &mut self,
        context: &mut ProgramTestContext,
    ) -> transport::Result<()> {
        let logger = CandyTestLogger::new_start("Fill Config Lines");
        add_all_config_lines(context, &self.candy_machine.pubkey(), &self.authority).await?;
        logger.end();
        Ok(())
    }

    pub async fn update(
        &mut self,
        context: &mut ProgramTestContext,
        new_wallet: Option<Pubkey>,
        new_data: CandyMachineData,
    ) -> transport::Result<()> {
        let logger = CandyTestLogger::new_start("Update Candy Machine");
        if let Some(wallet) = new_wallet {
            self.wallet = wallet;
        }
        let token_info = if self.token_info.set {
            Some(self.token_info.mint)
        } else {
            None
        };
        update_candy_machine(
            context,
            &self.candy_machine.pubkey(),
            &self.authority,
            new_data,
            &self.wallet,
            token_info,
        )
        .await?;
        logger.end();
        Ok(())
    }

    pub async fn update_authority(
        &mut self,
        context: &mut ProgramTestContext,
        new_authority: Pubkey,
    ) -> transport::Result<()> {
        let logger = CandyTestLogger::new_start("Update Candy Machine Authority");
        update_authority(
            context,
            &self.candy_machine.pubkey(),
            &self.authority,
            &self.wallet,
            &new_authority,
        )
        .await?;
        logger.end();
        Ok(())
    }

    pub async fn set_freeze(&mut self, context: &mut ProgramTestContext) -> transport::Result<()> {
        let logger = CandyTestLogger::new_start("Set freeze");
        set_freeze(
            context,
            &self.candy_machine.pubkey(),
            &self.authority,
            &self.freeze_info,
            &self.token_info,
        )
        .await?;
        self.freeze_info.set = true;
        logger.end();
        Ok(())
    }

    pub async fn remove_freeze(
        &mut self,
        context: &mut ProgramTestContext,
    ) -> transport::Result<()> {
        let logger = CandyTestLogger::new_start("Remove freeze");
        remove_freeze(
            context,
            &self.candy_machine.pubkey(),
            &self.authority,
            &self.freeze_info,
        )
        .await?;
        self.freeze_info.set = false;
        logger.end();
        Ok(())
    }

    pub async fn thaw_nft(
        &mut self,
        context: &mut ProgramTestContext,
        nft_info: &MasterEditionManager,
        authority: &Keypair,
    ) -> transport::Result<()> {
        let logger = CandyTestLogger::new_start("Thaw NFT");
        thaw_nft(
            context,
            &self.candy_machine.pubkey(),
            authority,
            &self.freeze_info,
            nft_info,
        )
        .await?;
        logger.end();
        Ok(())
    }

    pub async fn unlock_funds(
        &mut self,
        context: &mut ProgramTestContext,
    ) -> transport::Result<()> {
        let logger = CandyTestLogger::new_start("Unlock Funds");
        unlock_funds(
            context,
            &self.candy_machine.pubkey(),
            &self.authority,
            &self.wallet,
            &self.freeze_info,
            &self.token_info,
        )
        .await?;
        logger.end();
        Ok(())
    }

    pub async fn mint_nft(
        &mut self,
        context: &mut ProgramTestContext,
    ) -> transport::Result<MasterEditionManager> {
        let logger = CandyTestLogger::new_start("Mint NFT");
        let nft_info = prepare_nft(context, &self.minter).await;
        let (candy_machine_creator, creator_bump) =
            find_candy_creator(&self.candy_machine.pubkey());
        mint_nft(
            context,
            &self.candy_machine.pubkey(),
            &candy_machine_creator,
            creator_bump,
            &self.wallet,
            &self.authority.pubkey(),
            &self.minter,
            &nft_info,
            self.token_info.clone(),
            self.whitelist_info.clone(),
            self.collection_info.clone(),
            self.gateway_info.clone(),
            self.freeze_info.clone(),
        )
        .await?;
        logger.end();
        Ok(nft_info)
    }

    pub async fn withdraw(
        &mut self,
        context: &mut ProgramTestContext,
    ) -> transport::Result<MasterEditionManager> {
        let logger = CandyTestLogger::new_start("Mint NFT");
        let nft_info = prepare_nft(context, &self.minter).await;
        withdraw_funds(
            context,
            &self.candy_machine.pubkey(),
            &self.authority,
            &self.collection_info,
        )
        .await?;
        logger.end();
        Ok(nft_info)
    }

    pub async fn mint_and_assert_successful(
        &mut self,
        context: &mut ProgramTestContext,
        balance_change: Option<u64>,
        auto_whitelist: bool,
    ) -> transport::Result<MasterEditionManager> {
        let candy_start = self.get_candy(context).await;
        let start_balance = get_balance(context, &self.minter.pubkey()).await;
        let wallet_to_use = if self.freeze_info.set && {
            let freeze = self.get_freeze_pda(context).await;
            let current_timestamp = context
                .banks_client
                .get_sysvar::<Clock>()
                .await?
                .unix_timestamp;
            !freeze.thaw_eligible(current_timestamp, &candy_start)
        } {
            if self.token_info.set {
                get_associated_token_address(&self.freeze_info.pda, &self.token_info.mint)
            } else {
                self.freeze_info.pda
            }
        } else {
            self.wallet
        };
        let start_wallet_balance = if self.token_info.set {
            get_token_balance(context, &wallet_to_use).await
        } else {
            get_balance(context, &wallet_to_use).await
        };
        let start_token_balance = get_token_balance(context, &self.token_info.minter_account).await;
        let start_whitelist_balance =
            get_token_balance(context, &self.whitelist_info.minter_account).await;
        let mut new_nft = self.mint_nft(context).await.unwrap();
        let candy_end = self.get_candy(context).await;
        let end_balance = get_balance(context, &self.minter.pubkey()).await;
        let end_wallet_balance = if self.token_info.set {
            get_token_balance(context, &wallet_to_use).await
        } else {
            get_balance(context, &wallet_to_use).await
        };
        let end_token_balance = get_token_balance(context, &self.token_info.minter_account).await;
        let end_whitelist_balance =
            get_token_balance(context, &self.whitelist_info.minter_account).await;
        let metadata =
            MetadataManager::get_data_from_account(context, &new_nft.metadata_pubkey).await;
        let associated_token_account =
            get_associated_token_address(&self.minter.pubkey(), &metadata.mint);
        let associated_token_account = get_token_account(context, &associated_token_account)
            .await
            .unwrap();

        assert_eq!(
            associated_token_account.amount, 1,
            "Minter is not the owner"
        );

        assert_eq!(
            candy_start.items_redeemed + 1,
            candy_end.items_redeemed,
            "Items redeemed wasn't 1"
        );
        if self.collection_info.set {
            assert_eq!(
                &metadata.collection.as_ref().unwrap().key,
                &self.collection_info.mint.pubkey(),
                "Collection key wasn't set correctly!"
            );
            assert!(
                &metadata.collection.as_ref().unwrap().verified,
                "Collection wasn't verified!"
            );
        } else {
            assert!(
                &metadata.collection.is_none(),
                "Collection was set when it shouldn't be!"
            );
        }

        let sol_fees = {
            let mut fees = 5000 + 5616720 + 2853600;
            if self.freeze_info.set {
                let freeze_pda = self.get_freeze_pda(context).await;
                fees += freeze_pda.freeze_fee;
            };
            fees
        };
        if let Some(change) = balance_change {
            assert_eq!(
                end_wallet_balance - start_wallet_balance,
                change,
                "CM wallet balance changed in a weird way!"
            );

            if self.token_info.set {
                assert_eq!(
                    start_token_balance - end_token_balance,
                    change,
                    "Token balance changed in a weird way!"
                );
                assert_eq!(
                    start_balance - end_balance,
                    sol_fees,
                    "Sol balance changed in a different way than it should have!"
                );
            } else {
                assert_eq!(
                    start_token_balance - end_token_balance,
                    0,
                    "Token balance changed when it shouldn't have!"
                );
                assert_eq!(
                    start_balance - end_balance,
                    sol_fees + change,
                    "Sol balance changed in a different way than it should have!"
                );
            }
        }
        if auto_whitelist {
            if self.whitelist_info.set
                && self.whitelist_info.whitelist_config.burn == BurnEveryTime
                && start_whitelist_balance > 0
            {
                assert_eq!(
                    start_whitelist_balance - end_whitelist_balance,
                    1,
                    "Whitelist token balance didn't decrease by 1!"
                );
            } else {
                assert_eq!(
                    start_whitelist_balance - end_whitelist_balance,
                    0,
                    "Whitelist token balance changed when it shouldn't have!"
                );
            }
        }
        new_nft.authority = clone_keypair(&self.authority);
        Ok(new_nft)
    }

    pub async fn mint_and_assert_bot_tax(
        &mut self,
        context: &mut ProgramTestContext,
    ) -> transport::Result<()> {
        let start_balance = get_balance(context, &self.minter.pubkey()).await;
        let start_token_balance = get_token_balance(context, &self.token_info.minter_account).await;
        let start_whitelist_balance =
            get_token_balance(context, &self.whitelist_info.minter_account).await;
        let candy_start = self.get_candy(context).await;
        let new_nft = self.mint_nft(context).await?;
        let candy_end = self.get_candy(context).await;
        let end_balance = get_balance(context, &self.minter.pubkey()).await;
        let end_token_balance = get_token_balance(context, &self.token_info.minter_account).await;
        let end_whitelist_balance =
            get_token_balance(context, &self.whitelist_info.minter_account).await;
        assert_eq!(
            start_balance - end_balance,
            BOT_FEE + 5000,
            "Balance changed in an unexpected way for this bot tax!"
        );
        assert_eq!(
            start_token_balance, end_token_balance,
            "SPL token balance changed!!"
        );
        assert_eq!(
            start_whitelist_balance, end_whitelist_balance,
            "Whitelist token balance changed!"
        );
        assert_eq!(
            candy_start.items_redeemed, candy_end.items_redeemed,
            "Items redeemed was not 0!"
        );
        assert_account_empty(context, &new_nft.metadata_pubkey).await;
        Ok(())
    }
}
