use {
    bincode::deserialize,
    solana_program_test::{processor, BanksClientError, ProgramTest, ProgramTestContext},
    solana_sdk::{
        clock::{Clock, UnixTimestamp},
        commitment_config::CommitmentLevel,
        instruction::{Instruction, InstructionError},
        program_option::COption,
        program_pack::Pack,
        pubkey::Pubkey,
        rent::*,
        signature::{Keypair, Signer},
        system_instruction, sysvar,
        transaction::{Transaction, TransactionError},
    },
    spl_associated_token_account::get_associated_token_address_with_program_id,
    spl_token::{state::Mint, ID as SPL_TOKEN_PROGRAM_ID},
    stablebond_sdk::{
        errors::StablebondError,
        types::{PaymentFeedInfo, PaymentFeedType},
        STABLEBOND_ID,
    },
    std::{
        borrow::Borrow,
        time::{SystemTime, UNIX_EPOCH},
    },
};

const RUST_LOG_DEFAULT: &str = "solana_rbpf::vm=info,\
             solana_program_runtime::stable_log=debug,\
             solana_runtime::message_processor=debug,\
             solana_runtime::system_instruction_processor=info,\
             solana_program_test=info,\
             solana_bpf_loader_program=debug";

pub trait AddPacked {
    fn add_packable_account<T: Pack>(
        &mut self,
        pubkey: Pubkey,
        amount: u64,
        data: &T,
        owner: &Pubkey,
    );
}

impl AddPacked for ProgramTest {
    fn add_packable_account<T: Pack>(
        &mut self,
        pubkey: Pubkey,
        amount: u64,
        data: &T,
        owner: &Pubkey,
    ) {
        let mut account = solana_sdk::account::Account::new(amount, T::get_packed_len(), owner);
        data.pack_into_slice(&mut account.data);
        self.add_account(pubkey, account);
    }
}

#[derive(Copy, Clone)]
pub struct BondCookie {
    pub index: usize,
    pub unit: f64,
    pub payment_mint: Pubkey,
}

impl Default for BondCookie {
    fn default() -> Self {
        Self {
            index: 0,
            unit: 10u64.pow(6) as f64,
            payment_mint: Keypair::new().pubkey(),
        }
    }
}

pub struct StablebondProgramTest {
    pub context: ProgramTestContext,
    pub rent: Rent,
    pub admin: Keypair,
    pub delegate: Keypair,
    pub nft_collection_mint: Keypair,
    pub mints: Vec<BondCookie>,
    pub bond_mints: Vec<Keypair>,
    pub num_mints: usize,
    pub num_users: usize,
    pub users: Vec<Keypair>,
    pub payment_token_accounts: Vec<Pubkey>,
    pub usdc_mxn_payment_feed: PaymentFeedInfo,
    pub usdc_usd_payment_feed: PaymentFeedInfo,
    pub stub_payment_feed: PaymentFeedInfo,
}

impl StablebondProgramTest {
    #[allow(dead_code)]
    pub async fn start_new() -> Self {
        // Predefined mints, maybe can even add symbols to them
        let mints: Vec<BondCookie> = vec![
            BondCookie {
                index: 0,
                unit: 10u64.pow(6) as f64,
                ..Default::default()
            },
            BondCookie {
                index: 1,
                unit: 10u64.pow(6) as f64,
                ..Default::default()
            },
            BondCookie {
                index: 2,
                unit: 10u64.pow(6) as f64,
                ..Default::default()
            },
            BondCookie {
                index: 3,
                unit: 10u64.pow(6) as f64,
                ..Default::default()
            },
        ];

        solana_logger::setup_with_default(RUST_LOG_DEFAULT);

        let mut pt = ProgramTest::new("stablebond", STABLEBOND_ID, None);
        pt.add_program(
            "spl_token_2022",
            spl_token_2022::id(),
            processor!(spl_token_2022::processor::Processor::process),
        );
        pt.add_program(
            "spl_associated_token_account",
            spl_associated_token_account::id(),
            processor!(spl_associated_token_account::processor::process_instruction),
        );
        pt.add_program(
            "spl_token",
            SPL_TOKEN_PROGRAM_ID,
            processor!(spl_token::processor::Processor::process),
        );
        pt.add_program("mpl_token_metadata", mpl_token_metadata::ID, None);
        pt.set_compute_max_units(650_000);
        pt.prefer_bpf(false);

        // Generate bond mint keypairs
        let mut bond_mints = Vec::new();
        for _ in 0..mints.len() {
            let bond_mint = Keypair::new();
            bond_mints.push(bond_mint);
        }

        // Add payment mints to the ProgramTest
        for mint in mints.iter() {
            pt.add_packable_account(
                mint.payment_mint,
                u32::MAX as u64,
                &Mint {
                    is_initialized: true,
                    mint_authority: COption::Some(Pubkey::new_unique()),
                    decimals: 6,
                    ..Mint::default()
                },
                &spl_token::id(),
            );
        }

        let admin = Keypair::new();
        let delegate = Keypair::new();
        let nft_collection_mint = Keypair::new();

        let num_users = 10 as usize;
        let num_mints = mints.len();
        let mut users = Vec::new();
        let mut payment_token_accounts = Vec::new();
        for _ in 0..num_users {
            let user_key = Keypair::new();
            pt.add_account(
                user_key.pubkey(),
                solana_sdk::account::Account::new(
                    u32::MAX as u64,
                    0,
                    &solana_sdk::system_program::id(),
                ),
            );

            // Create token accounts for each payment mint on each user
            for mint_index in 0..num_mints {
                let mint = &mints[mint_index].payment_mint;
                let token_account = get_associated_token_address_with_program_id(
                    &user_key.pubkey(),
                    &mint,
                    &spl_token::id(),
                );
                pt.add_packable_account(
                    token_account,
                    u32::MAX as u64,
                    &spl_token::state::Account {
                        mint: mint.clone(),
                        owner: user_key.pubkey(),
                        amount: 1_000_000_000_000_000_000,
                        state: spl_token::state::AccountState::Initialized,
                        ..spl_token::state::Account::default()
                    },
                    &spl_token::id(),
                );
                payment_token_accounts.push(token_account);

                //add the token accounts to the admin as well for payouts and payments
                let token_account = get_associated_token_address_with_program_id(
                    &admin.pubkey(),
                    &mint,
                    &spl_token::id(),
                );
                pt.add_packable_account(
                    token_account,
                    u32::MAX as u64,
                    &spl_token::state::Account {
                        mint: mint.clone(),
                        owner: admin.pubkey(),
                        amount: 1_000_000_000_000_000_000,
                        state: spl_token::state::AccountState::Initialized,
                        ..spl_token::state::Account::default()
                    },
                    &spl_token::id(),
                );
            }
            users.push(user_key);
        }

        pt.add_account(
            admin.pubkey(),
            solana_sdk::account::Account::new(
                u32::MAX as u64,
                0,
                &solana_sdk::system_program::id(),
            ),
        );

        // TODO: Add multiple bonds and issuances

        let mut context = pt.start_with_context().await;
        let rent: Rent = context.banks_client.get_rent().await.unwrap();
        let usdc_mint = mints[0].payment_mint;

        let usdc_mxn_payment_feed = PaymentFeedInfo {
            payment_mint: usdc_mint,
            base_price_feed: Keypair::new().pubkey(),
            quote_price_feed: Keypair::new().pubkey(),
            payment_feed_type: PaymentFeedType::UsdcMxn,
        };
        let usdc_usd_payment_feed = PaymentFeedInfo {
            payment_mint: usdc_mint,
            base_price_feed: Keypair::new().pubkey(),
            quote_price_feed: Keypair::new().pubkey(),
            payment_feed_type: PaymentFeedType::UsdcUsd,
        };
        let stub_payment_feed = PaymentFeedInfo {
            payment_mint: usdc_mint,
            base_price_feed: Keypair::new().pubkey(),
            quote_price_feed: Pubkey::default(),
            payment_feed_type: PaymentFeedType::Stub,
        };

        Self {
            context,
            rent,
            mints,
            bond_mints,
            nft_collection_mint,
            admin,
            delegate,
            num_users,
            num_mints,
            users,
            payment_token_accounts,
            usdc_mxn_payment_feed,
            usdc_usd_payment_feed,
            stub_payment_feed,
        }
    }

    #[allow(dead_code)]
    pub async fn process_transaction(
        &mut self,
        instructions: &[Instruction],
        signers: Option<&[&Keypair]>,
    ) -> Result<(), BanksClientError> {
        let mut transaction =
            Transaction::new_with_payer(&instructions, Some(&self.context.payer.pubkey()));

        let mut all_signers = vec![&self.context.payer];

        if let Some(signers) = signers {
            all_signers.extend_from_slice(signers);
        }

        // This fails when warping is involved
        let recent_blockhash = self
            .context
            .banks_client
            .get_latest_blockhash()
            .await
            .unwrap();

        transaction.sign(&all_signers, recent_blockhash);

        self.context
            .banks_client
            .process_transaction_with_commitment(transaction, CommitmentLevel::Processed)
            .await
    }

    #[allow(dead_code)]
    pub async fn process_transaction_expect_error(
        &mut self,
        instructions: &[Instruction],
        signers: Option<&[&Keypair]>,
        error_code: StablebondError,
    ) {
        let mut transaction =
            Transaction::new_with_payer(&instructions, Some(&self.context.payer.pubkey()));

        let mut all_signers = vec![&self.context.payer];

        if let Some(signers) = signers {
            all_signers.extend_from_slice(signers);
        }

        // This fails when warping is involved
        let recent_blockhash = self
            .context
            .banks_client
            .get_latest_blockhash()
            .await
            .unwrap();

        transaction.sign(&all_signers, recent_blockhash);

        let error = self
            .context
            .banks_client
            .process_transaction_with_commitment(transaction, CommitmentLevel::Processed)
            .await
            .err()
            .unwrap()
            .unwrap();

        match error {
            TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => {
                let program_error = error_code as u32;
                assert_eq!(error_index, program_error);
            }
            _ => {
                panic!(
                "Wrong error occurs while testing fail_purchase_bond_after_starting_new_issuances"
            );
            }
        }
    }

    #[allow(dead_code)]
    pub async fn get_account(&mut self, address: Pubkey) -> solana_sdk::account::Account {
        return self
            .context
            .banks_client
            .get_account(address)
            .await
            .unwrap()
            .unwrap();
    }

    #[allow(dead_code)]
    pub fn get_payer_pk(&mut self) -> Pubkey {
        return self.context.payer.pubkey();
    }

    #[allow(dead_code)]
    pub async fn get_lamport_balance(&mut self, address: Pubkey) -> u64 {
        self.context
            .banks_client
            .get_account(address)
            .await
            .unwrap()
            .unwrap()
            .lamports
    }

    #[allow(dead_code)]
    pub async fn get_token_balance(&mut self, address: Pubkey) -> u64 {
        let token = self
            .context
            .banks_client
            .get_account(address)
            .await
            .unwrap()
            .unwrap();
        return spl_token::state::Account::unpack(&token.data[..])
            .unwrap()
            .amount;
    }

    #[allow(dead_code)]
    pub async fn create_account(&mut self, size: usize, owner: &Pubkey) -> Pubkey {
        let keypair = Keypair::new();
        let rent = self.rent.minimum_balance(size);

        let instructions = [system_instruction::create_account(
            &self.context.payer.pubkey(),
            &keypair.pubkey(),
            rent as u64,
            size as u64,
            owner,
        )];

        self.process_transaction(&instructions, Some(&[&keypair]))
            .await
            .unwrap();

        return keypair.pubkey();
    }

    #[allow(dead_code)]
    pub async fn create_mint(&mut self, mint_authority: &Pubkey) -> Pubkey {
        let keypair = Keypair::new();
        let rent = self.rent.minimum_balance(Mint::LEN);

        let instructions = [
            system_instruction::create_account(
                &self.context.payer.pubkey(),
                &keypair.pubkey(),
                rent,
                Mint::LEN as u64,
                &spl_token::id(),
            ),
            spl_token::instruction::initialize_mint(
                &spl_token::id(),
                &keypair.pubkey(),
                &mint_authority,
                None,
                0,
            )
            .unwrap(),
        ];

        self.process_transaction(&instructions, Some(&[&keypair]))
            .await
            .unwrap();

        return keypair.pubkey();
    }

    #[allow(dead_code)]
    pub fn with_mint(&mut self, mint_index: usize) -> BondCookie {
        return self.mints[mint_index];
    }

    #[allow(dead_code)]
    pub fn with_user_payment_token_account(
        &mut self,
        user_index: usize,
        mint_index: usize,
    ) -> Pubkey {
        return self.payment_token_accounts[(user_index * self.num_mints) + mint_index];
    }

    #[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;
        }
    }
}
