mod program_test;
use {
    mpl_token_metadata::accounts::{MasterEdition, Metadata},
    program_test::StablebondProgramTest,
    solana_program_test::{tokio, ProgramTestContext},
    solana_sdk::{
        instruction::InstructionError,
        pubkey::Pubkey,
        signature::{Keypair, Signer},
        transaction::{Transaction, TransactionError},
    },
    spl_associated_token_account::get_associated_token_address,
    stablebond_sdk::{
        accounts::Issuance,
        errors::StablebondError,
        find_bond_pda, find_config_pda, find_delegate_pda, find_issuance_pda,
        find_payment_feed_pda, find_payment_pda, find_payout_pda,
        instructions::{
            InitializeBond, InitializeBondInstructionArgs, InitializeConfig,
            InitializeConfigInstructionArgs, InitializeDelegate, InitializeIssuance,
            InitializeIssuanceInstructionArgs,
        },
        types::{OracleSetup, PaymentFeedType},
    },
};

struct ExecuteTestContext {
    context: ProgramTestContext,
    admin: Keypair,
    bond_mint: Keypair,
    usdc_mint: Pubkey,
}

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 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_bond_ix_args = InitializeBondInstructionArgs {
        name: "Test Mint".to_string(),
        symbol: "TM".to_string(),
        uri: "https://test.com".to_string(),
        payment_feed_type: PaymentFeedType::UsdcUsd,
        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 ixs = &[
        initialize_config_ix,
        initialize_delegate_ix,
        initialize_bond_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,
        admin,
        bond_mint,
        usdc_mint,
    }
}

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

    let current_time = StablebondProgramTest::get_current_time();
    let one_day_from_now: i64 = current_time + 60 * 60 * 24;
    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_day_from_now,
        length_in_seconds: two_days_from_now,
    };

    let issuance_number = 1;
    let bond_pda = find_bond_pda(et.bond_mint.pubkey()).0;
    let upcoming_issuance_pda = find_issuance_pda(bond_pda, issuance_number).0;
    let payment_pda = find_payment_pda(upcoming_issuance_pda).0;
    let payout_pda = find_payout_pda(upcoming_issuance_pda).0;
    let initialize_issuance_ix = InitializeIssuance {
        delegate_wallet: et.admin.pubkey(),
        delegate_account: find_delegate_pda(et.admin.pubkey()).0,
        bond_account: find_bond_pda(et.bond_mint.pubkey()).0,
        upcoming_issuance_account: upcoming_issuance_pda,
        payment_feed_account: find_payment_feed_pda(PaymentFeedType::UsdcUsd).0,
        payment_mint_account: et.usdc_mint,
        payment_account: payment_pda,
        payment_token_account: get_associated_token_address(&payment_pda, &et.usdc_mint),
        payout_account: payout_pda,
        payout_token_account: get_associated_token_address(&payout_pda, &et.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 recent_blockhash = et.context.last_blockhash;
    let transaction = Transaction::new_signed_with_payer(
        &[initialize_issuance_ix],
        Some(&et.admin.pubkey()),
        &[&et.admin],
        recent_blockhash,
    );

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

    // validate issuance values after starting
    let account = et
        .context
        .banks_client
        .get_account(upcoming_issuance_pda)
        .await
        .unwrap()
        .unwrap();

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

    // validate issuances are equal
    assert_eq!(
        issuance.status,
        stablebond_sdk::types::IssuanceStatus::Upcoming,
    );
    assert_eq!(issuance.parent_bond, bond_pda);
    assert_eq!(issuance.interest_rate_bps, 11_00);
    assert_eq!(issuance.liquidity, 100_000_000);
    assert_eq!(issuance.estimated_start_datetime, one_day_from_now);
    assert_eq!(issuance.length_in_seconds, two_days_from_now);
    assert_eq!(issuance.actual_start_datetime, 0);
}

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

    let current_time = StablebondProgramTest::get_current_time();
    let one_day_from_now: i64 = current_time + 60 * 60 * 24;
    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_day_from_now,
        length_in_seconds: two_days_from_now,
    };

    let issuance_number = 1;
    let bond_pda = find_bond_pda(et.bond_mint.pubkey()).0;
    let upcoming_issuance_pda = find_issuance_pda(bond_pda, issuance_number).0;
    let payment_pda = find_payment_pda(upcoming_issuance_pda).0;
    let payout_pda = find_payout_pda(upcoming_issuance_pda).0;
    let initialize_issuance_ix = InitializeIssuance {
        delegate_wallet: et.admin.pubkey(),
        delegate_account: find_delegate_pda(et.admin.pubkey()).0,
        bond_account: find_bond_pda(et.bond_mint.pubkey()).0,
        payment_feed_account: find_payment_feed_pda(PaymentFeedType::UsdcUsd).0,
        upcoming_issuance_account: upcoming_issuance_pda,
        payment_account: payment_pda,
        payment_mint_account: et.usdc_mint,
        payment_token_account: get_associated_token_address(&payment_pda, &et.usdc_mint),
        payout_account: payout_pda,
        payout_token_account: get_associated_token_address(&payout_pda, &et.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 recent_blockhash = et.context.last_blockhash;
    let transaction = Transaction::new_signed_with_payer(
        &[initialize_issuance_ix],
        Some(&et.admin.pubkey()),
        &[&et.admin],
        recent_blockhash,
    );

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

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

    let issuance_number = 2;
    let bond_pda = find_bond_pda(et.bond_mint.pubkey()).0;
    let upcoming_issuance_pda = find_issuance_pda(bond_pda, issuance_number).0;
    let payment_pda = find_payment_pda(upcoming_issuance_pda).0;
    let payout_pda = find_payout_pda(upcoming_issuance_pda).0;
    let current_issuance_pda = find_issuance_pda(bond_pda, 1).0;
    let initialize_issuance_ix = InitializeIssuance {
        delegate_wallet: et.admin.pubkey(),
        delegate_account: find_delegate_pda(et.admin.pubkey()).0,
        bond_account: find_bond_pda(et.bond_mint.pubkey()).0,
        upcoming_issuance_account: upcoming_issuance_pda,
        payment_feed_account: find_payment_feed_pda(PaymentFeedType::UsdcUsd).0,
        payment_account: payment_pda,
        payment_mint_account: et.usdc_mint,
        payment_token_account: get_associated_token_address(&payment_pda, &et.usdc_mint),
        payout_account: payout_pda,
        payout_token_account: get_associated_token_address(&payout_pda, &et.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: Some(current_issuance_pda),
    }
    .instruction(initialize_issuance_ix_args);

    let recent_blockhash = et.context.last_blockhash;
    let transaction = Transaction::new_signed_with_payer(
        &[initialize_issuance_ix],
        Some(&et.admin.pubkey()),
        &[&et.admin],
        recent_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::InvalidIssuance as u32;
            assert_eq!(error_index, program_error + 6000);
        }
        _ => {
            panic!(
                "Wrong error occurs while testing fail_initializing_issuance_if_prev_hasnt_started"
            );
        }
    }
}

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

    let current_time = StablebondProgramTest::get_current_time();
    let one_day_from_now: i64 = current_time + 60 * 60 * 24;
    let two_days_from_now: i64 = 60 * 60 * 24 * 2;

    let initialize_issuance_ix_args = InitializeIssuanceInstructionArgs {
        liquidity: 100_000_000,
        interest_rate_bps: 10_100,
        estimated_start_datetime: one_day_from_now,
        length_in_seconds: two_days_from_now,
    };

    let issuance_number = 1;
    let bond_pda = find_bond_pda(et.bond_mint.pubkey()).0;
    let upcoming_issuance_pda = find_issuance_pda(bond_pda, issuance_number).0;
    let payment_pda = find_payment_pda(upcoming_issuance_pda).0;
    let payout_pda = find_payout_pda(upcoming_issuance_pda).0;
    let initialize_issuance_ix = InitializeIssuance {
        delegate_wallet: et.admin.pubkey(),
        delegate_account: find_delegate_pda(et.admin.pubkey()).0,
        bond_account: find_bond_pda(et.bond_mint.pubkey()).0,
        upcoming_issuance_account: upcoming_issuance_pda,
        payment_feed_account: find_payment_feed_pda(PaymentFeedType::UsdcUsd).0,
        payment_mint_account: et.usdc_mint,
        payment_account: payment_pda,
        payment_token_account: get_associated_token_address(&payment_pda, &et.usdc_mint),
        payout_account: payout_pda,
        payout_token_account: get_associated_token_address(&payout_pda, &et.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 recent_blockhash = et.context.last_blockhash;
    let transaction = Transaction::new_signed_with_payer(
        &[initialize_issuance_ix],
        Some(&et.admin.pubkey()),
        &[&et.admin],
        recent_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::InvalidInterestRate as u32;
            assert_eq!(error_index, program_error + 6000);
        }
        _ => {
            panic!("Wrong error occurs while testing invalid interest rate");
        }
    }
}

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

    let current_time = StablebondProgramTest::get_current_time();
    let one_day_from_now: i64 = current_time + 60 * 60 * 24;
    let two_days_from_now: i64 = 60 * 60 * 24 * 2;

    let initialize_issuance_ix_args = InitializeIssuanceInstructionArgs {
        liquidity: 100_000_000,
        interest_rate_bps: -1,
        estimated_start_datetime: one_day_from_now,
        length_in_seconds: two_days_from_now,
    };

    let issuance_number = 1;
    let bond_pda = find_bond_pda(et.bond_mint.pubkey()).0;
    let upcoming_issuance_pda = find_issuance_pda(bond_pda, issuance_number).0;
    let payment_pda = find_payment_pda(upcoming_issuance_pda).0;
    let payout_pda = find_payout_pda(upcoming_issuance_pda).0;
    let initialize_issuance_ix = InitializeIssuance {
        delegate_wallet: et.admin.pubkey(),
        delegate_account: find_delegate_pda(et.admin.pubkey()).0,
        bond_account: find_bond_pda(et.bond_mint.pubkey()).0,
        upcoming_issuance_account: upcoming_issuance_pda,
        payment_feed_account: find_payment_feed_pda(PaymentFeedType::UsdcUsd).0,
        payment_mint_account: et.usdc_mint,
        payment_account: payment_pda,
        payment_token_account: get_associated_token_address(&payment_pda, &et.usdc_mint),
        payout_account: payout_pda,
        payout_token_account: get_associated_token_address(&payout_pda, &et.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 recent_blockhash = et.context.last_blockhash;
    let transaction = Transaction::new_signed_with_payer(
        &[initialize_issuance_ix],
        Some(&et.admin.pubkey()),
        &[&et.admin],
        recent_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::InvalidInterestRate as u32;
            assert_eq!(error_index, program_error + 6000);
        }
        _ => {
            panic!("Wrong error occurs while testing invalid interest rate");
        }
    }
}

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

    let current_time = StablebondProgramTest::get_current_time();
    let one_day_from_now: i64 = current_time + 60 * 60 * 24;
    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_day_from_now,
        length_in_seconds: two_days_from_now,
    };

    let issuance_number = 1;
    let bond_pda = find_bond_pda(et.bond_mint.pubkey()).0;
    let upcoming_issuance_pda = find_issuance_pda(bond_pda, issuance_number).0;
    let payment_pda = find_payment_pda(upcoming_issuance_pda).0;
    let payout_pda = find_payout_pda(upcoming_issuance_pda).0;
    let initialize_issuance_ix = InitializeIssuance {
        delegate_wallet: et.admin.pubkey(),
        delegate_account: find_delegate_pda(et.admin.pubkey()).0,
        bond_account: find_bond_pda(et.bond_mint.pubkey()).0,
        upcoming_issuance_account: upcoming_issuance_pda,
        payment_feed_account: find_payment_feed_pda(PaymentFeedType::UsdcUsd).0,
        payment_mint_account: et.usdc_mint,
        payment_account: payment_pda,
        payment_token_account: get_associated_token_address(&payment_pda, &et.usdc_mint),
        payout_account: payout_pda,
        payout_token_account: get_associated_token_address(&payout_pda, &et.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 recent_blockhash = et.context.last_blockhash;
    let transaction = Transaction::new_signed_with_payer(
        &[initialize_issuance_ix],
        Some(&et.admin.pubkey()),
        &[&et.admin],
        recent_blockhash,
    );

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

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

    let current_time = StablebondProgramTest::get_current_time();
    let one_day_from_now: i64 = current_time + 60 * 60 * 24;
    let two_days_from_now: i64 = 60 * 60 * 24 * 2;

    let initialize_issuance_ix_args = InitializeIssuanceInstructionArgs {
        liquidity: u64::MAX,
        interest_rate_bps: 00_00,
        estimated_start_datetime: one_day_from_now,
        length_in_seconds: two_days_from_now,
    };

    let issuance_number = 1;
    let bond_pda = find_bond_pda(et.bond_mint.pubkey()).0;
    let upcoming_issuance_pda = find_issuance_pda(bond_pda, issuance_number).0;
    let payment_pda = find_payment_pda(upcoming_issuance_pda).0;
    let payout_pda = find_payout_pda(upcoming_issuance_pda).0;
    let initialize_issuance_ix = InitializeIssuance {
        delegate_wallet: et.admin.pubkey(),
        delegate_account: find_delegate_pda(et.admin.pubkey()).0,
        bond_account: find_bond_pda(et.bond_mint.pubkey()).0,
        upcoming_issuance_account: upcoming_issuance_pda,
        payment_feed_account: find_payment_feed_pda(PaymentFeedType::UsdcUsd).0,
        payment_mint_account: et.usdc_mint,
        payment_account: payment_pda,
        payment_token_account: get_associated_token_address(&payment_pda, &et.usdc_mint),
        payout_account: payout_pda,
        payout_token_account: get_associated_token_address(&payout_pda, &et.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 recent_blockhash = et.context.last_blockhash;
    let transaction = Transaction::new_signed_with_payer(
        &[initialize_issuance_ix],
        Some(&et.admin.pubkey()),
        &[&et.admin],
        recent_blockhash,
    );

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

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

    let current_time = StablebondProgramTest::get_current_time();
    let one_day_from_now: i64 = current_time + 60 * 60 * 24;
    let two_days_from_now: i64 = 60 * 60 * 24 * 2;

    let initialize_issuance_ix_args = InitializeIssuanceInstructionArgs {
        liquidity: 0,
        interest_rate_bps: 00_00,
        estimated_start_datetime: one_day_from_now,
        length_in_seconds: two_days_from_now,
    };

    let issuance_number = 1;
    let bond_pda = find_bond_pda(et.bond_mint.pubkey()).0;
    let upcoming_issuance_pda = find_issuance_pda(bond_pda, issuance_number).0;
    let payment_pda = find_payment_pda(upcoming_issuance_pda).0;
    let payout_pda = find_payout_pda(upcoming_issuance_pda).0;
    let initialize_issuance_ix = InitializeIssuance {
        delegate_wallet: et.admin.pubkey(),
        delegate_account: find_delegate_pda(et.admin.pubkey()).0,
        bond_account: find_bond_pda(et.bond_mint.pubkey()).0,
        upcoming_issuance_account: upcoming_issuance_pda,
        payment_feed_account: find_payment_feed_pda(PaymentFeedType::UsdcUsd).0,
        payment_mint_account: et.usdc_mint,
        payment_account: payment_pda,
        payment_token_account: get_associated_token_address(&payment_pda, &et.usdc_mint),
        payout_account: payout_pda,
        payout_token_account: get_associated_token_address(&payout_pda, &et.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 recent_blockhash = et.context.last_blockhash;
    let transaction = Transaction::new_signed_with_payer(
        &[initialize_issuance_ix],
        Some(&et.admin.pubkey()),
        &[&et.admin],
        recent_blockhash,
    );

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