use super::*;

// This is an unspecific happy-case test that just runs a few instructions to check
// that they work in principle. It should be split up / renamed.
#[tokio::test]
async fn test_margin_trade() -> Result<(), BanksClientError> {
    let mut test_builder = TestContextBuilder::new();
    test_builder.test().set_compute_max_units(100_000);
    let context = test_builder.start_default().await;
    let solana = &context.solana.clone();

    let admin = TestKeypair::new();
    let owner = context.users[0].key;
    let payer = context.users[1].key;
    let mints = &context.mints[0..2];
    let payer_mint0_account = context.users[1].token_accounts[0];
    let loan_origination_fee = 0.0005;

    //
    // SETUP: Create a group, account, register a token (mint0)
    //

    let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
        admin,
        payer,
        mints: mints.to_vec(),
        ..GroupWithTokensConfig::default()
    }
    .create(solana)
    .await;
    let bank = tokens[0].bank;
    let vault = tokens[0].vault;

    //
    // provide some funds for tokens, so the test user can borrow
    //
    let provided_amount = 1000;
    create_funded_account(
        &solana,
        group,
        owner,
        1,
        &context.users[1],
        mints,
        provided_amount,
        0,
    )
    .await;

    //
    // create the test user account
    //

    let account = send_tx(
        solana,
        AccountCreateInstruction {
            account_num: 0,
            group,
            owner,
            payer,
            ..Default::default()
        },
    )
    .await
    .unwrap()
    .account;

    //
    // TEST: Deposit funds
    //
    let deposit_amount_initial = 100;
    {
        let start_balance = solana.token_account_balance(payer_mint0_account).await;

        send_tx(
            solana,
            TokenDepositInstruction {
                amount: deposit_amount_initial,
                reduce_only: false,
                account,
                owner,
                token_account: payer_mint0_account,
                token_authority: payer.clone(),
                bank_index: 0,
            },
        )
        .await
        .unwrap();

        assert_eq!(
            solana.token_account_balance(vault).await,
            provided_amount + deposit_amount_initial
        );
        assert_eq!(
            solana.token_account_balance(payer_mint0_account).await,
            start_balance - deposit_amount_initial
        );
        assert_eq!(
            account_position(solana, account, bank).await,
            deposit_amount_initial as i64,
        );
    }

    //
    // TEST: Margin trade
    //
    let margin_account = payer_mint0_account;
    let margin_account_initial = solana.token_account_balance(margin_account).await;
    let target_token_account = context.users[0].token_accounts[0];
    let withdraw_amount = 2;
    let deposit_amount = 1;
    let send_flash_loan_tx = |solana, withdraw_amount, deposit_amount| async move {
        let mut tx = ClientTransaction::new(solana);
        let loans = vec![FlashLoanPart {
            bank,
            token_account: target_token_account,
            withdraw_amount,
        }];
        tx.add_instruction(FlashLoanBeginInstruction {
            account,
            owner,
            loans: loans.clone(),
        })
        .await;
        if withdraw_amount > 0 {
            tx.add_instruction_direct(
                spl_token::instruction::transfer(
                    &spl_token::ID,
                    &target_token_account,
                    &margin_account,
                    &owner.pubkey(),
                    &[&owner.pubkey()],
                    withdraw_amount,
                )
                .unwrap(),
            );
        }
        if deposit_amount > 0 {
            tx.add_instruction_direct(
                spl_token::instruction::transfer(
                    &spl_token::ID,
                    &margin_account,
                    &target_token_account,
                    &payer.pubkey(),
                    &[&payer.pubkey()],
                    deposit_amount,
                )
                .unwrap(),
            );
            tx.add_signer(payer);
        }
        tx.add_instruction(FlashLoanEndInstruction {
            account,
            owner,
            loans,
            // the test only accesses a single token: not a swap
            flash_loan_type: mango_v4::accounts_ix::FlashLoanType::Unknown,
        })
        .await;
        tx.send().await.unwrap();
    };
    send_flash_loan_tx(solana, withdraw_amount, deposit_amount).await;

    assert_eq!(
        solana.token_account_balance(vault).await,
        provided_amount + deposit_amount_initial - withdraw_amount + deposit_amount
    );
    assert_eq!(
        solana.token_account_balance(margin_account).await,
        margin_account_initial + withdraw_amount - deposit_amount
    );
    // no fee because user had positive balance
    assert_eq_f64!(
        account_position_f64(solana, account, bank).await,
        (deposit_amount_initial - withdraw_amount + deposit_amount) as f64,
        0.0001
    );

    //
    // TEST: Bringing the balance to 0 deactivates the token
    //
    let deposit_amount_initial = account_position(solana, account, bank).await;
    let margin_account_initial = solana.token_account_balance(margin_account).await;
    let withdraw_amount = deposit_amount_initial as u64;
    let deposit_amount = 0;
    send_flash_loan_tx(solana, withdraw_amount, deposit_amount).await;
    assert_eq!(solana.token_account_balance(vault).await, provided_amount);
    assert_eq!(
        solana.token_account_balance(margin_account).await,
        margin_account_initial + withdraw_amount
    );
    // Check that position is fully deactivated
    let account_data = get_mango_account(solana, account).await;
    assert_eq!(account_data.active_token_positions().count(), 0);

    //
    // TEST: Activating a token via margin trade
    //
    let margin_account_initial = solana.token_account_balance(margin_account).await;
    let withdraw_amount = 0;
    let deposit_amount = 100;
    send_flash_loan_tx(solana, withdraw_amount, deposit_amount).await;
    assert_eq!(
        solana.token_account_balance(vault).await,
        provided_amount + deposit_amount
    );
    assert_eq!(
        solana.token_account_balance(margin_account).await,
        margin_account_initial - deposit_amount
    );
    assert_eq_f64!(
        account_position_f64(solana, account, bank).await,
        deposit_amount as f64,
        0.0001
    );

    //
    // TEST: Try loan fees by withdrawing more than the user balance
    //
    let margin_account_initial = solana.token_account_balance(margin_account).await;
    let deposit_amount_initial = account_position(solana, account, bank).await as u64;
    let withdraw_amount = 500;
    let deposit_amount = 450;
    println!("{}", deposit_amount_initial);
    send_flash_loan_tx(solana, withdraw_amount, deposit_amount).await;
    assert_eq!(
        solana.token_account_balance(vault).await,
        provided_amount + deposit_amount_initial + deposit_amount - withdraw_amount
    );
    assert_eq!(
        solana.token_account_balance(margin_account).await,
        margin_account_initial + withdraw_amount - deposit_amount
    );
    assert_eq_f64!(
        account_position_f64(solana, account, bank).await,
        (deposit_amount_initial + deposit_amount - withdraw_amount) as f64
            - (withdraw_amount - deposit_amount_initial) as f64 * loan_origination_fee,
        0.0001
    );

    Ok(())
}

#[tokio::test]
async fn test_flash_loan_swap_fee() -> Result<(), BanksClientError> {
    let mut test_builder = TestContextBuilder::new();
    test_builder.test().set_compute_max_units(150_000);
    let context = test_builder.start_default().await;
    let solana = &context.solana.clone();

    let admin = TestKeypair::new();
    let owner = context.users[0].key;
    let payer = context.users[1].key;
    let mints = &context.mints[0..2];
    let owner_accounts = context.users[0].token_accounts.clone();
    let payer_accounts = context.users[1].token_accounts.clone();

    //
    // SETUP: Create a group, account, register a token (mint0)
    //

    let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
        admin,
        payer,
        mints: mints.to_vec(),
        ..GroupWithTokensConfig::default()
    }
    .create(solana)
    .await;

    let swap_fee_rate = 0.042f64;
    send_tx(
        solana,
        TokenEdit {
            group,
            admin,
            mint: tokens[1].mint.pubkey,
            fallback_oracle: Pubkey::default(),
            options: mango_v4::instruction::TokenEdit {
                flash_loan_swap_fee_rate_opt: Some(swap_fee_rate as f32),
                ..token_edit_instruction_default()
            },
        },
    )
    .await
    .unwrap();

    //
    // provide some funds for tokens, so the test user can borrow
    //
    let provided_amount = 10000;
    create_funded_account(
        &solana,
        group,
        owner,
        1,
        &context.users[1],
        mints,
        provided_amount,
        0,
    )
    .await;

    //
    // create the test user account
    //

    let initial_deposit = 5000;
    let account = create_funded_account(
        &solana,
        group,
        owner,
        0,
        &context.users[1],
        &mints[0..1],
        initial_deposit,
        0,
    )
    .await;

    //
    // TEST: flash loan swap
    //
    let initial_owner_balance0 = solana.token_account_balance(owner_accounts[0]).await;
    let initial_owner_balance1 = solana.token_account_balance(owner_accounts[1]).await;
    let initial_payer_balance0 = solana.token_account_balance(payer_accounts[0]).await;
    let initial_payer_balance1 = solana.token_account_balance(payer_accounts[1]).await;

    let withdraw_amount = 1000;
    let deposit_amount = 1000;
    {
        let mut tx = ClientTransaction::new(solana);
        let loans = vec![
            FlashLoanPart {
                bank: tokens[0].bank,
                token_account: owner_accounts[0],
                withdraw_amount,
            },
            FlashLoanPart {
                bank: tokens[1].bank,
                token_account: owner_accounts[1],
                withdraw_amount: 0,
            },
        ];
        tx.add_instruction(FlashLoanBeginInstruction {
            account,
            owner,
            loans: loans.clone(),
        })
        .await;
        if withdraw_amount > 0 {
            tx.add_instruction_direct(
                spl_token::instruction::transfer(
                    &spl_token::ID,
                    &owner_accounts[0],
                    &payer_accounts[0],
                    &owner.pubkey(),
                    &[&owner.pubkey()],
                    withdraw_amount,
                )
                .unwrap(),
            );
        }
        if deposit_amount > 0 {
            tx.add_instruction_direct(
                spl_token::instruction::transfer(
                    &spl_token::ID,
                    &payer_accounts[1],
                    &owner_accounts[1],
                    &payer.pubkey(),
                    &[&payer.pubkey()],
                    deposit_amount,
                )
                .unwrap(),
            );
            tx.add_signer(payer);
        }
        tx.add_instruction(FlashLoanEndInstruction {
            account,
            owner,
            loans,
            flash_loan_type: mango_v4::accounts_ix::FlashLoanType::Swap,
        })
        .await;
        tx.send().await.unwrap();
    }

    let after_owner_balance0 = solana.token_account_balance(owner_accounts[0]).await;
    let after_owner_balance1 = solana.token_account_balance(owner_accounts[1]).await;
    let after_payer_balance0 = solana.token_account_balance(payer_accounts[0]).await;
    let after_payer_balance1 = solana.token_account_balance(payer_accounts[1]).await;

    assert_eq!(after_owner_balance0, initial_owner_balance0);
    assert_eq!(after_owner_balance1, initial_owner_balance1);
    assert_eq!(
        after_payer_balance0,
        initial_payer_balance0 + withdraw_amount
    );
    assert_eq!(
        after_payer_balance1,
        initial_payer_balance1 - deposit_amount
    );

    assert_eq!(
        solana.token_account_balance(tokens[0].vault).await,
        provided_amount + initial_deposit - withdraw_amount
    );
    assert_eq!(
        solana.token_account_balance(tokens[1].vault).await,
        provided_amount + deposit_amount
    );

    let mango_withdraw_amount = account_position_f64(solana, account, tokens[0].bank).await;
    assert_eq_f64!(
        mango_withdraw_amount,
        initial_deposit as f64 - withdraw_amount as f64 * (1.0 + swap_fee_rate),
        0.0001
    );

    let mango_deposit_amount = account_position_f64(solana, account, tokens[1].bank).await;
    assert_eq_f64!(mango_deposit_amount, deposit_amount as f64, 0.0001);

    Ok(())
}

#[tokio::test]
async fn test_flash_loan_creates_ata_accounts() -> Result<(), BanksClientError> {
    let mut test_builder = TestContextBuilder::new();
    test_builder.test().set_compute_max_units(200_000); // ata::create_idempotent adds a lot!
    let context = test_builder.start_default().await;
    let solana = &context.solana.clone();

    let admin = TestKeypair::new();
    let owner = context.users[0].key;
    let payer = context.users[1].key;
    let mints = &context.mints[0..2];
    let payer_mint0_account = context.users[1].token_accounts[0];
    let payer_mint1_account = context.users[1].token_accounts[1];

    //
    // SETUP: Create a group, account, register a token (mint0)
    //

    let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
        admin,
        payer,
        mints: mints.to_vec(),
        ..GroupWithTokensConfig::default()
    }
    .create(solana)
    .await;
    let bank = tokens[0].bank;
    let vault = tokens[0].vault;

    //
    // provide some funds for tokens, so the test user can borrow
    //
    let provided_amount = 1000;
    create_funded_account(
        &solana,
        group,
        payer,
        1,
        &context.users[1],
        mints,
        provided_amount,
        0,
    )
    .await;

    //
    // create the test user account
    //
    let account = send_tx(
        solana,
        AccountCreateInstruction {
            account_num: 0,
            group,
            owner,
            payer,
            ..Default::default()
        },
    )
    .await
    .unwrap()
    .account;

    //
    // TEST: Deposit funds
    //
    let deposit_amount_initial = 100;
    {
        let start_balance = solana.token_account_balance(payer_mint0_account).await;

        send_tx(
            solana,
            TokenDepositInstruction {
                amount: deposit_amount_initial,
                reduce_only: false,
                account,
                owner,
                token_account: payer_mint0_account,
                token_authority: payer.clone(),
                bank_index: 0,
            },
        )
        .await
        .unwrap();

        assert_eq!(
            solana.token_account_balance(vault).await,
            provided_amount + deposit_amount_initial
        );
        assert_eq!(
            solana.token_account_balance(payer_mint0_account).await,
            start_balance - deposit_amount_initial
        );
        assert_eq!(
            account_position(solana, account, bank).await,
            deposit_amount_initial as i64,
        );
    }

    //
    // SETUP: Wipe owner ATAs that are set up by default
    //
    use solana_sdk::account::AccountSharedData;
    let owner_token0_ata = context.users[0].token_accounts[0];
    let owner_token1_ata = context.users[0].token_accounts[1];
    solana
        .context
        .borrow_mut()
        .set_account(&owner_token0_ata, &AccountSharedData::default());
    solana
        .context
        .borrow_mut()
        .set_account(&owner_token1_ata, &AccountSharedData::default());
    assert!(solana.get_account_data(owner_token0_ata).await.is_none());
    assert!(solana.get_account_data(owner_token1_ata).await.is_none());

    //
    // TEST: Margin trade
    //
    let in_bank = tokens[0].bank;
    let out_bank = tokens[1].bank;
    let in_token = owner_token0_ata;
    let out_token = owner_token1_ata;
    let withdraw_amount = 2;
    let deposit_amount = 1;
    let send_flash_loan_tx = |solana, withdraw_amount, deposit_amount| async move {
        let mut tx = ClientTransaction::new(solana);
        tx.add_instruction(FlashLoanSwapBeginInstruction {
            account,
            owner,
            in_bank,
            out_bank,
            in_loan: withdraw_amount,
        })
        .await;
        if withdraw_amount > 0 {
            tx.add_instruction_direct(
                spl_token::instruction::transfer(
                    &spl_token::ID,
                    &in_token,
                    &payer_mint0_account,
                    &owner.pubkey(),
                    &[&owner.pubkey()],
                    withdraw_amount,
                )
                .unwrap(),
            );
        }
        if deposit_amount > 0 {
            tx.add_instruction_direct(
                spl_token::instruction::transfer(
                    &spl_token::ID,
                    &payer_mint1_account,
                    &out_token,
                    &payer.pubkey(),
                    &[&payer.pubkey()],
                    deposit_amount,
                )
                .unwrap(),
            );
            tx.add_signer(payer);
        }
        tx.add_instruction(FlashLoanEndInstruction {
            account,
            owner,
            loans: vec![
                FlashLoanPart {
                    bank: in_bank,
                    token_account: in_token,
                    withdraw_amount,
                },
                FlashLoanPart {
                    bank: out_bank,
                    token_account: out_token,
                    withdraw_amount: 0,
                },
            ],
            flash_loan_type: mango_v4::accounts_ix::FlashLoanType::Swap,
        })
        .await;
        tx.send().await.unwrap();
    };
    send_flash_loan_tx(solana, withdraw_amount, deposit_amount).await;

    // ATAs were created
    assert!(solana.get_account_data(owner_token0_ata).await.is_some());
    assert!(solana.get_account_data(owner_token1_ata).await.is_some());

    // running the same tx again will still work
    send_flash_loan_tx(solana, withdraw_amount, deposit_amount).await;

    Ok(())
}

#[tokio::test]
async fn test_margin_trade_deposit_limit() -> Result<(), BanksClientError> {
    let mut test_builder = TestContextBuilder::new();
    test_builder.test().set_compute_max_units(100_000);
    let context = test_builder.start_default().await;
    let solana = &context.solana.clone();

    let admin = TestKeypair::new();
    let owner = context.users[0].key;
    let payer = context.users[1].key;
    let mints = &context.mints[0..2];
    let payer_mint0_account = context.users[1].token_accounts[0];

    //
    // SETUP: Create a group, account, register a token (mint0)
    //

    let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
        admin,
        payer,
        mints: mints.to_vec(),
        ..GroupWithTokensConfig::default()
    }
    .create(solana)
    .await;
    let bank = tokens[0].bank;

    //
    // SETUP: deposit limit
    //
    send_tx(
        solana,
        TokenEdit {
            group,
            admin,
            mint: tokens[0].mint.pubkey,
            fallback_oracle: Pubkey::default(),
            options: mango_v4::instruction::TokenEdit {
                deposit_limit_opt: Some(1000),
                ..token_edit_instruction_default()
            },
        },
    )
    .await
    .unwrap();

    //
    // create the test user account
    //

    let deposit_amount_initial = 100;
    let account = create_funded_account(
        &solana,
        group,
        owner,
        0,
        &context.users[1],
        &mints[..1],
        deposit_amount_initial,
        0,
    )
    .await;

    //
    // TEST: Margin trade
    //
    let margin_account = payer_mint0_account;
    let target_token_account = context.users[0].token_accounts[0];
    let make_flash_loan_tx = |solana, deposit_amount| async move {
        let mut tx = ClientTransaction::new(solana);
        let loans = vec![FlashLoanPart {
            bank,
            token_account: target_token_account,
            withdraw_amount: 0,
        }];
        tx.add_instruction(FlashLoanBeginInstruction {
            account,
            owner,
            loans: loans.clone(),
        })
        .await;
        tx.add_instruction_direct(
            spl_token::instruction::transfer(
                &spl_token::ID,
                &margin_account,
                &target_token_account,
                &payer.pubkey(),
                &[&payer.pubkey()],
                deposit_amount,
            )
            .unwrap(),
        );
        tx.add_signer(payer);
        tx.add_instruction(FlashLoanEndInstruction {
            account,
            owner,
            loans,
            // the test only accesses a single token: not a swap
            flash_loan_type: mango_v4::accounts_ix::FlashLoanType::Unknown,
        })
        .await;
        tx
    };

    make_flash_loan_tx(solana, 901)
        .await
        .send_expect_error(MangoError::BankDepositLimit)
        .await
        .unwrap();

    make_flash_loan_tx(solana, 899).await.send().await.unwrap();

    Ok(())
}

#[tokio::test]
async fn test_margin_trade_skip_bank() -> Result<(), BanksClientError> {
    let mut test_builder = TestContextBuilder::new();
    test_builder.test().set_compute_max_units(100_000);
    let context = test_builder.start_default().await;
    let solana = &context.solana.clone();

    let admin = TestKeypair::new();
    let owner = context.users[0].key;
    let payer = context.users[1].key;
    let mints = &context.mints[0..2];
    let payer_mint0_account = context.users[1].token_accounts[0];

    //
    // SETUP: Create a group, account, register a token (mint0)
    //

    let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
        admin,
        payer,
        mints: mints.to_vec(),
        ..GroupWithTokensConfig::default()
    }
    .create(solana)
    .await;
    let bank = tokens[0].bank;

    //
    // create the test user account
    //

    let deposit_amount_initial = 100;
    let account = create_funded_account(
        &solana,
        group,
        owner,
        0,
        &context.users[1],
        &mints,
        deposit_amount_initial,
        0,
    )
    .await;

    //
    // TEST: Margin trade
    //
    let margin_account = payer_mint0_account;
    let target_token_account = context.users[0].token_accounts[0];
    let make_flash_loan_tx = |solana, deposit_amount, skip_banks| async move {
        let mut tx = ClientTransaction::new(solana);
        let loans = vec![FlashLoanPart {
            bank,
            token_account: target_token_account,
            withdraw_amount: 0,
        }];
        tx.add_instruction(FlashLoanBeginInstruction {
            account,
            owner,
            loans: loans.clone(),
        })
        .await;
        tx.add_instruction_direct(
            spl_token::instruction::transfer(
                &spl_token::ID,
                &margin_account,
                &target_token_account,
                &payer.pubkey(),
                &[&payer.pubkey()],
                deposit_amount,
            )
            .unwrap(),
        );
        tx.add_signer(payer);
        tx.add_instruction(HealthAccountSkipping {
            inner: FlashLoanEndInstruction {
                account,
                owner,
                loans,
                // the test only accesses a single token: not a swap
                flash_loan_type: mango_v4::accounts_ix::FlashLoanType::Unknown,
            },
            skip_banks,
        })
        .await;
        tx
    };

    make_flash_loan_tx(solana, 1, vec![])
        .await
        .send()
        .await
        .unwrap();

    make_flash_loan_tx(solana, 1, vec![tokens[1].bank])
        .await
        .send()
        .await
        .unwrap();

    make_flash_loan_tx(solana, 1, vec![tokens[0].bank])
        .await
        .send_expect_error(MangoError::InvalidBank)
        .await
        .unwrap();

    Ok(())
}
