use std::ops::DerefMut;

use anchor_lang::prelude::*;
use anchor_spl::token::{self, TokenAccount};

use fixed::types::I80F48;

use crate::accounts_ix::*;
use crate::accounts_zerocopy::AccountInfoRef;
use crate::error::*;
use crate::health::*;
use crate::logs::{
    emit_perp_balances, emit_stack, PerpLiqBankruptcyLog, PerpLiqNegativePnlOrBankruptcyLog,
    TokenBalanceLog,
};
use crate::state::*;

pub fn perp_liq_negative_pnl_or_bankruptcy(
    ctx: Context<PerpLiqNegativePnlOrBankruptcyV2>,
    max_liab_transfer: u64,
) -> Result<()> {
    let mango_group = ctx.accounts.group.key();

    let now_slot = Clock::get()?.slot;
    let now_ts = Clock::get()?.unix_timestamp.try_into().unwrap();

    let perp_market_index;
    let settle_token_index;
    let perp_oracle_price;
    let settle_token_oracle_price;
    let insurance_token_oracle_price;
    {
        let perp_market = ctx.accounts.perp_market.load()?;
        perp_market_index = perp_market.perp_market_index;
        settle_token_index = perp_market.settle_token_index;
        let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;
        perp_oracle_price = perp_market.oracle_price(
            &OracleAccountInfos::from_reader(oracle_ref),
            Some((now_ts, now_slot)),
        )?;

        let settle_bank = ctx.accounts.settle_bank.load()?;
        let settle_oracle_ref = &AccountInfoRef::borrow(ctx.accounts.settle_oracle.as_ref())?;
        settle_token_oracle_price = settle_bank.oracle_price(
            &OracleAccountInfos::from_reader(settle_oracle_ref),
            Some((now_ts, now_slot)),
        )?;
        drop(settle_bank); // could be the same as insurance_bank

        let insurance_bank = ctx.accounts.insurance_bank.load()?;
        let insurance_oracle_ref = &AccountInfoRef::borrow(ctx.accounts.insurance_oracle.as_ref())?;
        // We're not getting the insurance token price from the HealthCache because
        // the liqee isn't guaranteed to have an insurance fund token position.
        insurance_token_oracle_price = insurance_bank.oracle_price(
            &OracleAccountInfos::from_reader(insurance_oracle_ref),
            Some((now_ts, now_slot)),
        )?;
    }

    require_keys_neq!(ctx.accounts.liqor.key(), ctx.accounts.liqee.key());
    let mut liqee = ctx.accounts.liqee.load_full_mut()?;
    let mut liqor = ctx.accounts.liqor.load_full_mut()?;
    // account constraint #1
    require!(
        liqor
            .fixed
            .is_owner_or_delegate(ctx.accounts.liqor_owner.key()),
        MangoError::SomeError
    );
    require_msg_typed!(
        !liqor.fixed.being_liquidated(),
        MangoError::BeingLiquidated,
        "liqor account"
    );

    let retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, &mango_group)
        .context("create account retriever")?;
    let mut liqee_health_cache = new_health_cache(&liqee.borrow(), &retriever, now_ts)?;
    drop(retriever);
    let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);

    // Guarantees that perp base position is 0 and perp quote position is <= 0.
    liqee_health_cache.require_after_phase2_liquidation()?;

    if liqee.check_liquidatable(&liqee_health_cache)? != CheckLiquidatable::Liquidatable {
        return Ok(());
    }

    // check positions exist/create them, done early for nicer error messages
    {
        liqee.perp_position(perp_market_index)?;
        liqee.token_position(settle_token_index)?;
        liqor.ensure_perp_position(perp_market_index, settle_token_index)?;
        liqor.ensure_token_position(settle_token_index)?;
    }

    let mut perp_market = ctx.accounts.perp_market.load_mut()?;

    let (settlement, insurance_transfer) = {
        let mut settle_bank = ctx.accounts.settle_bank.load_mut()?;
        let mut insurance_bank_opt =
            if ctx.accounts.settle_bank.key() != ctx.accounts.insurance_bank.key() {
                Some(ctx.accounts.insurance_bank.load_mut()?)
            } else {
                None
            };
        liquidation_action(
            ctx.accounts.group.key(),
            &mut perp_market,
            perp_oracle_price,
            &mut settle_bank,
            settle_token_oracle_price,
            insurance_bank_opt.as_mut().map(|v| v.deref_mut()),
            insurance_token_oracle_price,
            &ctx.accounts.insurance_vault,
            &mut liqor.borrow_mut(),
            ctx.accounts.liqor.key(),
            &mut liqee.borrow_mut(),
            ctx.accounts.liqee.key(),
            &mut liqee_health_cache,
            liqee_liq_end_health,
            now_ts,
            max_liab_transfer,
        )?
    };

    // Execute the insurance fund transfer if needed
    if insurance_transfer > 0 {
        let group = ctx.accounts.group.load()?;
        let group_seeds = group_seeds!(group);
        token::transfer(
            ctx.accounts.transfer_ctx().with_signer(&[group_seeds]),
            insurance_transfer,
        )?;
    }

    //
    // Log positions afterwards
    //
    if settlement > 0 {
        let settle_bank = ctx.accounts.settle_bank.load()?;
        let liqor_token_position = liqor.token_position(settle_token_index)?;
        emit_stack(TokenBalanceLog {
            mango_group,
            mango_account: ctx.accounts.liqor.key(),
            token_index: settle_token_index,
            indexed_position: liqor_token_position.indexed_position.to_bits(),
            deposit_index: settle_bank.deposit_index.to_bits(),
            borrow_index: settle_bank.borrow_index.to_bits(),
        });

        let liqee_token_position = liqee.token_position(settle_token_index)?;
        emit_stack(TokenBalanceLog {
            mango_group,
            mango_account: ctx.accounts.liqee.key(),
            token_index: settle_token_index,
            indexed_position: liqee_token_position.indexed_position.to_bits(),
            deposit_index: settle_bank.deposit_index.to_bits(),
            borrow_index: settle_bank.borrow_index.to_bits(),
        });
    }

    if insurance_transfer > 0 {
        let insurance_bank = ctx.accounts.insurance_bank.load()?;
        let liqor_token_position = liqor.token_position(insurance_bank.token_index)?;
        emit_stack(TokenBalanceLog {
            mango_group,
            mango_account: ctx.accounts.liqor.key(),
            token_index: insurance_bank.token_index,
            indexed_position: liqor_token_position.indexed_position.to_bits(),
            deposit_index: insurance_bank.deposit_index.to_bits(),
            borrow_index: insurance_bank.borrow_index.to_bits(),
        });
    }

    let liqee_perp_position = liqee.perp_position(perp_market_index)?;
    let liqor_perp_position = liqor.perp_position(perp_market_index)?;
    emit_perp_balances(
        mango_group,
        ctx.accounts.liqor.key(),
        liqor_perp_position,
        &perp_market,
    );
    emit_perp_balances(
        mango_group,
        ctx.accounts.liqee.key(),
        liqee_perp_position,
        &perp_market,
    );

    // Check liqee health again: bankruptcy would improve health
    liqee_health_cache.recompute_perp_info(liqee_perp_position, &perp_market)?;
    let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
    liqee
        .fixed
        .maybe_recover_from_being_liquidated(liqee_liq_end_health);

    drop(perp_market);

    // Check liqor's health
    if !liqor.fixed.is_in_health_region() {
        let account_retriever =
            ScanningAccountRetriever::new(ctx.remaining_accounts, &mango_group)?;
        let liqor_health = compute_health(
            &liqor.borrow(),
            HealthType::Init,
            &account_retriever,
            now_ts,
        )
        .context("compute liqor health")?;
        require!(liqor_health >= 0, MangoError::HealthMustBePositive);
    }

    Ok(())
}

pub(crate) fn liquidation_action(
    group_key: Pubkey,
    perp_market: &mut PerpMarket,
    perp_oracle_price: I80F48,
    settle_bank: &mut Bank,
    settle_token_oracle_price: I80F48,
    insurance_bank_opt: Option<&mut Bank>,
    insurance_token_oracle_price: I80F48,
    insurance_vault: &TokenAccount,
    liqor: &mut MangoAccountRefMut,
    liqor_key: Pubkey,
    liqee: &mut MangoAccountRefMut,
    liqee_key: Pubkey,
    liqee_health_cache: &mut HealthCache,
    liqee_liq_end_health: I80F48,
    now_ts: u64,
    max_liab_transfer: u64,
) -> Result<(I80F48, u64)> {
    let perp_market_index = perp_market.perp_market_index;
    let settle_token_index = perp_market.settle_token_index;
    let liqee_max_settle = liqee_health_cache.perp_max_settle(settle_token_index)?;
    let liqee_health_token_balances =
        liqee_health_cache.effective_token_balances(HealthType::LiquidationEnd);

    //
    // Step 1: Allow the liqor to take over ("settle") negative liqee pnl.
    //
    // The only limitation is the liqee's perp_max_settle and its perp pnl settle limit.
    // This does not change liqee health.
    //
    let settlement;
    let max_settlement_liqee;
    let mut liqee_pnl;
    {
        let liqee_perp_position = liqee.perp_position_mut(perp_market_index)?;
        let liqor_perp_position = liqor.perp_position_mut(perp_market_index)?;
        liqee_perp_position.settle_funding(&perp_market);
        liqor_perp_position.settle_funding(&perp_market);

        liqee_pnl = liqee_perp_position.unsettled_pnl(&perp_market, perp_oracle_price)?;
        require_gt!(0, liqee_pnl, MangoError::ProfitabilityMismatch);

        // Get settleable pnl on the liqee
        liqee_perp_position.update_settle_limit(&perp_market, now_ts);
        let liqee_settleable_pnl =
            liqee_perp_position.apply_pnl_settle_limit(&perp_market, liqee_pnl);

        max_settlement_liqee = liqee_max_settle
            .min(-liqee_settleable_pnl)
            .max(I80F48::ZERO);
        settlement = max_settlement_liqee
            .min(I80F48::from(max_liab_transfer))
            .max(I80F48::ZERO);
        if settlement > 0 {
            liqor_perp_position.record_liquidation_quote_change(-settlement);
            liqee_perp_position.record_settle(-settlement, &perp_market);

            // Update the accounts' perp_spot_transfer statistics.
            let settlement_i64 = settlement.round_to_zero().to_num::<i64>();
            liqor_perp_position.perp_spot_transfers += settlement_i64;
            liqee_perp_position.perp_spot_transfers -= settlement_i64;
            liqor.fixed.perp_spot_transfers += settlement_i64;
            liqee.fixed.perp_spot_transfers -= settlement_i64;

            // Transfer token balance
            let liqor_token_position = liqor.token_position_mut(settle_token_index)?.0;
            let liqee_token_position = liqee.token_position_mut(settle_token_index)?.0;
            settle_bank.deposit(liqor_token_position, settlement, now_ts)?;
            settle_bank.withdraw_without_fee(liqee_token_position, settlement, now_ts)?;
            liqee_health_cache.adjust_token_balance(&settle_bank, -settlement)?;

            emit_stack(PerpLiqNegativePnlOrBankruptcyLog {
                mango_group: group_key,
                liqee: liqee_key,
                liqor: liqor_key,
                perp_market_index,
                settlement: settlement.to_bits(),
            });

            liqee_pnl += settlement;
            msg!("liquidated pnl = {}", settlement);
        }
    };
    let max_liab_transfer = I80F48::from(max_liab_transfer) - settlement;

    // Step 2: bankruptcy
    //
    // If the liqee still has negative pnl and couldn't possibly be settled further, allow bankruptcy
    // to reduce the negative pnl.
    //
    // Remaining pnl that brings the account into negative init health is either:
    // - taken by the liqor in exchange for spot from the insurance fund, or
    // - wiped away and socialized among all perp participants (this does not involve the liqor)
    //
    let insurance_transfer;
    if settlement == max_settlement_liqee && liqee_pnl < 0 {
        // Preparation that's needed for both, insurance fund based pnl takeover and socialized loss

        let liqee_settle_token_balance = liqee_health_token_balances
            [liqee_health_cache.token_info_index(settle_token_index)?]
        .spot_and_perp;
        let liqee_perp_position = liqee.perp_position_mut(perp_market_index)?;

        // recompute for safety
        liqee_pnl = liqee_perp_position.unsettled_pnl(&perp_market, perp_oracle_price)?;

        // Each unit of pnl increase (towards 0) increases health, but the amount depends on whether
        // the health token position is negative or positive.
        // Compute how much pnl would need to be increased to reach liq end health 0 (while ignoring
        // liqee_pnl and other constraints initially, those are applied below)
        let max_for_health = {
            let liab_weighted_price = settle_token_oracle_price * settle_bank.init_liab_weight;
            let asset_weighted_price = settle_token_oracle_price * settle_bank.init_asset_weight;
            spot_amount_given_for_health_zero(
                liqee_liq_end_health,
                liqee_settle_token_balance,
                asset_weighted_price,
                liab_weighted_price,
            )?
        };

        let max_liab_transfer_from_liqee = (-liqee_pnl).min(max_for_health).max(I80F48::ZERO);

        let max_liab_transfer_to_liqor = max_liab_transfer_from_liqee
            .min(max_liab_transfer)
            .max(I80F48::ZERO);

        // Check if the insurance fund can be used to reimburse the liqor for taking on negative pnl

        // Available insurance fund coverage
        let insurance_vault_amount = if perp_market.elligible_for_group_insurance_fund() {
            insurance_vault.amount
        } else {
            0
        };

        let liquidation_fee_factor = I80F48::ONE + perp_market.base_liquidation_fee;
        let settle_token_price_with_fee = settle_token_oracle_price * liquidation_fee_factor;

        // Amount given to the liqor from the insurance fund
        insurance_transfer = (max_liab_transfer_to_liqor * settle_token_price_with_fee
            / insurance_token_oracle_price)
            .ceil()
            .to_num::<u64>()
            .min(insurance_vault_amount);

        let insurance_transfer_i80f48 = I80F48::from(insurance_transfer);
        let insurance_fund_exhausted = insurance_transfer == insurance_vault_amount;

        // Amount of negative perp pnl transfered to the liqor
        let insurance_liab_transfer = (insurance_transfer_i80f48 * insurance_token_oracle_price
            / settle_token_price_with_fee)
            .min(max_liab_transfer_to_liqor);

        // Try using the insurance fund if possible
        if insurance_transfer > 0 {
            let insurance_bank = insurance_bank_opt.unwrap_or(settle_bank);
            require_keys_eq!(insurance_bank.mint, insurance_vault.mint);

            // moving insurance assets into the insurance bank vault happens outside
            // of this function to ensure this is unittestable!

            // credit the liqor with quote tokens
            let (liqor_quote, _, _) = liqor.ensure_token_position(insurance_bank.token_index)?;
            insurance_bank.deposit(liqor_quote, insurance_transfer_i80f48, now_ts)?;

            // transfer perp quote loss from the liqee to the liqor
            let liqor_perp_position = liqor.perp_position_mut(perp_market_index)?;
            liqee_perp_position.record_settle(-insurance_liab_transfer, &perp_market);
            liqor_perp_position.record_liquidation_quote_change(-insurance_liab_transfer);

            msg!(
                "bankruptcy: {} pnl for {} insurance",
                insurance_liab_transfer,
                insurance_transfer
            );
        }

        // Socialize loss if the insurance fund is exhausted

        // At this point, we don't care about the liqor's requested max_liab_tranfer
        let remaining_liab = max_liab_transfer_from_liqee - insurance_liab_transfer;
        let mut socialized_loss = I80F48::ZERO;
        let (starting_long_funding, starting_short_funding) =
            (perp_market.long_funding, perp_market.short_funding);
        if insurance_fund_exhausted && remaining_liab > 0 {
            perp_market.socialize_loss(-remaining_liab)?;
            liqee_perp_position.record_settle(-remaining_liab, &perp_market);
            socialized_loss = remaining_liab;
            msg!("socialized loss: {}", socialized_loss);
        }

        emit_stack(PerpLiqBankruptcyLog {
            mango_group: group_key,
            liqee: liqee_key,
            liqor: liqor_key,
            perp_market_index: perp_market.perp_market_index,
            insurance_transfer: insurance_transfer_i80f48.to_bits(),
            socialized_loss: socialized_loss.to_bits(),
            starting_long_funding: starting_long_funding.to_bits(),
            starting_short_funding: starting_short_funding.to_bits(),
            ending_long_funding: perp_market.long_funding.to_bits(),
            ending_short_funding: perp_market.short_funding.to_bits(),
        });
    } else {
        insurance_transfer = 0;
    };

    Ok((settlement, insurance_transfer))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::health::{self, test::*};

    #[derive(Clone)]
    struct TestSetup {
        group: Pubkey,
        insurance_bank: TestAccount<Bank>,
        settle_bank: TestAccount<Bank>,
        other_bank: TestAccount<Bank>,
        insurance_oracle: TestAccount<StubOracle>,
        settle_oracle: TestAccount<StubOracle>,
        other_oracle: TestAccount<StubOracle>,
        perp_market: TestAccount<PerpMarket>,
        perp_oracle: TestAccount<StubOracle>,
        liqee: MangoAccountValue,
        liqor: MangoAccountValue,
        insurance_vault: spl_token::state::Account,
    }

    impl TestSetup {
        fn new() -> Self {
            let group = Pubkey::new_unique();
            let (mut insurance_bank, insurance_oracle) =
                mock_bank_and_oracle(group, 0, 1.0, 0.0, 0.0);
            let (settle_bank, settle_oracle) = mock_bank_and_oracle(group, 1, 1.0, 0.0, 0.0);

            let (_bank3, perp_oracle) = mock_bank_and_oracle(group, 4, 1.0, 0.5, 0.3);
            let mut perp_market =
                mock_perp_market(group, perp_oracle.pubkey, 1.0, 9, (0.2, 0.1), (0.2, 0.1));
            perp_market.data().settle_token_index = 1;
            perp_market.data().base_lot_size = 1;
            perp_market.data().group_insurance_fund = 1;

            let (other_bank, other_oracle) = mock_bank_and_oracle(group, 2, 1.0, 0.0, 0.0);

            let liqee_buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
            let mut liqee = MangoAccountValue::from_bytes(&liqee_buffer).unwrap();
            {
                liqee.ensure_token_position(1).unwrap();
                liqee.ensure_token_position(2).unwrap();
                liqee.ensure_perp_position(9, 1).unwrap();
            }

            let liqor_buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
            let mut liqor = MangoAccountValue::from_bytes(&liqor_buffer).unwrap();
            {
                liqor.ensure_token_position(0).unwrap();
                liqor.ensure_token_position(1).unwrap();
                liqor.ensure_perp_position(9, 1).unwrap();
            }

            let mut insurance_vault = spl_token::state::Account::default();
            insurance_vault.state = spl_token::state::AccountState::Initialized;
            insurance_vault.mint = insurance_bank.data().mint;

            Self {
                group,
                insurance_bank,
                settle_bank,
                other_bank,
                insurance_oracle,
                settle_oracle,
                other_oracle,
                perp_market,
                perp_oracle,
                liqee,
                liqor,
                insurance_vault,
            }
        }

        fn run(&self, max_liab_transfer: u64) -> Result<Self> {
            let mut setup = self.clone();

            let mut liqee_health_cache;
            let liqee_liq_end_health;
            {
                let ais = vec![
                    setup.insurance_bank.as_account_info(),
                    setup.settle_bank.as_account_info(),
                    setup.other_bank.as_account_info(),
                    setup.insurance_oracle.as_account_info(),
                    setup.settle_oracle.as_account_info(),
                    setup.other_oracle.as_account_info(),
                    setup.perp_market.as_account_info(),
                    setup.perp_oracle.as_account_info(),
                ];
                let retriever =
                    ScanningAccountRetriever::new_with_staleness(&ais, &setup.group, None).unwrap();

                liqee_health_cache =
                    health::new_health_cache(&setup.liqee.borrow(), &retriever, 0).unwrap();
                liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
            }

            let insurance_price = {
                let insurance_oracle_ai = setup.insurance_oracle.as_account_info();
                let insurance_oracle_ref = &AccountInfoRef::borrow(&insurance_oracle_ai)?;
                setup
                    .insurance_bank
                    .data()
                    .oracle_price(&OracleAccountInfos::from_reader(insurance_oracle_ref), None)
                    .unwrap()
            };
            let settle_price = {
                let settle_oracle_ai = setup.settle_oracle.as_account_info();
                let settle_oracle_ref = &AccountInfoRef::borrow(&settle_oracle_ai)?;
                setup
                    .settle_bank
                    .data()
                    .oracle_price(&OracleAccountInfos::from_reader(settle_oracle_ref), None)
                    .unwrap()
            };
            let perp_price = {
                let perp_oracle_ai = setup.perp_oracle.as_account_info();
                let perp_oracle_ref = &AccountInfoRef::borrow(&perp_oracle_ai)?;
                setup
                    .perp_market
                    .data()
                    .oracle_price(&OracleAccountInfos::from_reader(perp_oracle_ref), None)
                    .unwrap()
            };

            // There's no way to construct a TokenAccount directly...
            let mut buffer = [0u8; 165];
            use solana_program::program_pack::Pack;
            setup.insurance_vault.pack_into_slice(&mut buffer);
            let insurance_vault =
                TokenAccount::try_deserialize_unchecked(&mut &buffer[..]).unwrap();

            liquidation_action(
                setup.group.key(),
                setup.perp_market.data(),
                perp_price,
                setup.settle_bank.data(),
                settle_price,
                Some(setup.insurance_bank.data()),
                insurance_price,
                &insurance_vault,
                &mut setup.liqor.borrow_mut(),
                Pubkey::new_unique(),
                &mut setup.liqee.borrow_mut(),
                Pubkey::new_unique(),
                &mut liqee_health_cache,
                liqee_liq_end_health,
                0,
                max_liab_transfer,
            )?;

            Ok(setup)
        }
    }

    fn insurance_p(account: &mut MangoAccountValue) -> &mut TokenPosition {
        account.token_position_mut(0).unwrap().0
    }
    fn settle_p(account: &mut MangoAccountValue) -> &mut TokenPosition {
        account.token_position_mut(1).unwrap().0
    }
    fn other_p(account: &mut MangoAccountValue) -> &mut TokenPosition {
        account.token_position_mut(2).unwrap().0
    }

    fn perp_p(account: &mut MangoAccountValue) -> &mut PerpPosition {
        account.perp_position_mut(9).unwrap()
    }

    macro_rules! assert_eq_f {
        ($value:expr, $expected:expr, $max_error:expr) => {
            let value = $value;
            let expected = $expected;
            let ok = (value.to_num::<f64>() - expected).abs() < $max_error;
            assert!(ok, "value: {value}, expected: {expected}");
        };
    }

    #[test]
    fn test_liq_negative_pnl_or_bankruptcy() {
        let test_cases = vec![
            (
                "nothing",
                (0.9, 1.0, 1.0),
                (0.0, 0.0, 0.0, 0, 0),
                (false, 0.0, 0.0),
                (0.0, 0.0, 0.0, 0.0),
                100,
            ),
            (
                "settle 1",
                (0.9, 2.0, 3.0),
                // perp_settle_health = 40 * 2.0 * 0.9 - 36 = 36
                // max settle = 36 / (0.9 * 2.0) = 20
                (40.0, -50.0, -36.0, 6, 100),
                (true, 30.0, -40.0),
                (10.0, -10.0, 0.0, 0.0),
                10,
            ),
            (
                "settle 2 (+insurance)",
                (0.9, 2.0, 3.0),
                // perp_settle_health = 40 * 2.0 * 0.9 - 36 = 36
                // max settle = 36 / (0.9 * 2.0) = 20
                (40.0, -50.0, -36.0, 100, 100),
                (true, 20.0, -21.0),
                (20.0, -29.0, 6.0, 0.0),
                29, // limited by max_liab_transfer
            ),
            (
                "settle 3 (+insurance)",
                (0.9, 2.0, 3.0),
                // perp_settle_health = 40 * 2.0 * 0.9 - 36 = 36
                // max settle = 36 / (0.9 * 2.0) = 20
                (40.0, -50.0, -36.0, 100, 10), // limited by settleable pnl
                (true, 30.0, -31.0),
                (10.0, -19.0, 6.0, 0.0),
                19,
            ),
            (
                "settle 4 (+socialized loss)",
                (0.9, 2.0, 3.0),
                // perp_settle_health = 5 * 2.0 * 0.9 + 30 = 39
                // max settle = 5 + (39 - 9) / (2*1.1) = 18.63
                (5.0, -20.0, 30.0, 0, 100),
                (true, -13.63, 0.0),
                (18.63, -18.63, 0.0, 1.36), // socialized loss
                100,
            ),
            (
                "bankruptcy, no insurance 1",
                (0.9, 2.0, 1.0),
                (0.0, -5.0, 2.2, 0, 0),
                (true, 0.0, -1.0), // -1 * 2.0 * 1.1 = 2.2
                (0.0, 0.0, 0.0, 4.0),
                0,
            ),
            (
                "bankruptcy, no insurance 2",
                (0.9, 2.0, 1.0),
                (4.0, -5.0, -3.6, 0, 0), // health token balance goes from -1 to +2
                (true, 4.0, -2.0),
                (0.0, 0.0, 0.0, 3.0),
                0,
            ),
            (
                "bankruptcy, no insurance 3",
                (0.9, 2.0, 1.0),
                (4.0, -5.0, -3.6, 0, 0),
                (true, 4.0, -2.0),
                (0.0, 0.0, 0.0, 3.0),
                100, // liqor being willing to take over changes nothing
            ),
            (
                "bankruptcy, with insurance 1",
                (0.9, 2.0, 3.0),
                (40.0, -50.0, -36.0, 6, 0),
                (true, 40.0, -20.0),
                (0.0, -9.0, 6.0, 21.0), // 6 * 3.0 / 2.0 = 9 taken over, rest socialized loss
                100,
            ),
            (
                "bankruptcy, with insurance 2",
                (0.9, 2.0, 3.0),
                (40.0, -50.0, -36.0, 6, 0),
                (true, 40.0, -47.0),
                (0.0, -3.0, 2.0, 0.0),
                3, // liqor is limited, don't socialize loss since insurance not exhausted!
            ),
            (
                "bankruptcy, with insurance 3",
                (0.9, 2.0, 3.0),
                (40.0, -50.0, -36.0, 1000, 0), // insurance fund is big enough to cover fully
                (true, 40.0, -20.0),
                (0.0, -30.0, 20.0, 0.0),
                100,
            ),
            (
                "everything 1",
                (0.9, 2.0, 3.0),
                // perp_settle_health = 40 * 2.0 * 0.9 - 36 = 36
                // max settle = 36 / (0.9 * 2.0) = 20
                (40.0, -50.0, -36.0, 6, 100),
                (true, 20.0, 0.0),
                (20.0, -29.0, 6.0, 21.0),
                40,
            ),
            (
                "everything 2",
                (0.9, 2.0, 3.0),
                // perp_settle_health = 10 * 2.0 * 0.9 + 9 = 19
                // max settle = 10 + 9 / (1.1 * 2.0) = 14.1
                (10.0, -50.0, 9.0, 6, 100),
                (true, -4.1, 0.0), // perp position always goes to 0 because we use the same weights for init and maint
                (14.1, -14.1 - 9.0, 6.0, 50.0 - 14.1 - 9.0),
                40,
            ),
        ];

        for (
            name,
            (settle_token_weight, settle_price, insurance_price),
            // starting position
            (init_settle_token, init_perp, init_other, insurance_amount, settle_limit),
            // the expected liqee end position
            (exp_success, exp_liqee_settle_token, exp_liqee_perp),
            // expected liqor end position
            (exp_liqor_settle_token, exp_liqor_perp, exp_liqor_insurance, exp_socialized_loss),
            // maximum liquidation the liqor requests
            max_liab_transfer,
        ) in test_cases
        {
            println!("test: {name}");
            let mut setup = TestSetup::new();
            {
                let t = setup.settle_bank.data();
                t.init_asset_weight = I80F48::from_num(settle_token_weight);
                t.init_liab_weight = I80F48::from_num(2.0 - settle_token_weight);
                // maint weights used for perp settle health
                t.maint_asset_weight = I80F48::from_num(settle_token_weight);
                t.maint_liab_weight = I80F48::from_num(2.0 - settle_token_weight);
                t.stable_price_model.stable_price = settle_price;
                setup.settle_oracle.data().price = I80F48::from_num(settle_price);

                let t = setup.insurance_bank.data();
                t.stable_price_model.stable_price = insurance_price;
                setup.insurance_oracle.data().price = I80F48::from_num(insurance_price);

                let p = setup.perp_market.data();
                p.init_overall_asset_weight = I80F48::from_num(0.0);
                p.open_interest = 1;

                setup.insurance_vault.amount = insurance_amount;
            }
            {
                let p = perp_p(&mut setup.liqee);
                p.quote_position_native = I80F48::from_num(init_perp);
                p.recurring_settle_pnl_allowance = (settle_limit as i64).abs();

                let settle_bank = setup.settle_bank.data();
                settle_bank
                    .change_without_fee(
                        settle_p(&mut setup.liqee),
                        I80F48::from_num(init_settle_token),
                        0,
                    )
                    .unwrap();

                let other_bank = setup.other_bank.data();
                other_bank
                    .change_without_fee(other_p(&mut setup.liqee), I80F48::from_num(init_other), 0)
                    .unwrap();
            }

            let result = setup.run(max_liab_transfer);
            if !exp_success {
                assert!(result.is_err());
                continue;
            }
            let mut result = result.unwrap();

            let settle_bank = result.settle_bank.data();
            assert_eq_f!(
                settle_p(&mut result.liqee).native(settle_bank),
                exp_liqee_settle_token,
                0.01
            );
            assert_eq_f!(
                settle_p(&mut result.liqor).native(settle_bank),
                exp_liqor_settle_token,
                0.01
            );

            let insurance_bank = result.insurance_bank.data();
            assert_eq_f!(
                insurance_p(&mut result.liqor).native(insurance_bank),
                exp_liqor_insurance,
                0.01
            );

            assert_eq_f!(
                perp_p(&mut result.liqee).quote_position_native,
                exp_liqee_perp,
                0.1
            );
            assert_eq_f!(
                perp_p(&mut result.liqor).quote_position_native,
                exp_liqor_perp,
                0.1
            );

            assert_eq_f!(
                result.perp_market.data().long_funding,
                exp_socialized_loss,
                0.1
            );
        }
    }
}
