use super::{create_mint, create_token_account, ExternalPrice, Metadata};
use mpl_token_vault::{instruction, state::PREFIX};
use solana_program::{pubkey::Pubkey, system_instruction};
use solana_program_test::*;
use solana_sdk::{
    signature::{Keypair, Signer},
    transaction::Transaction,
    transport,
};

pub struct Vault {
    pub keypair: Keypair,
    pub mint: Keypair,
    pub redeem_treasury: Keypair,
    pub fraction_treasury: Keypair,
}

impl Vault {
    pub fn new() -> Self {
        Vault {
            keypair: Keypair::new(),
            mint: Keypair::new(),
            redeem_treasury: Keypair::new(),
            fraction_treasury: Keypair::new(),
        }
    }

    pub async fn add_token_to_inactive_vault(
        &self,
        context: &mut ProgramTestContext,
        amount: u64,
        metadata: &Metadata,
    ) -> transport::Result<(Pubkey, Pubkey)> {
        let vault_pubkey = self.keypair.pubkey();
        let metaplex_token_vault_id = mpl_token_vault::id();

        let store = Keypair::new();
        let token_mint_pubkey = metadata.mint.pubkey();

        let seeds = &[
            PREFIX.as_bytes(),
            vault_pubkey.as_ref(),
            token_mint_pubkey.as_ref(),
        ];
        let (safety_deposit_box, _) = Pubkey::find_program_address(seeds, &metaplex_token_vault_id);
        let seeds = &[
            PREFIX.as_bytes(),
            metaplex_token_vault_id.as_ref(),
            vault_pubkey.as_ref(),
        ];
        let (authority, _) = Pubkey::find_program_address(seeds, &metaplex_token_vault_id);
        create_token_account(context, &store, &token_mint_pubkey, &authority).await?;

        let tx = Transaction::new_signed_with_payer(
            &[instruction::create_add_token_to_inactive_vault_instruction(
                mpl_token_vault::id(),
                safety_deposit_box,
                metadata.token.pubkey(),
                store.pubkey(),
                self.keypair.pubkey(),
                context.payer.pubkey(),
                context.payer.pubkey(),
                context.payer.pubkey(),
                amount,
            )],
            Some(&context.payer.pubkey()),
            &[&context.payer, &context.payer, &context.payer],
            context.last_blockhash,
        );
        context.banks_client.process_transaction(tx).await?;

        Ok((safety_deposit_box, store.pubkey()))
    }

    pub async fn activate(
        &self,
        context: &mut ProgramTestContext,
        number_of_shares: u64,
    ) -> transport::Result<()> {
        let metaplex_token_vault_id = mpl_token_vault::id();
        let vault_pubkey = self.keypair.pubkey();

        let seeds = &[
            PREFIX.as_bytes(),
            metaplex_token_vault_id.as_ref(),
            vault_pubkey.as_ref(),
        ];
        let (authority, _) = Pubkey::find_program_address(seeds, &metaplex_token_vault_id);

        let tx = Transaction::new_signed_with_payer(
            &[instruction::create_activate_vault_instruction(
                mpl_token_vault::id(),
                self.keypair.pubkey(),
                self.mint.pubkey(),
                self.fraction_treasury.pubkey(),
                authority,
                context.payer.pubkey(),
                number_of_shares,
            )],
            Some(&context.payer.pubkey()),
            &[&context.payer],
            context.last_blockhash,
        );

        context.banks_client.process_transaction(tx).await
    }

    pub async fn combine(
        &self,
        context: &mut ProgramTestContext,
        external_price: &ExternalPrice,
    ) -> transport::Result<()> {
        let outstanding_token_account = Keypair::new();
        let paying_token_account = Keypair::new();

        let metaplex_token_vault_id = mpl_token_vault::id();
        let vault_pubkey = self.keypair.pubkey();

        create_token_account(
            context,
            &outstanding_token_account,
            &self.mint.pubkey(),
            &context.payer.pubkey(),
        )
        .await?;
        create_token_account(
            context,
            &paying_token_account,
            &external_price.price_mint.pubkey(),
            &context.payer.pubkey(),
        )
        .await?;

        let seeds = &[
            PREFIX.as_bytes(),
            metaplex_token_vault_id.as_ref(),
            vault_pubkey.as_ref(),
        ];
        let (authority, _) = Pubkey::find_program_address(seeds, &metaplex_token_vault_id);

        let tx = Transaction::new_signed_with_payer(
            &[instruction::create_combine_vault_instruction(
                mpl_token_vault::id(),
                self.keypair.pubkey(),
                outstanding_token_account.pubkey(),
                paying_token_account.pubkey(),
                self.mint.pubkey(),
                self.fraction_treasury.pubkey(),
                self.redeem_treasury.pubkey(),
                context.payer.pubkey(),
                context.payer.pubkey(),
                context.payer.pubkey(),
                authority,
                external_price.keypair.pubkey(),
            )],
            Some(&context.payer.pubkey()),
            &[&context.payer, &context.payer, &context.payer],
            context.last_blockhash,
        );

        context.banks_client.process_transaction(tx).await
    }

    pub async fn create(
        &self,
        context: &mut ProgramTestContext,
        external_price: &ExternalPrice,
    ) -> transport::Result<()> {
        let metaplex_token_vault_id = mpl_token_vault::id();
        let vault_pubkey = self.keypair.pubkey();

        let seeds = &[
            PREFIX.as_bytes(),
            metaplex_token_vault_id.as_ref(),
            vault_pubkey.as_ref(),
        ];
        let (authority, _) = Pubkey::find_program_address(seeds, &metaplex_token_vault_id);

        create_mint(context, &self.mint, &authority, Some(&authority)).await?;
        create_token_account(
            context,
            &self.redeem_treasury,
            &external_price.price_mint.pubkey(),
            &authority,
        )
        .await?;
        create_token_account(
            context,
            &self.fraction_treasury,
            &self.mint.pubkey(),
            &authority,
        )
        .await?;

        let rent = context.banks_client.get_rent().await.unwrap();
        let tx = Transaction::new_signed_with_payer(
            &[
                system_instruction::create_account(
                    &context.payer.pubkey(),
                    &self.keypair.pubkey(),
                    rent.minimum_balance(mpl_token_vault::state::MAX_VAULT_SIZE),
                    mpl_token_vault::state::MAX_VAULT_SIZE as u64,
                    &mpl_token_vault::id(),
                ),
                instruction::create_init_vault_instruction(
                    mpl_token_vault::id(),
                    self.mint.pubkey(),
                    self.redeem_treasury.pubkey(),
                    self.fraction_treasury.pubkey(),
                    self.keypair.pubkey(),
                    context.payer.pubkey(),
                    external_price.keypair.pubkey(),
                    false,
                ),
            ],
            Some(&context.payer.pubkey()),
            &[&context.payer, &context.payer, &self.keypair],
            context.last_blockhash,
        );

        context.banks_client.process_transaction(tx).await
    }
}

impl Default for Vault {
    fn default() -> Self {
        Self::new()
    }
}
