mod program_test;
use {
    mpl_token_metadata::accounts::{MasterEdition, Metadata},
    program_test::StablebondProgramTest,
    solana_program_test::{tokio, ProgramTestContext},
    solana_sdk::{
        instruction::InstructionError,
        signature::{Keypair, Signer},
        transaction::{Transaction, TransactionError},
    },
    spl_associated_token_account::get_associated_token_address,
    stablebond_sdk::{
        accounts::Delegate,
        errors::StablebondError,
        find_config_pda, find_delegate_pda, find_payment_feed_pda,
        instructions::{InitializeConfig, InitializeConfigInstructionArgs, InitializeDelegate},
        types::{Discriminator, OracleSetup, PaymentFeedType},
    },
};

struct ExecuteTestContext {
    context: ProgramTestContext,
    admin: Keypair,
}

async fn setup() -> ExecuteTestContext {
    let pt = StablebondProgramTest::start_new().await;
    let admin = pt.admin;
    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 ixs = &[initialize_config_ix];

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

    context
        .banks_client
        .process_transaction(transaction)
        .await
        .unwrap();
    ExecuteTestContext { context, admin }
}

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

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

    let transaction = Transaction::new_signed_with_payer(
        &[initialize_delegate_ix],
        Some(&et.admin.pubkey()),
        &[&et.admin],
        et.context.last_blockhash,
    );

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

    //validate the delegate account
    let account = et
        .context
        .banks_client
        .get_account(find_delegate_pda(et.admin.pubkey()).0)
        .await
        .unwrap();

    assert!(account.is_some());
    let delegate = Delegate::from_bytes(&account.unwrap().data).unwrap();
    assert_eq!(delegate.discriminator, Discriminator::Delegate);
    assert_eq!(delegate.delegate, et.admin.pubkey());
}

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

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

    let transaction = Transaction::new_signed_with_payer(
        &[initialize_delegate_ix],
        Some(&et.admin.pubkey()),
        &[&et.admin, &other_wallet],
        et.context.last_blockhash,
    );

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

    //validate the delegate account for the other wallet
    let account = et
        .context
        .banks_client
        .get_account(find_delegate_pda(other_wallet_pubkey).0)
        .await
        .unwrap();

    assert!(account.is_some());
    let delegate = Delegate::from_bytes(&account.unwrap().data).unwrap();
    assert_eq!(delegate.discriminator, Discriminator::Delegate);
    assert_eq!(delegate.delegate, other_wallet_pubkey);
}

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

    let other_wallet = Keypair::new();
    let other_wallet_pubkey = other_wallet.pubkey();

    let transfer_sol_ix = solana_program::system_instruction::transfer(
        &et.context.payer.pubkey(),
        &other_wallet_pubkey,
        1_000_000_000,
    );

    let transaction = Transaction::new_signed_with_payer(
        &[transfer_sol_ix],
        Some(&et.context.payer.pubkey()),
        &[&et.context.payer],
        et.context.last_blockhash,
    );

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

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

    let transaction = Transaction::new_signed_with_payer(
        &[initialize_delegate_ix],
        Some(&other_wallet_pubkey),
        &[&other_wallet],
        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::AdminConfigMismatch as u32;
            assert_eq!(error_index, program_error + 6000);
        }
        _ => {
            panic!("Wrong error occurs while testing initialize delegate");
        }
    }
}

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

    let other_wallet = Keypair::new();
    let other_wallet_pubkey = other_wallet.pubkey();

    let transfer_sol_ix = solana_program::system_instruction::transfer(
        &et.context.payer.pubkey(),
        &other_wallet_pubkey,
        1_000_000_000,
    );

    let transaction = Transaction::new_signed_with_payer(
        &[transfer_sol_ix],
        Some(&et.context.payer.pubkey()),
        &[&et.context.payer],
        et.context.last_blockhash,
    );

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

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

    let transaction = Transaction::new_signed_with_payer(
        &[initialize_delegate_ix],
        Some(&other_wallet_pubkey),
        &[&other_wallet],
        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::InvalidConfigAddress as u32;
            assert_eq!(error_index, program_error + 6000);
        }
        _ => {
            panic!("Wrong error occurs while testing initialize delegate");
        }
    }
}
