mod program_test;
use {
    bincode::deserialize,
    mpl_token_metadata::accounts::{MasterEdition, Metadata},
    program_test::StablebondProgramTest,
    solana_program::{instruction::Instruction, system_instruction},
    solana_program_test::{tokio, ProgramTestContext},
    solana_sdk::{
        clock::{Clock, UnixTimestamp},
        compute_budget::ComputeBudgetInstruction,
        instruction::InstructionError,
        program_pack::Pack,
        pubkey::Pubkey,
        signature::{Keypair, Signer},
        sysvar,
        transaction::{Transaction, TransactionError},
    },
    spl_associated_token_account::{
        get_associated_token_address, get_associated_token_address_with_program_id,
    },
    spl_token_2022::{instruction as token_instruction, state::Multisig},
    stablebond_sdk::{
        accounts::Issuance,
        errors::StablebondError,
        find_bond_multisig_meta_pda, find_bond_pda, find_config_pda, find_delegate_pda,
        find_issuance_pda, find_nft_issuance_vault_pda, find_payment_feed_pda, find_payment_pda,
        find_payout_pda,
        instructions::{
            InitializeBond, InitializeBondInstructionArgs, InitializeConfig,
            InitializeConfigInstructionArgs, InitializeDelegate, InitializeIssuance,
            InitializeIssuanceInstructionArgs, InitializePaymentFeed,
            InitializePaymentFeedInstructionArgs, MigrateMintAuthorityToMultisig, PayoutIssuance,
            PurchaseBond, PurchaseBondInstructionArgs, RequestRedemption,
            RequestRedemptionInstructionArgs, StartIssuance,
        },
        types::{OracleSetup, PaymentFeedType},
    },
    std::{
        borrow::Borrow,
        time::{SystemTime, UNIX_EPOCH},
    },
};

struct ExecuteTestContext {
    context: ProgramTestContext,
    user: Keypair,
    admin: Keypair,
    bond_mint: Keypair,
    usdc_mint: Pubkey,
    payment_base_price_feed_account: Pubkey,
    payment_quote_price_feed_account: Option<Pubkey>,
    nft_collection_mint: Pubkey,
}

impl ExecuteTestContext {
    #[allow(dead_code)]
    pub fn get_current_time() -> i64 {
        let now = SystemTime::now();
        now.duration_since(UNIX_EPOCH).unwrap().as_secs() as i64
    }

    #[allow(dead_code)]
    pub async fn get_bincode_account<T: serde::de::DeserializeOwned>(
        &mut self,
        address: &Pubkey,
    ) -> T {
        self.context
            .banks_client
            .get_account(*address)
            .await
            .unwrap()
            .map(|a| deserialize::<T>(&a.data.borrow()).unwrap())
            .expect(format!("GET-TEST-ACCOUNT-ERROR: Account {}", address).as_str())
    }

    #[allow(dead_code)]
    pub async fn get_clock(&mut self) -> Clock {
        self.get_bincode_account::<Clock>(&sysvar::clock::id())
            .await
    }

    #[allow(dead_code)]
    pub async fn advance_clock_by_slots(&mut self, slots: u64) {
        let mut clock: Clock = self.get_clock().await;
        println!("clock slot before: {}", clock.slot);
        self.context.warp_to_slot(clock.slot + slots).unwrap();
        clock = self.get_clock().await;
        println!("clock slot after: {}", clock.slot);
    }

    #[allow(dead_code)]
    pub async fn advance_clock_past_timestamp(&mut self, unix_timestamp: UnixTimestamp) {
        let mut clock: Clock = self.get_clock().await;
        let mut n = 1;

        while clock.unix_timestamp <= unix_timestamp {
            // Since the exact time is not deterministic keep wrapping by arbitrary 400
            // slots until we pass the requested timestamp
            self.context.warp_to_slot(clock.slot + 400).unwrap();

            n = n + 1;
            clock = self.get_clock().await;
        }
    }
}

async fn setup() -> ExecuteTestContext {
    let pt = StablebondProgramTest::start_new().await;
    let usdc_mint = pt.mints[0].payment_mint;
    let bond_mint = pt.bond_mints[0].insecure_clone();
    let admin = pt.admin;
    let user = pt.users[0].insecure_clone();
    let payment_base_price_feed_account = pt.stub_payment_feed.base_price_feed;
    let payment_quote_price_feed_account = None;
    let nft_collection_mint = pt.nft_collection_mint;

    let initialize_config_ix_args = InitializeConfigInstructionArgs {
        oracle: OracleSetup::Stub,
        usdc_mxn_payment_feed: pt.usdc_mxn_payment_feed.clone(),
        usdc_usd_payment_feed: pt.usdc_usd_payment_feed,
    };

    let initialize_config_ix = InitializeConfig {
        admin_wallet: admin.pubkey(),
        config_account: find_config_pda().0,
        config_token_account: get_associated_token_address(
            &find_config_pda().0,
            &nft_collection_mint.pubkey(),
        ),
        mint_account: nft_collection_mint.pubkey(),
        payment_mint_account: pt.usdc_mxn_payment_feed.payment_mint,
        metadata_account: Metadata::find_pda(&nft_collection_mint.pubkey()).0,
        master_edition_account: MasterEdition::find_pda(&nft_collection_mint.pubkey()).0,
        usdc_mxn_feed_account: find_payment_feed_pda(PaymentFeedType::UsdcMxn).0,
        usdc_usd_feed_account: find_payment_feed_pda(PaymentFeedType::UsdcUsd).0,
        token_program: spl_token::id(),
        associated_token_program: spl_associated_token_account::id(),
        metadata_program: mpl_token_metadata::ID,
        system_program: solana_program::system_program::id(),
    }
    .instruction(initialize_config_ix_args);

    let initialize_delegate_ix = InitializeDelegate {
        config_account: find_config_pda().0,
        admin_wallet: admin.pubkey(),
        delegate_wallet: admin.pubkey(),
        delegate_account: find_delegate_pda(admin.pubkey()).0,
        system_program: solana_program::system_program::id(),
    }
    .instruction();

    let initialize_payment_feed_args = InitializePaymentFeedInstructionArgs {
        payment_feed_info: pt.stub_payment_feed,
    };

    let initialize_payment_feed_ix = InitializePaymentFeed {
        delegate_wallet: admin.pubkey(),
        delegate_account: find_delegate_pda(admin.pubkey()).0,
        payment_feed_account: find_payment_feed_pda(PaymentFeedType::Stub).0,
        payment_mint_account: usdc_mint,
        system_program: solana_program::system_program::id(),
    }
    .instruction(initialize_payment_feed_args);

    let initialize_bond_ix_args = InitializeBondInstructionArgs {
        name: "Test Mint".to_string(),
        symbol: "TM".to_string(),
        uri: "https://test.com".to_string(),
        payment_feed_type: PaymentFeedType::Stub,
        cutoff_in_seconds: 60 * 60 * 24,
    };

    let initialize_bond_ix = InitializeBond {
        delegate_account: find_delegate_pda(admin.pubkey()).0,
        delegate_wallet: admin.pubkey(),
        bond_account: find_bond_pda(bond_mint.pubkey()).0,
        mint_account: bond_mint.pubkey(),
        metadata_account: Metadata::find_pda(&bond_mint.pubkey()).0,
        token2022_program: spl_token_2022::id(),
        metadata_program: mpl_token_metadata::ID,
        sysvar_instructions: solana_program::sysvar::instructions::id(),
        system_program: solana_program::system_program::id(),
    }
    .instruction(initialize_bond_ix_args);

    let current_time = StablebondProgramTest::get_current_time();
    let one_minute_ago: i64 = current_time - 60;
    let two_days_from_now: i64 = 60 * 60 * 24 * 2;

    let initialize_issuance_ix_args = InitializeIssuanceInstructionArgs {
        liquidity: 100_000_000,
        interest_rate_bps: 11_00,
        estimated_start_datetime: one_minute_ago,
        length_in_seconds: two_days_from_now,
    };

    let issuance_number = 1;
    let bond_pda = find_bond_pda(bond_mint.pubkey()).0;
    let issuance_pda = find_issuance_pda(bond_pda, issuance_number).0;
    let payment_pda = find_payment_pda(issuance_pda).0;
    let payout_pda = find_payout_pda(issuance_pda).0;
    let initialize_issuance_ix = InitializeIssuance {
        delegate_wallet: admin.pubkey(),
        delegate_account: find_delegate_pda(admin.pubkey()).0,
        bond_account: find_bond_pda(bond_mint.pubkey()).0,
        upcoming_issuance_account: issuance_pda,
        payment_feed_account: find_payment_feed_pda(PaymentFeedType::Stub).0,
        payment_mint_account: usdc_mint,
        payment_account: payment_pda,
        payment_token_account: get_associated_token_address(&payment_pda, &usdc_mint),
        payout_account: payout_pda,
        payout_token_account: get_associated_token_address(&payout_pda, &usdc_mint),
        associated_token_program: spl_associated_token_account::id(),
        token_program: spl_token::id(),
        system_program: solana_program::system_program::id(),
        current_issuance_account: None,
    }
    .instruction(initialize_issuance_ix_args);

    let start_issuance_ix = StartIssuance {
        delegate_account: find_delegate_pda(admin.pubkey()).0,
        delegate_wallet: admin.pubkey(),
        bond_account: find_bond_pda(bond_mint.pubkey()).0,
        issuance_account: find_issuance_pda(find_bond_pda(bond_mint.pubkey()).0, 1).0,
        mint_account: bond_mint.pubkey(),
        token2022_program: spl_token_2022::id(),
        system_program: solana_program::system_program::id(),
    }
    .instruction();

    let ixs = &[
        initialize_config_ix,
        initialize_delegate_ix,
        initialize_payment_feed_ix,
        initialize_bond_ix,
        initialize_issuance_ix,
        start_issuance_ix,
    ];

    let mut context = pt.context;
    let transaction = Transaction::new_signed_with_payer(
        ixs,
        Some(&admin.pubkey()),
        &[&admin, &nft_collection_mint, &bond_mint],
        context.last_blockhash,
    );

    context
        .banks_client
        .process_transaction(transaction)
        .await
        .unwrap();
    ExecuteTestContext {
        context,
        user,
        admin,
        bond_mint,
        usdc_mint,
        payment_base_price_feed_account,
        payment_quote_price_feed_account,
        nft_collection_mint: nft_collection_mint.pubkey(),
    }
}

pub fn create_multisig(
    payer: &Pubkey,
    multisig_account: &Pubkey,
    members: Vec<Pubkey>,
    threshold: u8,
) -> Vec<Instruction> {
    // Calculate the space needed for the multisig account
    let space = Multisig::get_packed_len();
    let rent = solana_sdk::rent::Rent::default();
    let lamports = rent.minimum_balance(space);

    // Create the account
    let create_account_ix = system_instruction::create_account(
        payer,
        multisig_account,
        lamports,
        space as u64,
        &spl_token_2022::id(),
    );

    // Convert members to the correct type
    let member_refs: Vec<&Pubkey> = members.iter().collect();

    // Initialize the multisig
    let init_multisig_ix = token_instruction::initialize_multisig(
        &spl_token_2022::id(),
        multisig_account,
        &member_refs,
        threshold,
    )
    .unwrap();

    // Return both instructions
    vec![create_account_ix, init_multisig_ix]
}

async fn migrate_to_multisig(et: &mut ExecuteTestContext) -> (Keypair, Pubkey) {
    // Create a new keypair for the multisig account
    let multisig_account = Keypair::new();
    let bond_pda = find_bond_pda(et.bond_mint.pubkey()).0;

    // Create multisig with Bond Mint PDA as member
    let create_multisig_ixs = create_multisig(
        &et.admin.pubkey(),
        &multisig_account.pubkey(),
        vec![bond_pda, Keypair::new().pubkey()], // randomly generated second member
        1,
    );

    // Create the migrate mint authority to multisig instruction
    let migrate_mint_authority_to_multisig_ix = MigrateMintAuthorityToMultisig {
        delegate_wallet: et.admin.pubkey(),
        delegate_account: find_delegate_pda(et.admin.pubkey()).0,
        mint_account: et.bond_mint.pubkey(),
        bond_account: find_bond_pda(et.bond_mint.pubkey()).0,
        bond_multisig_meta_account: find_bond_multisig_meta_pda(et.bond_mint.pubkey()).0,
        system_program: solana_program::system_program::id(),
        multisig_account: multisig_account.pubkey(),
        token2022_program: spl_token_2022::id(),
    }
    .instruction();

    // Combine all instructions
    let mut instructions = create_multisig_ixs;
    instructions.push(migrate_mint_authority_to_multisig_ix);

    // Send the transaction
    let recent_blockhash = et.context.last_blockhash;
    let transaction = Transaction::new_signed_with_payer(
        &instructions,
        Some(&et.admin.pubkey()),
        &[&et.admin, &multisig_account],
        recent_blockhash,
    );

    // Process the transaction
    et.context
        .banks_client
        .process_transaction(transaction)
        .await
        .unwrap();

    (multisig_account, bond_pda)
}

#[tokio::test]
async fn payout_issuance() {
    let mut et = setup().await;

    let recent_blockhash = et.context.last_blockhash;

    let purchase_bond_ix_args = PurchaseBondInstructionArgs { amount: 1_000_000 };
    let bond_account = find_bond_pda(et.bond_mint.pubkey()).0;
    let issuance_account = find_issuance_pda(bond_account, 1).0;
    let payment_account = find_payment_pda(issuance_account).0;
    let payment_token_account = get_associated_token_address(&payment_account, &et.usdc_mint);
    let user_token_account = get_associated_token_address_with_program_id(
        &et.user.pubkey(),
        &et.bond_mint.pubkey(),
        &spl_token_2022::id(),
    );
    let user_payment_token_account = get_associated_token_address(&et.user.pubkey(), &et.usdc_mint);

    let purchase_bond_ix = PurchaseBond {
        user_wallet: et.user.pubkey(),
        user_token_account,
        user_payment_token_account,
        bond_account,
        issuance_account,
        payment_account,
        payment_token_account,
        mint_account: et.bond_mint.pubkey(),
        payment_mint_account: et.usdc_mint,
        payment_feed_account: find_payment_feed_pda(PaymentFeedType::Stub).0,
        payment_base_price_feed_account: et.payment_base_price_feed_account,
        payment_quote_price_feed_account: et.payment_quote_price_feed_account,
        associated_token_program: spl_associated_token_account::id(),
        token_program: spl_token::id(),
        token2022_program: spl_token_2022::id(),
        system_program: solana_program::system_program::id(),
        mint_auth_multisig_account: None,
    }
    .instruction(purchase_bond_ix_args);

    let transaction = Transaction::new_signed_with_payer(
        &[purchase_bond_ix],
        Some(&et.user.pubkey()),
        &[&et.user],
        recent_blockhash,
    );

    assert!(et
        .context
        .banks_client
        .process_transaction(transaction)
        .await
        .is_ok());

    let nft_mint = Keypair::new();
    let nft_issuance_vault_account = find_nft_issuance_vault_pda(nft_mint.pubkey()).0;
    let request_redemption_ix_args = RequestRedemptionInstructionArgs { amount: 1_000_000 };
    let request_redemption_ix = RequestRedemption {
        user_wallet: et.user.pubkey(),
        config_account: find_config_pda().0,
        bond_account,
        issuance_account,
        mint_account: et.bond_mint.pubkey(),
        user_token_account,
        nft_mint_account: nft_mint.pubkey(),
        nft_issuance_vault_account,
        nft_issuance_vault_token_account: get_associated_token_address_with_program_id(
            &nft_issuance_vault_account,
            &et.bond_mint.pubkey(),
            &spl_token_2022::id(),
        ),
        user_nft_token_account: get_associated_token_address(&et.user.pubkey(), &nft_mint.pubkey()),
        nft_metadata_account: Metadata::find_pda(&nft_mint.pubkey()).0,
        nft_master_edition_account: MasterEdition::find_pda(&nft_mint.pubkey()).0,
        nft_collection_mint: et.nft_collection_mint,
        nft_collection_metadata_account: Metadata::find_pda(&et.nft_collection_mint).0,
        nft_collection_master_edition_account: MasterEdition::find_pda(&et.nft_collection_mint).0,
        associated_token_program: spl_associated_token_account::id(),
        token_program: spl_token::id(),
        token2022_program: spl_token_2022::id(),
        metadata_program: mpl_token_metadata::ID,
        sysvar_instructions: solana_program::sysvar::instructions::id(),
        system_program: solana_program::system_program::id(),
    }
    .instruction(request_redemption_ix_args);

    let transaction = Transaction::new_signed_with_payer(
        &[request_redemption_ix],
        Some(&et.user.pubkey()),
        &[&et.user, &nft_mint],
        recent_blockhash,
    );

    assert!(et
        .context
        .banks_client
        .process_transaction(transaction)
        .await
        .is_ok());

    let current_time = et.get_clock().await.unix_timestamp;
    let two_days_from_now: i64 = 60 * 60 * 24 * 2;
    et.advance_clock_past_timestamp(current_time + two_days_from_now + 1)
        .await;

    let delegate_wallet = et.admin.pubkey();
    let delegate_account = find_delegate_pda(delegate_wallet).0;
    let payout_account = find_payout_pda(issuance_account).0;
    let payout_token_account = get_associated_token_address(&payout_account, &et.usdc_mint);
    let payment_mint_account = et.usdc_mint;
    let payout_issuance_ix = PayoutIssuance {
        delegate_wallet,
        delegate_wallet_token_account: get_associated_token_address(
            &delegate_wallet,
            &et.usdc_mint,
        ),
        delegate_account,
        bond_account,
        issuance_account,
        mint_account: et.bond_mint.pubkey(),
        payment_mint_account,
        payout_account,
        payout_token_account,
        payment_feed_account: find_payment_feed_pda(PaymentFeedType::Stub).0,
        payment_base_price_feed_account: et.payment_base_price_feed_account,
        payment_quote_price_feed_account: et.payment_quote_price_feed_account,
        token_program: spl_token::id(),
        system_program: solana_program::system_program::id(),
    }
    .instruction();

    let transaction = Transaction::new_signed_with_payer(
        &[
            ComputeBudgetInstruction::set_compute_unit_limit(300_000),
            payout_issuance_ix,
        ],
        Some(&et.admin.pubkey()),
        &[&et.admin],
        et.context.last_blockhash,
    );

    assert!(et
        .context
        .banks_client
        .process_transaction(transaction)
        .await
        .is_ok());

    let account = et
        .context
        .banks_client
        .get_account(issuance_account)
        .await
        .unwrap()
        .unwrap();

    let issuance = Issuance::from_bytes(&account.data).unwrap();

    assert_eq!(
        issuance.status,
        stablebond_sdk::types::IssuanceStatus::Matured,
    );
    assert!(issuance.parent_bond == bond_account)
}

#[tokio::test]
async fn fail_premature_payout_issuance() {
    let mut et = setup().await;

    let recent_blockhash = et.context.last_blockhash;

    let purchase_bond_ix_args = PurchaseBondInstructionArgs { amount: 1_000_000 };
    let bond_account = find_bond_pda(et.bond_mint.pubkey()).0;
    let issuance_account = find_issuance_pda(bond_account, 1).0;
    let payment_account = find_payment_pda(issuance_account).0;
    let payment_token_account = get_associated_token_address(&payment_account, &et.usdc_mint);
    let user_token_account = get_associated_token_address_with_program_id(
        &et.user.pubkey(),
        &et.bond_mint.pubkey(),
        &spl_token_2022::id(),
    );
    let user_payment_token_account = get_associated_token_address(&et.user.pubkey(), &et.usdc_mint);

    let purchase_bond_ix = PurchaseBond {
        user_wallet: et.user.pubkey(),
        user_token_account,
        user_payment_token_account,
        bond_account,
        issuance_account,
        payment_account,
        payment_token_account,
        mint_account: et.bond_mint.pubkey(),
        payment_mint_account: et.usdc_mint,
        payment_feed_account: find_payment_feed_pda(PaymentFeedType::Stub).0,
        payment_base_price_feed_account: et.payment_base_price_feed_account,
        payment_quote_price_feed_account: et.payment_quote_price_feed_account,
        associated_token_program: spl_associated_token_account::id(),
        token_program: spl_token::id(),
        token2022_program: spl_token_2022::id(),
        system_program: solana_program::system_program::id(),
        mint_auth_multisig_account: None,
    }
    .instruction(purchase_bond_ix_args);

    let transaction = Transaction::new_signed_with_payer(
        &[purchase_bond_ix],
        Some(&et.user.pubkey()),
        &[&et.user],
        recent_blockhash,
    );

    assert!(et
        .context
        .banks_client
        .process_transaction(transaction)
        .await
        .is_ok());

    let nft_mint = Keypair::new();
    let nft_issuance_vault_account = find_nft_issuance_vault_pda(nft_mint.pubkey()).0;
    let request_redemption_ix_args = RequestRedemptionInstructionArgs { amount: 1_000_000 };
    let request_redemption_ix = RequestRedemption {
        user_wallet: et.user.pubkey(),
        config_account: find_config_pda().0,
        bond_account,
        issuance_account,
        mint_account: et.bond_mint.pubkey(),
        user_token_account,
        nft_mint_account: nft_mint.pubkey(),
        nft_issuance_vault_account,
        nft_issuance_vault_token_account: get_associated_token_address_with_program_id(
            &nft_issuance_vault_account,
            &et.bond_mint.pubkey(),
            &spl_token_2022::id(),
        ),
        user_nft_token_account: get_associated_token_address(&et.user.pubkey(), &nft_mint.pubkey()),
        nft_metadata_account: Metadata::find_pda(&nft_mint.pubkey()).0,
        nft_master_edition_account: MasterEdition::find_pda(&nft_mint.pubkey()).0,
        nft_collection_mint: et.nft_collection_mint,
        nft_collection_metadata_account: Metadata::find_pda(&et.nft_collection_mint).0,
        nft_collection_master_edition_account: MasterEdition::find_pda(&et.nft_collection_mint).0,
        associated_token_program: spl_associated_token_account::id(),
        token_program: spl_token::id(),
        token2022_program: spl_token_2022::id(),
        metadata_program: mpl_token_metadata::ID,
        sysvar_instructions: solana_program::sysvar::instructions::id(),
        system_program: solana_program::system_program::id(),
    }
    .instruction(request_redemption_ix_args);

    let transaction = Transaction::new_signed_with_payer(
        &[request_redemption_ix],
        Some(&et.user.pubkey()),
        &[&et.user, &nft_mint],
        recent_blockhash,
    );

    assert!(et
        .context
        .banks_client
        .process_transaction(transaction)
        .await
        .is_ok());

    let current_time = et.get_clock().await.unix_timestamp;
    let one_day_from_now: i64 = 60 * 60 * 24 * 1;
    et.advance_clock_past_timestamp(current_time + one_day_from_now)
        .await;

    let delegate_wallet = et.admin.pubkey();
    let delegate_account = find_delegate_pda(delegate_wallet).0;
    let payout_account = find_payout_pda(issuance_account).0;
    let payout_token_account = get_associated_token_address(&payout_account, &et.usdc_mint);
    let payment_mint_account = et.usdc_mint;
    let payout_issuance_ix = PayoutIssuance {
        delegate_wallet,
        delegate_wallet_token_account: get_associated_token_address(
            &delegate_wallet,
            &et.usdc_mint,
        ),
        delegate_account,
        bond_account,
        issuance_account,
        mint_account: et.bond_mint.pubkey(),
        payment_mint_account,
        payout_account,
        payout_token_account,
        payment_feed_account: find_payment_feed_pda(PaymentFeedType::Stub).0,
        payment_base_price_feed_account: et.payment_base_price_feed_account,
        payment_quote_price_feed_account: et.payment_quote_price_feed_account,
        token_program: spl_token::id(),
        system_program: solana_program::system_program::id(),
    }
    .instruction();

    let transaction = Transaction::new_signed_with_payer(
        &[
            ComputeBudgetInstruction::set_compute_unit_limit(300_000),
            payout_issuance_ix,
        ],
        Some(&et.admin.pubkey()),
        &[&et.admin],
        et.context.last_blockhash,
    );

    let error = et
        .context
        .banks_client
        .process_transaction(transaction)
        .await
        .err()
        .unwrap()
        .unwrap();

    match error {
        TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => {
            let program_error = StablebondError::IssuanceNotMatured as u32;
            assert_eq!(error_index, program_error + 6000);
        }
        _ => {
            panic!("Wrong error occurs while testing fail premature issuance payout");
        }
    }
}

#[tokio::test]
async fn payout_issuance_with_multisig() {
    let mut et = setup().await;

    // Migrate to multisig
    let (multisig_account, _) = migrate_to_multisig(&mut et).await;

    let recent_blockhash = et.context.last_blockhash;

    let purchase_bond_ix_args = PurchaseBondInstructionArgs { amount: 1_000_000 };
    let bond_account = find_bond_pda(et.bond_mint.pubkey()).0;
    let issuance_account = find_issuance_pda(bond_account, 1).0;
    let payment_account = find_payment_pda(issuance_account).0;
    let payment_token_account = get_associated_token_address(&payment_account, &et.usdc_mint);
    let user_token_account = get_associated_token_address_with_program_id(
        &et.user.pubkey(),
        &et.bond_mint.pubkey(),
        &spl_token_2022::id(),
    );
    let user_payment_token_account = get_associated_token_address(&et.user.pubkey(), &et.usdc_mint);

    let purchase_bond_ix = PurchaseBond {
        user_wallet: et.user.pubkey(),
        user_token_account,
        user_payment_token_account,
        bond_account,
        issuance_account,
        payment_account,
        payment_token_account,
        mint_account: et.bond_mint.pubkey(),
        payment_mint_account: et.usdc_mint,
        payment_feed_account: find_payment_feed_pda(PaymentFeedType::Stub).0,
        payment_base_price_feed_account: et.payment_base_price_feed_account,
        payment_quote_price_feed_account: et.payment_quote_price_feed_account,
        associated_token_program: spl_associated_token_account::id(),
        token_program: spl_token::id(),
        token2022_program: spl_token_2022::id(),
        system_program: solana_program::system_program::id(),
        mint_auth_multisig_account: Some(multisig_account.pubkey()),
    }
    .instruction(purchase_bond_ix_args);

    let transaction = Transaction::new_signed_with_payer(
        &[purchase_bond_ix],
        Some(&et.user.pubkey()),
        &[&et.user],
        recent_blockhash,
    );

    assert!(et
        .context
        .banks_client
        .process_transaction(transaction)
        .await
        .is_ok());

    let nft_mint = Keypair::new();
    let nft_issuance_vault_account = find_nft_issuance_vault_pda(nft_mint.pubkey()).0;
    let request_redemption_ix_args = RequestRedemptionInstructionArgs { amount: 1_000_000 };
    let request_redemption_ix = RequestRedemption {
        user_wallet: et.user.pubkey(),
        config_account: find_config_pda().0,
        bond_account,
        issuance_account,
        mint_account: et.bond_mint.pubkey(),
        user_token_account,
        nft_mint_account: nft_mint.pubkey(),
        nft_issuance_vault_account,
        nft_issuance_vault_token_account: get_associated_token_address_with_program_id(
            &nft_issuance_vault_account,
            &et.bond_mint.pubkey(),
            &spl_token_2022::id(),
        ),
        user_nft_token_account: get_associated_token_address(&et.user.pubkey(), &nft_mint.pubkey()),
        nft_metadata_account: Metadata::find_pda(&nft_mint.pubkey()).0,
        nft_master_edition_account: MasterEdition::find_pda(&nft_mint.pubkey()).0,
        nft_collection_mint: et.nft_collection_mint,
        nft_collection_metadata_account: Metadata::find_pda(&et.nft_collection_mint).0,
        nft_collection_master_edition_account: MasterEdition::find_pda(&et.nft_collection_mint).0,
        associated_token_program: spl_associated_token_account::id(),
        token_program: spl_token::id(),
        token2022_program: spl_token_2022::id(),
        metadata_program: mpl_token_metadata::ID,
        sysvar_instructions: solana_program::sysvar::instructions::id(),
        system_program: solana_program::system_program::id(),
    }
    .instruction(request_redemption_ix_args);

    let transaction = Transaction::new_signed_with_payer(
        &[request_redemption_ix],
        Some(&et.user.pubkey()),
        &[&et.user, &nft_mint],
        recent_blockhash,
    );

    assert!(et
        .context
        .banks_client
        .process_transaction(transaction)
        .await
        .is_ok());

    let current_time = et.get_clock().await.unix_timestamp;
    let two_days_from_now: i64 = 60 * 60 * 24 * 2;
    et.advance_clock_past_timestamp(current_time + two_days_from_now + 1)
        .await;

    let delegate_wallet = et.admin.pubkey();
    let delegate_account = find_delegate_pda(delegate_wallet).0;
    let payout_account = find_payout_pda(issuance_account).0;
    let payout_token_account = get_associated_token_address(&payout_account, &et.usdc_mint);
    let payment_mint_account = et.usdc_mint;
    let payout_issuance_ix = PayoutIssuance {
        delegate_wallet,
        delegate_wallet_token_account: get_associated_token_address(
            &delegate_wallet,
            &et.usdc_mint,
        ),
        delegate_account,
        bond_account,
        issuance_account,
        mint_account: et.bond_mint.pubkey(),
        payment_mint_account,
        payout_account,
        payout_token_account,
        payment_feed_account: find_payment_feed_pda(PaymentFeedType::Stub).0,
        payment_base_price_feed_account: et.payment_base_price_feed_account,
        payment_quote_price_feed_account: et.payment_quote_price_feed_account,
        token_program: spl_token::id(),
        system_program: solana_program::system_program::id(),
    }
    .instruction();

    let transaction = Transaction::new_signed_with_payer(
        &[
            ComputeBudgetInstruction::set_compute_unit_limit(300_000),
            payout_issuance_ix,
        ],
        Some(&et.admin.pubkey()),
        &[&et.admin],
        et.context.last_blockhash,
    );

    assert!(et
        .context
        .banks_client
        .process_transaction(transaction)
        .await
        .is_ok());

    let account = et
        .context
        .banks_client
        .get_account(issuance_account)
        .await
        .unwrap()
        .unwrap();

    let issuance = Issuance::from_bytes(&account.data).unwrap();

    assert_eq!(
        issuance.status,
        stablebond_sdk::types::IssuanceStatus::Matured,
    );
    assert!(issuance.parent_bond == bond_account)
}

#[tokio::test]
async fn fail_premature_payout_issuance_with_multisig() {
    let mut et = setup().await;

    // Migrate to multisig
    let (multisig_account, _) = migrate_to_multisig(&mut et).await;

    let recent_blockhash = et.context.last_blockhash;

    let purchase_bond_ix_args = PurchaseBondInstructionArgs { amount: 1_000_000 };
    let bond_account = find_bond_pda(et.bond_mint.pubkey()).0;
    let issuance_account = find_issuance_pda(bond_account, 1).0;
    let payment_account = find_payment_pda(issuance_account).0;
    let payment_token_account = get_associated_token_address(&payment_account, &et.usdc_mint);
    let user_token_account = get_associated_token_address_with_program_id(
        &et.user.pubkey(),
        &et.bond_mint.pubkey(),
        &spl_token_2022::id(),
    );
    let user_payment_token_account = get_associated_token_address(&et.user.pubkey(), &et.usdc_mint);

    let purchase_bond_ix = PurchaseBond {
        user_wallet: et.user.pubkey(),
        user_token_account,
        user_payment_token_account,
        bond_account,
        issuance_account,
        payment_account,
        payment_token_account,
        mint_account: et.bond_mint.pubkey(),
        payment_mint_account: et.usdc_mint,
        payment_feed_account: find_payment_feed_pda(PaymentFeedType::Stub).0,
        payment_base_price_feed_account: et.payment_base_price_feed_account,
        payment_quote_price_feed_account: et.payment_quote_price_feed_account,
        associated_token_program: spl_associated_token_account::id(),
        token_program: spl_token::id(),
        token2022_program: spl_token_2022::id(),
        system_program: solana_program::system_program::id(),
        mint_auth_multisig_account: Some(multisig_account.pubkey()),
    }
    .instruction(purchase_bond_ix_args);

    let transaction = Transaction::new_signed_with_payer(
        &[purchase_bond_ix],
        Some(&et.user.pubkey()),
        &[&et.user],
        recent_blockhash,
    );

    assert!(et
        .context
        .banks_client
        .process_transaction(transaction)
        .await
        .is_ok());

    let nft_mint = Keypair::new();
    let nft_issuance_vault_account = find_nft_issuance_vault_pda(nft_mint.pubkey()).0;
    let request_redemption_ix_args = RequestRedemptionInstructionArgs { amount: 1_000_000 };
    let request_redemption_ix = RequestRedemption {
        user_wallet: et.user.pubkey(),
        config_account: find_config_pda().0,
        bond_account,
        issuance_account,
        mint_account: et.bond_mint.pubkey(),
        user_token_account,
        nft_mint_account: nft_mint.pubkey(),
        nft_issuance_vault_account,
        nft_issuance_vault_token_account: get_associated_token_address_with_program_id(
            &nft_issuance_vault_account,
            &et.bond_mint.pubkey(),
            &spl_token_2022::id(),
        ),
        user_nft_token_account: get_associated_token_address(&et.user.pubkey(), &nft_mint.pubkey()),
        nft_metadata_account: Metadata::find_pda(&nft_mint.pubkey()).0,
        nft_master_edition_account: MasterEdition::find_pda(&nft_mint.pubkey()).0,
        nft_collection_mint: et.nft_collection_mint,
        nft_collection_metadata_account: Metadata::find_pda(&et.nft_collection_mint).0,
        nft_collection_master_edition_account: MasterEdition::find_pda(&et.nft_collection_mint).0,
        associated_token_program: spl_associated_token_account::id(),
        token_program: spl_token::id(),
        token2022_program: spl_token_2022::id(),
        metadata_program: mpl_token_metadata::ID,
        sysvar_instructions: solana_program::sysvar::instructions::id(),
        system_program: solana_program::system_program::id(),
    }
    .instruction(request_redemption_ix_args);

    let transaction = Transaction::new_signed_with_payer(
        &[request_redemption_ix],
        Some(&et.user.pubkey()),
        &[&et.user, &nft_mint],
        recent_blockhash,
    );

    assert!(et
        .context
        .banks_client
        .process_transaction(transaction)
        .await
        .is_ok());

    let current_time = et.get_clock().await.unix_timestamp;
    let one_day_from_now: i64 = 60 * 60 * 24 * 1;
    et.advance_clock_past_timestamp(current_time + one_day_from_now)
        .await;

    let delegate_wallet = et.admin.pubkey();
    let delegate_account = find_delegate_pda(delegate_wallet).0;
    let payout_account = find_payout_pda(issuance_account).0;
    let payout_token_account = get_associated_token_address(&payout_account, &et.usdc_mint);
    let payment_mint_account = et.usdc_mint;
    let payout_issuance_ix = PayoutIssuance {
        delegate_wallet,
        delegate_wallet_token_account: get_associated_token_address(
            &delegate_wallet,
            &et.usdc_mint,
        ),
        delegate_account,
        bond_account,
        issuance_account,
        mint_account: et.bond_mint.pubkey(),
        payment_mint_account,
        payout_account,
        payout_token_account,
        payment_feed_account: find_payment_feed_pda(PaymentFeedType::Stub).0,
        payment_base_price_feed_account: et.payment_base_price_feed_account,
        payment_quote_price_feed_account: et.payment_quote_price_feed_account,
        token_program: spl_token::id(),
        system_program: solana_program::system_program::id(),
    }
    .instruction();

    let transaction = Transaction::new_signed_with_payer(
        &[
            ComputeBudgetInstruction::set_compute_unit_limit(300_000),
            payout_issuance_ix,
        ],
        Some(&et.admin.pubkey()),
        &[&et.admin],
        et.context.last_blockhash,
    );

    let error = et
        .context
        .banks_client
        .process_transaction(transaction)
        .await
        .err()
        .unwrap()
        .unwrap();

    match error {
        TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => {
            let program_error = StablebondError::IssuanceNotMatured as u32;
            assert_eq!(error_index, program_error + 6000);
        }
        _ => {
            panic!("Wrong error occurs while testing fail premature issuance payout");
        }
    }
}
