/*!
 * This module deals with computing different types of health for a mango account.
 *
 * Health is a number in USD and represents a risk-engine assessment of the account's
 * positions and open orders. The larger the health the better. Negative health
 * often means some action is necessary or a limitation is placed on the user.
 *
 * The different types of health are described in the HealthType enum.
 *
 * The key struct in this module is HealthCache, typically constructed by the
 * new_health_cache() function. With it, the different health types can be
 * computed.
 *
 * The HealthCache holds the data it needs in TokenInfo, Serum3Info and PerpInfo.
 */

use anchor_lang::prelude::*;

use fixed::types::I80F48;

use crate::error::*;
use crate::i80f48::LowPrecisionDivision;
use crate::serum3_cpi::{OpenOrdersAmounts, OpenOrdersSlim};
use crate::state::{
    Bank, MangoAccountRef, PerpMarket, PerpMarketIndex, PerpPosition, Serum3MarketIndex,
    Serum3Orders, TokenIndex,
};

use super::*;

/// Information about prices for a bank or perp market.
#[derive(Clone, Debug)]
pub struct Prices {
    /// The current oracle price
    pub oracle: I80F48, // native/native

    /// A "stable" price, provided by StablePriceModel
    pub stable: I80F48, // native/native
}

impl Prices {
    // intended for tests
    pub fn new_single_price(price: I80F48) -> Self {
        Self {
            oracle: price,
            stable: price,
        }
    }

    /// The liability price to use for the given health type
    #[inline(always)]
    pub fn liab(&self, health_type: HealthType) -> I80F48 {
        match health_type {
            HealthType::Maint | HealthType::LiquidationEnd => self.oracle,
            HealthType::Init => self.oracle.max(self.stable),
        }
    }

    /// The asset price to use for the given health type
    #[inline(always)]
    pub fn asset(&self, health_type: HealthType) -> I80F48 {
        match health_type {
            HealthType::Maint | HealthType::LiquidationEnd => self.oracle,
            HealthType::Init => self.oracle.min(self.stable),
        }
    }
}

/// There are three types of health:
/// - initial health ("init"): users can only open new positions if it's >= 0
/// - maintenance health ("maint"): users get liquidated if it's < 0
/// - liquidation end health: once liquidation started (see being_liquidated), it
///   only stops once this is >= 0
///
/// The ordering is
///   init health <= liquidation end health <= maint health
///
/// The different health types are realized by using different weights and prices:
/// - init health: init weights with scaling, stable-price adjusted prices
/// - liq end health: init weights without scaling, oracle prices
/// - maint health: maint weights, oracle prices
///
#[derive(PartialEq, Copy, Clone, AnchorSerialize, AnchorDeserialize)]
pub enum HealthType {
    Init,
    Maint, // aka LiquidationStart
    LiquidationEnd,
}

/// Computes health for a mango account given a set of account infos
///
/// These account infos must fit the fixed layout defined by FixedOrderAccountRetriever.
pub fn compute_health_from_fixed_accounts(
    account: &MangoAccountRef,
    health_type: HealthType,
    ais: &[AccountInfo],
    now_ts: u64,
) -> Result<I80F48> {
    let retriever = new_fixed_order_account_retriever(
        ais,
        account,
        Clock::get().map(|c| (c.unix_timestamp as u64, c.slot as u64))?,
    )?;
    Ok(new_health_cache(account, &retriever, now_ts)?.health(health_type))
}

/// Compute health with an arbitrary AccountRetriever
pub fn compute_health(
    account: &MangoAccountRef,
    health_type: HealthType,
    retriever: &impl AccountRetriever,
    now_ts: u64,
) -> Result<I80F48> {
    Ok(new_health_cache(account, retriever, now_ts)?.health(health_type))
}

/// How much of a token can be taken away before health decreases to zero?
///
/// If health is negative, returns 0.
pub fn spot_amount_taken_for_health_zero(
    mut health: I80F48,
    starting_spot: I80F48,
    asset_weighted_price: I80F48,
    liab_weighted_price: I80F48,
) -> Result<I80F48> {
    if health <= 0 {
        return Ok(I80F48::ZERO);
    }

    let mut taken_spot = I80F48::ZERO;
    if starting_spot > 0 {
        if asset_weighted_price > 0 {
            let asset_max = health / asset_weighted_price;
            if asset_max <= starting_spot {
                return Ok(asset_max);
            }
        }
        taken_spot = starting_spot;
        health -= starting_spot * asset_weighted_price;
    }
    if health > 0 {
        require_gt!(liab_weighted_price, 0);
        taken_spot += health / liab_weighted_price;
    }
    Ok(taken_spot)
}

/// How much of a token can be gained before health increases to zero?
///
/// Returns 0 if health is positive.
pub fn spot_amount_given_for_health_zero(
    health: I80F48,
    starting_spot: I80F48,
    asset_weighted_price: I80F48,
    liab_weighted_price: I80F48,
) -> Result<I80F48> {
    // asset/liab prices are reversed intentionally
    spot_amount_taken_for_health_zero(
        -health,
        -starting_spot,
        liab_weighted_price,
        asset_weighted_price,
    )
}

#[derive(Clone, Debug)]
pub struct TokenInfo {
    pub token_index: TokenIndex,
    pub maint_asset_weight: I80F48,
    pub init_asset_weight: I80F48,
    pub init_scaled_asset_weight: I80F48,
    pub maint_liab_weight: I80F48,
    pub init_liab_weight: I80F48,
    pub init_scaled_liab_weight: I80F48,
    pub prices: Prices,

    /// Freely available spot balance for the token.
    ///
    /// Includes TokenPosition and free Serum3OpenOrders balances.
    /// Does not include perp upnl or Serum3 reserved amounts.
    pub balance_spot: I80F48,

    pub allow_asset_liquidation: bool,
}

/// Temporary value used during health computations
#[derive(Clone, Default)]
pub struct TokenBalance {
    /// Sum of token_info.balance_spot and perp health_unsettled_pnl balances
    pub spot_and_perp: I80F48,
}

#[derive(Clone, Default)]
pub struct TokenMaxReserved {
    /// The sum of serum-reserved amounts over all markets
    pub max_serum_reserved: I80F48,
}

impl TokenInfo {
    #[inline(always)]
    fn asset_weight(&self, health_type: HealthType) -> I80F48 {
        match health_type {
            HealthType::Init => self.init_scaled_asset_weight,
            HealthType::LiquidationEnd => self.init_asset_weight,
            HealthType::Maint => self.maint_asset_weight,
        }
    }

    #[inline(always)]
    pub fn asset_weighted_price(&self, health_type: HealthType) -> I80F48 {
        self.asset_weight(health_type) * self.prices.asset(health_type)
    }

    #[inline(always)]
    fn liab_weight(&self, health_type: HealthType) -> I80F48 {
        match health_type {
            HealthType::Init => self.init_scaled_liab_weight,
            HealthType::LiquidationEnd => self.init_liab_weight,
            HealthType::Maint => self.maint_liab_weight,
        }
    }

    #[inline(always)]
    pub fn liab_weighted_price(&self, health_type: HealthType) -> I80F48 {
        self.liab_weight(health_type) * self.prices.liab(health_type)
    }

    #[inline(always)]
    pub fn health_contribution(&self, health_type: HealthType, balance: I80F48) -> I80F48 {
        let weighted_price = if balance.is_negative() {
            self.liab_weighted_price(health_type)
        } else {
            self.asset_weighted_price(health_type)
        };
        balance * weighted_price
    }
}

/// Information about reserved funds on Serum3 open orders accounts.
///
/// Note that all "free" funds on open orders accounts are added directly
/// to the token info. This is only about dealing with the reserved funds
/// that might end up as base OR quote tokens, depending on whether the
/// open orders execute on not.
#[derive(Clone, Debug)]
pub struct Serum3Info {
    // reserved amounts as stored on the open orders
    pub reserved_base: I80F48,
    pub reserved_quote: I80F48,

    // Reserved amounts, converted to the opposite token, while using the most extreme order price
    // May be zero if the extreme bid/ask price is not available (for orders placed in the past)
    pub reserved_base_as_quote_lowest_ask: I80F48,
    pub reserved_quote_as_base_highest_bid: I80F48,

    // Index into TokenInfos _not_ a TokenIndex
    pub base_info_index: usize,
    pub quote_info_index: usize,

    pub market_index: Serum3MarketIndex,

    /// The open orders account has no free or reserved funds
    pub has_zero_funds: bool,
}

impl Serum3Info {
    fn new(
        serum_account: &Serum3Orders,
        open_orders: &impl OpenOrdersAmounts,
        base_info_index: usize,
        quote_info_index: usize,
    ) -> Self {
        // track the reserved amounts
        let reserved_base = I80F48::from(open_orders.native_base_reserved());
        let reserved_quote = I80F48::from(open_orders.native_quote_reserved());

        let reserved_base_as_quote_lowest_ask =
            reserved_base * I80F48::from_num(serum_account.lowest_placed_ask);
        let reserved_quote_as_base_highest_bid =
            reserved_quote * I80F48::from_num(serum_account.highest_placed_bid_inv);

        Self {
            reserved_base,
            reserved_quote,
            reserved_base_as_quote_lowest_ask,
            reserved_quote_as_base_highest_bid,
            base_info_index,
            quote_info_index,
            market_index: serum_account.market_index,
            has_zero_funds: open_orders.native_base_total() == 0
                && open_orders.native_quote_total() == 0
                && open_orders.native_rebates() == 0,
        }
    }

    #[inline(always)]
    fn all_reserved_as_base(
        &self,
        health_type: HealthType,
        quote_info: &TokenInfo,
        base_info: &TokenInfo,
    ) -> I80F48 {
        let quote_asset = quote_info.prices.asset(health_type);
        let base_liab = base_info.prices.liab(health_type);
        let reserved_quote_as_base_oracle = (self.reserved_quote * quote_asset)
            .checked_div_f64_precision(base_liab)
            .unwrap();
        if self.reserved_quote_as_base_highest_bid != 0 {
            self.reserved_base
                + reserved_quote_as_base_oracle.min(self.reserved_quote_as_base_highest_bid)
        } else {
            self.reserved_base + reserved_quote_as_base_oracle
        }
    }

    #[inline(always)]
    fn all_reserved_as_quote(
        &self,
        health_type: HealthType,
        quote_info: &TokenInfo,
        base_info: &TokenInfo,
    ) -> I80F48 {
        let base_asset = base_info.prices.asset(health_type);
        let quote_liab = quote_info.prices.liab(health_type);
        let reserved_base_as_quote_oracle = (self.reserved_base * base_asset)
            .checked_div_f64_precision(quote_liab)
            .unwrap();
        if self.reserved_base_as_quote_lowest_ask != 0 {
            self.reserved_quote
                + reserved_base_as_quote_oracle.min(self.reserved_base_as_quote_lowest_ask)
        } else {
            self.reserved_quote + reserved_base_as_quote_oracle
        }
    }

    /// Compute the health contribution from active open orders.
    ///
    /// For open orders, health is about the worst-case outcome: Consider the scenarios:
    /// - all reserved base tokens convert to quote tokens
    /// - all reserved quote tokens convert to base tokens
    /// Which would lead to the smaller token health?
    ///
    /// Answering this question isn't straightforward for two reasons:
    /// 1. We don't have information about the actual open orders here. Just about the amount
    ///    of reserved tokens. Hence we assume base/quote conversion would happen at current
    ///    asset/liab prices.
    /// 2. Technically, there are interaction effects between multiple spot markets. If the
    ///    account has open orders on SOL/USDC, BTC/USDC and SOL/BTC, then the worst case for
    ///    SOL/USDC might be dependent on what happens with the open orders on the other two
    ///    markets.
    ///
    /// To simplify 2, we give up on computing the actual worst-case and instead compute something
    /// that's guaranteed to be less: Get the worst case for each market independently while
    /// assuming all other market open orders resolved maximally unfavorably.
    ///
    /// To be able to do that, we compute `token_max_reserved` for each token, which is the maximum
    /// token amount that would be generated if open orders in all markets that deal with the token
    /// turn its way. (in the example above: the open orders in the SOL/USDC and SOL/BTC market
    /// both produce SOL) See `compute_serum3_reservations()` below.
    #[inline(always)]
    fn health_contribution(
        &self,
        health_type: HealthType,
        token_infos: &[TokenInfo],
        token_balances: &[TokenBalance],
        token_max_reserved: &[TokenMaxReserved],
        market_reserved: &Serum3Reserved,
    ) -> I80F48 {
        if market_reserved.all_reserved_as_base.is_zero()
            || market_reserved.all_reserved_as_quote.is_zero()
        {
            return I80F48::ZERO;
        }

        let base_info = &token_infos[self.base_info_index];
        let quote_info = &token_infos[self.quote_info_index];

        // How much would health increase if the reserved balance were applied to the passed
        // token info?
        let compute_health_effect = |token_info: &TokenInfo,
                                     balance: &TokenBalance,
                                     max_reserved: &TokenMaxReserved,
                                     market_reserved: I80F48| {
            // This balance includes all possible reserved funds from markets that relate to the
            // token, including this market itself: `market_reserved` is already included in `max_serum_reserved`.
            let max_balance = balance.spot_and_perp + max_reserved.max_serum_reserved;

            // For simplicity, we assume that `market_reserved` was added to `max_balance` last
            // (it underestimates health because that gives the smallest effects): how much did
            // health change because of it?
            let (asset_part, liab_part) = if max_balance >= market_reserved {
                (market_reserved, I80F48::ZERO)
            } else if max_balance.is_negative() {
                (I80F48::ZERO, market_reserved)
            } else {
                (max_balance, market_reserved - max_balance)
            };

            let asset_weight = token_info.asset_weight(health_type);
            let liab_weight = token_info.liab_weight(health_type);
            let asset_price = token_info.prices.asset(health_type);
            let liab_price = token_info.prices.liab(health_type);
            asset_part * asset_weight * asset_price + liab_part * liab_weight * liab_price
        };

        let health_base = compute_health_effect(
            base_info,
            &token_balances[self.base_info_index],
            &token_max_reserved[self.base_info_index],
            market_reserved.all_reserved_as_base,
        );
        let health_quote = compute_health_effect(
            quote_info,
            &token_balances[self.quote_info_index],
            &token_max_reserved[self.quote_info_index],
            market_reserved.all_reserved_as_quote,
        );
        health_base.min(health_quote)
    }
}

#[derive(Clone)]
pub(crate) struct Serum3Reserved {
    /// base tokens when the serum3info.reserved_quote get converted to base and added to reserved_base
    all_reserved_as_base: I80F48,
    /// ditto the other way around
    all_reserved_as_quote: I80F48,
}

/// Stores information about perp market positions and their open orders.
///
/// Perp markets affect account health indirectly, though the token balance in the
/// perp market's settle token. See `effective_token_balances()`.
#[derive(Clone, Debug)]
pub struct PerpInfo {
    pub perp_market_index: PerpMarketIndex,
    pub settle_token_index: TokenIndex,
    pub maint_base_asset_weight: I80F48,
    pub init_base_asset_weight: I80F48,
    pub maint_base_liab_weight: I80F48,
    pub init_base_liab_weight: I80F48,
    pub maint_overall_asset_weight: I80F48,
    pub init_overall_asset_weight: I80F48,
    pub base_lot_size: i64,
    pub base_lots: i64,
    pub bids_base_lots: i64,
    pub asks_base_lots: i64,
    // in health-reference-token native units, no asset/liab factor needed
    pub quote: I80F48,
    pub base_prices: Prices,
    pub has_open_orders: bool,
    pub has_open_fills: bool,
}

impl PerpInfo {
    fn new(
        perp_position: &PerpPosition,
        perp_market: &PerpMarket,
        base_prices: Prices,
    ) -> Result<Self> {
        let base_lots = perp_position.base_position_lots() + perp_position.taker_base_lots;

        let unsettled_funding = perp_position.unsettled_funding(perp_market);
        let taker_quote = I80F48::from(perp_position.taker_quote_lots * perp_market.quote_lot_size);
        let quote_current = perp_position.quote_position_native() - unsettled_funding + taker_quote;

        Ok(Self {
            perp_market_index: perp_market.perp_market_index,
            settle_token_index: perp_market.settle_token_index,
            init_base_asset_weight: perp_market.init_base_asset_weight,
            init_base_liab_weight: perp_market.init_base_liab_weight,
            maint_base_asset_weight: perp_market.maint_base_asset_weight,
            maint_base_liab_weight: perp_market.maint_base_liab_weight,
            init_overall_asset_weight: perp_market.init_overall_asset_weight,
            maint_overall_asset_weight: perp_market.maint_overall_asset_weight,
            base_lot_size: perp_market.base_lot_size,
            base_lots,
            bids_base_lots: perp_position.bids_base_lots,
            asks_base_lots: perp_position.asks_base_lots,
            quote: quote_current,
            base_prices,
            has_open_orders: perp_position.has_open_orders(),
            has_open_fills: perp_position.has_open_taker_fills(),
        })
    }

    /// The perp-risk (but not token-risk) adjusted upnl. Also called "hupnl".
    ///
    /// In settle token native units.
    ///
    /// This is what gets added to effective_token_balances() and then contributes
    /// to account health.
    ///
    /// For fully isolated perp markets, users may never borrow against unsettled
    /// positive perp pnl, there overall_asset_weight == 0 and there can't be positive
    /// health contributions from these perp market. We sometimes call these markets
    /// "untrusted markets".
    ///
    /// In these, users need to settle their perp pnl with other perp market participants
    /// in order to realize their gains if they want to use them as collateral.
    ///
    /// This is because we don't trust the perp's base price to not suddenly jump to
    /// zero (if users could borrow against their perp balances they might now
    /// be bankrupt) or suddenly increase a lot (if users could borrow against perp
    /// balances they could now borrow other assets).
    ///
    /// Other markets may be liquid enough that we have enough confidence to allow
    /// users to borrow against unsettled positive pnl to some extend. In these cases,
    /// the overall asset weights would be >0.
    #[inline(always)]
    pub fn health_unsettled_pnl(&self, health_type: HealthType) -> I80F48 {
        let contribution = self.unweighted_health_unsettled_pnl(health_type);
        self.weigh_uhupnl_overall(contribution, health_type)
    }

    /// Convert uhupnl to hupnl by applying the overall weight. In settle token native units.
    #[inline(always)]
    fn weigh_uhupnl_overall(&self, unweighted: I80F48, health_type: HealthType) -> I80F48 {
        if unweighted > 0 {
            let overall_weight = match health_type {
                HealthType::Init | HealthType::LiquidationEnd => self.init_overall_asset_weight,
                HealthType::Maint => self.maint_overall_asset_weight,
            };
            overall_weight * unweighted
        } else {
            unweighted
        }
    }

    /// Settle token native provided by perp position and open orders, without the overall asset weight.
    ///
    /// Also called "uhupnl".
    ///
    /// For open orders, this computes the worst-case amount by considering the scenario where all
    /// bids execute and the one where all asks execute.
    ///
    /// It's always less than the PerpPosition's `unsettled_pnl()` for two reasons: The open orders
    /// are taken into account and the base weight is applied to the base position.
    ///
    /// Generally: hupnl <= uhupnl <= upnl
    #[inline(always)]
    pub fn unweighted_health_unsettled_pnl(&self, health_type: HealthType) -> I80F48 {
        let order_execution_case = |orders_base_lots: i64, order_price: I80F48| {
            let net_base_native =
                I80F48::from((self.base_lots + orders_base_lots) * self.base_lot_size);
            let weight = match (health_type, net_base_native.is_negative()) {
                (HealthType::Init, true) | (HealthType::LiquidationEnd, true) => {
                    self.init_base_liab_weight
                }
                (HealthType::Init, false) | (HealthType::LiquidationEnd, false) => {
                    self.init_base_asset_weight
                }
                (HealthType::Maint, true) => self.maint_base_liab_weight,
                (HealthType::Maint, false) => self.maint_base_asset_weight,
            };
            let base_price = if net_base_native.is_negative() {
                self.base_prices.liab(health_type)
            } else {
                self.base_prices.asset(health_type)
            };

            // Total value of the order-execution adjusted base position
            let base_health = net_base_native * weight * base_price;

            let orders_base_native = I80F48::from(orders_base_lots * self.base_lot_size);
            // The quote change from executing the bids/asks
            let order_quote = -orders_base_native * order_price;

            base_health + order_quote
        };

        // What is worse: Executing all bids at oracle_price.liab, or executing all asks at oracle_price.asset?
        let bids_case =
            order_execution_case(self.bids_base_lots, self.base_prices.liab(health_type));
        let asks_case =
            order_execution_case(-self.asks_base_lots, self.base_prices.asset(health_type));
        let worst_case = bids_case.min(asks_case);

        self.quote + worst_case
    }
}

/// Store information needed to compute account health
///
/// This is called a cache, because it extracts information from a MangoAccount and
/// the Bank, Perp, oracle accounts once and then allows computing different types
/// of health.
///
/// For compute-saving reasons, it also allows applying adjustments to the extracted
/// positions. That's often helpful for instructions that want to re-compute health
/// after having made small, well-known changes to an account. Recomputing the
/// HealthCache from scratch would be significantly more expensive.
///
/// However, there's a real risk of getting the adjustments wrong and computing an
/// inconsistent result, so particular care needs to be taken when this is done.
#[allow(unused)]
#[derive(Clone, Debug)]
pub struct HealthCache {
    pub token_infos: Vec<TokenInfo>,
    pub(crate) serum3_infos: Vec<Serum3Info>,
    pub(crate) perp_infos: Vec<PerpInfo>,
    #[allow(unused)]
    pub(crate) being_liquidated: bool,
}

impl HealthCache {
    pub fn health(&self, health_type: HealthType) -> I80F48 {
        let token_balances = self.effective_token_balances(health_type);
        let mut health = I80F48::ZERO;
        let sum = |contrib| {
            health += contrib;
        };
        self.health_sum(health_type, sum, &token_balances);
        health
    }

    /// The health ratio is
    /// - 0 if health is 0 - meaning assets = liabs
    /// - 100 if there's 2x as many assets as liabs
    /// - 200 if there's 3x as many assets as liabs
    /// - MAX if liabs = 0
    ///
    /// Maybe talking about the collateralization ratio assets/liabs is more intuitive?
    pub fn health_ratio(&self, health_type: HealthType) -> I80F48 {
        let (assets, liabs) = self.health_assets_and_liabs_stable_liabs(health_type);
        let hundred = I80F48::from(100);
        if liabs > 0 {
            // feel free to saturate to MAX for tiny liabs
            (hundred * (assets - liabs)).saturating_div(liabs)
        } else {
            I80F48::MAX
        }
    }

    pub fn health_assets_and_liabs_stable_assets(
        &self,
        health_type: HealthType,
    ) -> (I80F48, I80F48) {
        self.health_assets_and_liabs(health_type, true)
    }
    pub fn health_assets_and_liabs_stable_liabs(
        &self,
        health_type: HealthType,
    ) -> (I80F48, I80F48) {
        self.health_assets_and_liabs(health_type, false)
    }

    /// Loop over the token, perp, serum contributions and add up all positive values into `assets`
    /// and (the abs) of negative values separately into `liabs`. Return (assets, liabs).
    ///
    /// Due to the way token and perp positions sum before being weighted, there's some flexibility
    /// in how the sum is split up. It can either be split up such that the amount of liabs stays
    /// constant when assets change, or the other way around.
    ///
    /// For example, if assets are held stable: An account with $10 in SOL and -$12 hupnl in a
    /// SOL-settled perp market would have:
    /// - assets: $10 * SOL_asset_weight
    /// - liabs: $10 * SOL_asset_weight + $2 * SOL_liab_weight
    /// because some of the liabs are weighted lower as they are just compensating the assets.
    ///
    /// Same example if liabs are held stable:
    /// - liabs: $12 * SOL_liab_weight
    /// - assets: $10 * SOL_liab_weight
    ///
    /// The value `assets - liabs` is the health and the same in both cases.
    fn health_assets_and_liabs(
        &self,
        health_type: HealthType,
        stable_assets: bool,
    ) -> (I80F48, I80F48) {
        let mut total_assets = I80F48::ZERO;
        let mut total_liabs = I80F48::ZERO;
        let add = |assets: &mut I80F48, liabs: &mut I80F48, value: I80F48| {
            if value > 0 {
                *assets += value;
            } else {
                *liabs += -value;
            }
        };

        for token_info in self.token_infos.iter() {
            // For each token, health only considers the effective token position. But for
            // this function we want to distinguish the contribution from token deposits from
            // contributions by perp markets.
            // However, the overall weight is determined by the sum, so first collect all
            // assets parts and all liab parts and then determine the actual values.
            let mut asset_balance = I80F48::ZERO;
            let mut liab_balance = I80F48::ZERO;

            add(
                &mut asset_balance,
                &mut liab_balance,
                token_info.balance_spot,
            );

            for perp_info in self.perp_infos.iter() {
                if perp_info.settle_token_index != token_info.token_index {
                    continue;
                }
                let health_unsettled = perp_info.health_unsettled_pnl(health_type);
                add(&mut asset_balance, &mut liab_balance, health_unsettled);
            }

            // The assignment to total_assets and total_liabs is a bit arbitrary.
            // As long as the (added_assets - added_liabs) = weighted(asset_balance - liab_balance),
            // the result will be consistent.
            if stable_assets {
                let asset_weighted_price = token_info.asset_weighted_price(health_type);
                let assets = asset_balance * asset_weighted_price;
                total_assets += assets;
                if asset_balance >= liab_balance {
                    // liabs partially compensate
                    total_liabs += liab_balance * asset_weighted_price;
                } else {
                    let liab_weighted_price = token_info.liab_weighted_price(health_type);
                    // the liabs fully compensate the assets and even add something extra
                    total_liabs += assets + (liab_balance - asset_balance) * liab_weighted_price;
                }
            } else {
                let liab_weighted_price = token_info.liab_weighted_price(health_type);
                let liabs = liab_balance * liab_weighted_price;
                total_liabs += liabs;
                if asset_balance >= liab_balance {
                    let asset_weighted_price = token_info.asset_weighted_price(health_type);
                    // the assets fully compensate the liabs and even add something extra
                    total_assets += liabs + (asset_balance - liab_balance) * asset_weighted_price;
                } else {
                    // assets partially compensate
                    total_assets += asset_balance * liab_weighted_price;
                }
            }
        }

        let token_balances = self.effective_token_balances(health_type);
        let (token_max_reserved, serum3_reserved) = self.compute_serum3_reservations(health_type);
        for (serum3_info, reserved) in self.serum3_infos.iter().zip(serum3_reserved.iter()) {
            let contrib = serum3_info.health_contribution(
                health_type,
                &self.token_infos,
                &token_balances,
                &token_max_reserved,
                reserved,
            );
            add(&mut total_assets, &mut total_liabs, contrib);
        }

        (total_assets, total_liabs)
    }

    /// Computes the account assets and liabilities marked to market.
    ///
    /// Contrary to health_assets_and_liabs, there's no health weighing or adjustment
    /// for stable prices. It uses oracle prices directly.
    ///
    /// Returns (assets, liabilities)
    pub fn assets_and_liabs(&self) -> (I80F48, I80F48) {
        let mut assets = I80F48::ZERO;
        let mut liabs = I80F48::ZERO;

        for token_info in self.token_infos.iter() {
            if token_info.balance_spot.is_negative() {
                liabs -= token_info.balance_spot * token_info.prices.oracle;
            } else {
                assets += token_info.balance_spot * token_info.prices.oracle;
            }
        }

        for serum_info in self.serum3_infos.iter() {
            let quote = &self.token_infos[serum_info.quote_info_index];
            let base = &self.token_infos[serum_info.base_info_index];
            assets += serum_info.reserved_base * base.prices.oracle;
            assets += serum_info.reserved_quote * quote.prices.oracle;
        }

        for perp_info in self.perp_infos.iter() {
            let quote_price = self.token_infos[perp_info.settle_token_index as usize]
                .prices
                .oracle;
            let quote_position_value = perp_info.quote * quote_price;
            if perp_info.quote.is_negative() {
                liabs -= quote_position_value;
            } else {
                assets += quote_position_value;
            }

            let base_position_value = I80F48::from(perp_info.base_lots * perp_info.base_lot_size)
                * perp_info.base_prices.oracle
                * quote_price;
            if base_position_value.is_negative() {
                liabs -= base_position_value;
            } else {
                assets += base_position_value;
            }
        }

        return (assets, liabs);
    }

    /// Computes the account leverage as ratio of liabs / (assets - liabs)
    ///
    /// The goal of this function is to provide a quick overview over the accounts balance sheet.
    /// It's not actually used to make any margin decisions internally and doesn't account for
    /// open orders or stable / oracle price differences. Use health_ratio to make risk decisions.
    pub fn leverage(&self) -> I80F48 {
        let (assets, liabs) = self.assets_and_liabs();
        let equity = assets - liabs;
        liabs / equity.max(I80F48::from_num(0.001))
    }

    pub fn token_info(&self, token_index: TokenIndex) -> Result<&TokenInfo> {
        Ok(&self.token_infos[self.token_info_index(token_index)?])
    }

    pub fn token_info_index(&self, token_index: TokenIndex) -> Result<usize> {
        self.token_infos
            .iter()
            .position(|t| t.token_index == token_index)
            .ok_or_else(|| {
                error_msg_typed!(
                    MangoError::TokenPositionDoesNotExist,
                    "token index {} not found",
                    token_index
                )
            })
    }

    pub fn has_token_info(&self, token_index: TokenIndex) -> bool {
        self.token_infos
            .iter()
            .any(|t| t.token_index == token_index)
    }

    pub fn perp_info(&self, perp_market_index: PerpMarketIndex) -> Result<&PerpInfo> {
        Ok(&self.perp_infos[self.perp_info_index(perp_market_index)?])
    }

    pub(crate) fn perp_info_index(&self, perp_market_index: PerpMarketIndex) -> Result<usize> {
        self.perp_infos
            .iter()
            .position(|t| t.perp_market_index == perp_market_index)
            .ok_or_else(|| {
                error_msg_typed!(
                    MangoError::PerpPositionDoesNotExist,
                    "perp market index {} not found",
                    perp_market_index
                )
            })
    }

    /// Changes the cached user account token balance.
    pub fn adjust_token_balance(&mut self, bank: &Bank, change: I80F48) -> Result<()> {
        let entry_index = self.token_info_index(bank.token_index)?;
        let mut entry = &mut self.token_infos[entry_index];

        // Note: resetting the weights here assumes that the change has been applied to
        // the passed in bank already
        entry.init_scaled_asset_weight =
            bank.scaled_init_asset_weight(entry.prices.asset(HealthType::Init));
        entry.init_scaled_liab_weight =
            bank.scaled_init_liab_weight(entry.prices.liab(HealthType::Init));

        // Work around the fact that -((-x) * y) == x * y does not hold for I80F48:
        // We need to make sure that if balance is before * price, then change = -before
        // brings it to exactly zero.
        let removed_contribution = -change;
        entry.balance_spot -= removed_contribution;
        Ok(())
    }

    /// Recompute the cached information about a serum market.
    ///
    /// WARNING: You must also call recompute_token_weights() after all bank
    /// deposit/withdraw changes!
    pub fn recompute_serum3_info(
        &mut self,
        serum_account: &Serum3Orders,
        open_orders: &OpenOrdersSlim,
        free_base_change: I80F48,
        free_quote_change: I80F48,
    ) -> Result<()> {
        let serum_info_index = self
            .serum3_infos
            .iter_mut()
            .position(|m| m.market_index == serum_account.market_index)
            .ok_or_else(|| error_msg!("serum3 market {} not found", serum_account.market_index))?;

        let serum_info = &self.serum3_infos[serum_info_index];
        {
            let base_entry = &mut self.token_infos[serum_info.base_info_index];
            base_entry.balance_spot += free_base_change;
        }
        {
            let quote_entry = &mut self.token_infos[serum_info.quote_info_index];
            quote_entry.balance_spot += free_quote_change;
        }

        let serum_info = &mut self.serum3_infos[serum_info_index];
        *serum_info = Serum3Info::new(
            serum_account,
            open_orders,
            serum_info.base_info_index,
            serum_info.quote_info_index,
        );
        Ok(())
    }

    pub fn recompute_perp_info(
        &mut self,
        perp_position: &PerpPosition,
        perp_market: &PerpMarket,
    ) -> Result<()> {
        let perp_entry = self
            .perp_infos
            .iter_mut()
            .find(|m| m.perp_market_index == perp_market.perp_market_index)
            .ok_or_else(|| error_msg!("perp market {} not found", perp_market.perp_market_index))?;
        *perp_entry = PerpInfo::new(perp_position, perp_market, perp_entry.base_prices.clone())?;
        Ok(())
    }

    /// Liquidatable spot assets mean: actual token deposits and also a positive effective token balance
    /// and is available for asset liquidation
    pub fn has_liq_spot_assets(&self) -> bool {
        let health_token_balances = self.effective_token_balances(HealthType::LiquidationEnd);
        self.token_infos
            .iter()
            .zip(health_token_balances.iter())
            .any(|(ti, b)| {
                // need 1 native token to use token_liq_with_token
                ti.balance_spot >= 1 && b.spot_and_perp >= 1 && ti.allow_asset_liquidation
            })
    }

    /// Liquidatable spot borrows mean: actual token borrows plus a negative effective token balance
    pub fn has_liq_spot_borrows(&self) -> bool {
        let health_token_balances = self.effective_token_balances(HealthType::LiquidationEnd);
        self.token_infos
            .iter()
            .zip(health_token_balances.iter())
            .any(|(ti, b)| ti.balance_spot < 0 && b.spot_and_perp < 0)
    }

    // This function exists separately from has_liq_spot_assets and has_liq_spot_borrows for performance reasons
    pub fn has_possible_spot_liquidations(&self) -> bool {
        let health_token_balances = self.effective_token_balances(HealthType::LiquidationEnd);
        let all_iter = || self.token_infos.iter().zip(health_token_balances.iter());
        all_iter().any(|(ti, b)| ti.balance_spot < 0 && b.spot_and_perp < 0)
            && all_iter().any(|(ti, b)| {
                ti.balance_spot >= 1 && b.spot_and_perp >= 1 && ti.allow_asset_liquidation
            })
    }

    pub fn has_serum3_open_orders_funds(&self) -> bool {
        self.serum3_infos.iter().any(|si| !si.has_zero_funds)
    }

    pub fn has_perp_open_orders(&self) -> bool {
        self.perp_infos.iter().any(|p| p.has_open_orders)
    }

    pub fn has_perp_base_positions(&self) -> bool {
        self.perp_infos.iter().any(|p| p.base_lots != 0)
    }

    pub fn has_perp_open_fills(&self) -> bool {
        self.perp_infos.iter().any(|p| p.has_open_fills)
    }

    pub fn has_perp_positive_pnl_no_base(&self) -> bool {
        self.perp_infos
            .iter()
            .any(|p| p.base_lots == 0 && p.quote > 0)
    }

    pub fn has_perp_negative_pnl_no_base(&self) -> bool {
        self.perp_infos
            .iter()
            .any(|p| p.base_lots == 0 && p.quote < 0)
    }

    /// Phase1 is spot/perp order cancellation and spot settlement since
    /// neither of these come at a cost to the liqee
    pub fn has_phase1_liquidatable(&self) -> bool {
        self.has_serum3_open_orders_funds() || self.has_perp_open_orders()
    }

    pub fn require_after_phase1_liquidation(&self) -> Result<()> {
        require!(
            !self.has_serum3_open_orders_funds(),
            MangoError::HasOpenOrUnsettledSerum3Orders
        );
        require!(!self.has_perp_open_orders(), MangoError::HasOpenPerpOrders);
        Ok(())
    }

    pub fn in_phase1_liquidation(&self) -> bool {
        self.has_phase1_liquidatable()
    }

    /// Phase2 is for:
    /// - token-token liquidation
    /// - liquidation of perp base positions (an open fill isn't liquidatable, but
    ///   it changes the base position, so need to wait for it to be processed...)
    /// - bringing positive trusted perp pnl into the spot realm
    pub fn has_phase2_liquidatable(&self) -> bool {
        self.has_possible_spot_liquidations()
            || self.has_perp_base_positions()
            || self.has_perp_open_fills()
            || self.has_perp_positive_pnl_no_base()
    }

    pub fn require_after_phase2_liquidation(&self) -> Result<()> {
        self.require_after_phase1_liquidation()?;
        require!(
            !self.has_possible_spot_liquidations(),
            MangoError::HasLiquidatableTokenPosition
        );
        require!(
            !self.has_perp_base_positions(),
            MangoError::HasLiquidatablePerpBasePosition
        );
        require!(
            !self.has_perp_open_fills(),
            MangoError::HasOpenPerpTakerFills
        );
        require!(
            !self.has_perp_positive_pnl_no_base(),
            MangoError::HasLiquidatablePositivePerpPnl
        );
        Ok(())
    }

    pub fn in_phase2_liquidation(&self) -> bool {
        !self.has_phase1_liquidatable() && self.has_phase2_liquidatable()
    }

    /// Phase3 is bankruptcy:
    /// - token bankruptcy
    /// - perp bankruptcy
    pub fn has_phase3_liquidatable(&self) -> bool {
        self.has_liq_spot_borrows() || self.has_perp_negative_pnl_no_base()
    }

    pub fn in_phase3_liquidation(&self) -> bool {
        !self.has_phase1_liquidatable()
            && !self.has_phase2_liquidatable()
            && self.has_phase3_liquidatable()
    }

    pub(crate) fn compute_serum3_reservations(
        &self,
        health_type: HealthType,
    ) -> (Vec<TokenMaxReserved>, Vec<Serum3Reserved>) {
        let mut token_max_reserved = vec![TokenMaxReserved::default(); self.token_infos.len()];

        // For each serum market, compute what happened if reserved_base was converted to quote
        // or reserved_quote was converted to base.
        let mut serum3_reserved = Vec::with_capacity(self.serum3_infos.len());

        for info in self.serum3_infos.iter() {
            let quote_info = &self.token_infos[info.quote_info_index];
            let base_info = &self.token_infos[info.base_info_index];

            let all_reserved_as_base =
                info.all_reserved_as_base(health_type, quote_info, base_info);
            let all_reserved_as_quote =
                info.all_reserved_as_quote(health_type, quote_info, base_info);

            token_max_reserved[info.base_info_index].max_serum_reserved += all_reserved_as_base;
            token_max_reserved[info.quote_info_index].max_serum_reserved += all_reserved_as_quote;

            serum3_reserved.push(Serum3Reserved {
                all_reserved_as_base,
                all_reserved_as_quote,
            });
        }

        (token_max_reserved, serum3_reserved)
    }

    /// Returns token balances that account for spot and perp contributions
    ///
    /// Spot contributions are just the regular deposits or borrows, as well as from free
    /// funds on serum3 open orders accounts.
    ///
    /// Perp contributions come from perp positions in markets that use the token as a settle token:
    /// For these the hupnl is added to the total because that's the risk-adjusted expected to be
    /// gained or lost from settlement.
    pub fn effective_token_balances(&self, health_type: HealthType) -> Vec<TokenBalance> {
        self.effective_token_balances_internal(health_type, false)
    }

    /// Implementation of effective_token_balances()
    ///
    /// The ignore_negative_perp flag exists for perp_max_settle(). When it is enabled, all negative
    /// token contributions from perp markets are ignored. That's useful for knowing how much token
    /// collateral is available when limiting negative upnl settlement.
    fn effective_token_balances_internal(
        &self,
        health_type: HealthType,
        ignore_negative_perp: bool,
    ) -> Vec<TokenBalance> {
        let mut token_balances = vec![TokenBalance::default(); self.token_infos.len()];

        for perp_info in self.perp_infos.iter() {
            let settle_token_index = self.token_info_index(perp_info.settle_token_index).unwrap();
            let perp_settle_token = &mut token_balances[settle_token_index];
            let health_unsettled = perp_info.health_unsettled_pnl(health_type);
            if !ignore_negative_perp || health_unsettled > 0 {
                perp_settle_token.spot_and_perp += health_unsettled;
            }
        }

        for (token_info, token_balance) in self.token_infos.iter().zip(token_balances.iter_mut()) {
            token_balance.spot_and_perp += token_info.balance_spot;
        }

        token_balances
    }

    pub(crate) fn health_sum(
        &self,
        health_type: HealthType,
        mut action: impl FnMut(I80F48),
        token_balances: &[TokenBalance],
    ) {
        for (token_info, token_balance) in self.token_infos.iter().zip(token_balances.iter()) {
            let contrib = token_info.health_contribution(health_type, token_balance.spot_and_perp);
            action(contrib);
        }

        let (token_max_reserved, serum3_reserved) = self.compute_serum3_reservations(health_type);
        for (serum3_info, reserved) in self.serum3_infos.iter().zip(serum3_reserved.iter()) {
            let contrib = serum3_info.health_contribution(
                health_type,
                &self.token_infos,
                &token_balances,
                &token_max_reserved,
                reserved,
            );
            action(contrib);
        }
    }

    /// Returns how much pnl is settleable for a given settle token.
    ///
    /// The idea of this limit is that settlement is only permissible as long as there are
    /// non-perp assets that back it. If an account with 1 USD deposited somehow gets
    /// a large negative perp upnl, it should not be allowed to settle that perp loss into
    /// the spot world fully (because of perp/spot isolation, translating perp losses and
    /// gains into tokens is restricted). Only 1 USD worth would be allowed.
    ///
    /// Effectively, there's a health variant "perp settle health" that ignores negative
    /// token contributions from perp markets. Settlement is allowed as long as perp settle
    /// health remains >= 0.
    ///
    /// For example, if perp_settle_health is 50 USD, then the settleable amount in SOL
    /// would depend on the SOL price, the user's current spot balance and the SOL weights:
    /// We need to compute how much the user's spot SOL balance may decrease before the
    /// perp_settle_health becomes zero.
    ///
    /// Note that the account's actual health would not change during settling negative upnl:
    /// the spot balance goes down but the perp hupnl goes up accordingly.
    ///
    /// Examples:
    /// - An account may have maint_health < 0, but settling perp pnl could still be allowed.
    ///   (+100 USDC health, -50 USDT health, -50 perp health -> allow settling 50 health worth)
    /// - Positive health from trusted pnl markets counts
    /// - If overall health is 0 with two trusted perp pnl < 0, settling may still be possible.
    ///   (+100 USDC health, -150 perp1 health, -150 perp2 health -> allow settling 100 health worth)
    /// - Positive trusted perp pnl can enable settling.
    ///   (+100 trusted perp1 health, -100 perp2 health -> allow settling of 100 health worth)
    pub fn perp_max_settle(&self, settle_token_index: TokenIndex) -> Result<I80F48> {
        let maint_type = HealthType::Maint;

        let token_balances = self.effective_token_balances_internal(maint_type, true);
        let mut perp_settle_health = I80F48::ZERO;
        let sum = |contrib| {
            perp_settle_health += contrib;
        };
        self.health_sum(maint_type, sum, &token_balances);

        let token_info_index = self.token_info_index(settle_token_index)?;
        let token = &self.token_infos[token_info_index];
        spot_amount_taken_for_health_zero(
            perp_settle_health,
            token_balances[token_info_index].spot_and_perp,
            token.asset_weighted_price(maint_type),
            token.liab_weighted_price(maint_type),
        )
    }

    pub fn total_serum3_potential(
        &self,
        health_type: HealthType,
        token_index: TokenIndex,
    ) -> Result<I80F48> {
        let target_token_info_index = self.token_info_index(token_index)?;
        let total_reserved = self
            .serum3_infos
            .iter()
            .filter_map(|info| {
                if info.quote_info_index == target_token_info_index {
                    Some(info.all_reserved_as_quote(
                        health_type,
                        &self.token_infos[info.quote_info_index],
                        &self.token_infos[info.base_info_index],
                    ))
                } else if info.base_info_index == target_token_info_index {
                    Some(info.all_reserved_as_base(
                        health_type,
                        &self.token_infos[info.quote_info_index],
                        &self.token_infos[info.base_info_index],
                    ))
                } else {
                    None
                }
            })
            .sum();
        Ok(total_reserved)
    }
}

pub(crate) fn find_token_info_index(infos: &[TokenInfo], token_index: TokenIndex) -> Result<usize> {
    infos
        .iter()
        .position(|ti| ti.token_index == token_index)
        .ok_or_else(|| {
            error_msg_typed!(
                MangoError::TokenPositionDoesNotExist,
                "token index {} not found",
                token_index
            )
        })
}

/// Generate a HealthCache for an account and its health accounts.
pub fn new_health_cache(
    account: &MangoAccountRef,
    retriever: &impl AccountRetriever,
    now_ts: u64,
) -> Result<HealthCache> {
    new_health_cache_impl(account, retriever, now_ts, false)
}

/// Generate a special HealthCache for an account and its health accounts
/// where nonnegative token positions for bad oracles are skipped as well as missing banks.
///
/// This health cache must be used carefully, since it doesn't provide the actual
/// account health, just a value that is guaranteed to be less than it.
pub fn new_health_cache_skipping_missing_banks_and_bad_oracles(
    account: &MangoAccountRef,
    retriever: &impl AccountRetriever,
    now_ts: u64,
) -> Result<HealthCache> {
    new_health_cache_impl(account, retriever, now_ts, true)
}

// On `allow_skipping_banks`:
//   If (a Bank is not provided or its oracle is stale or inconfident) and the health contribution would
//   not be negative, skip it. This decreases health, but many operations are still allowed as long
//   as the decreased amount stays positive.
fn new_health_cache_impl(
    account: &MangoAccountRef,
    retriever: &impl AccountRetriever,
    now_ts: u64,
    allow_skipping_banks: bool,
) -> Result<HealthCache> {
    // token contribution from token accounts
    let mut token_infos = Vec::with_capacity(account.active_token_positions().count());

    // As a CU optimization, don't call available_banks() unless necessary
    let available_banks_opt = if allow_skipping_banks {
        Some(retriever.available_banks()?)
    } else {
        None
    };

    for (i, position) in account.active_token_positions().enumerate() {
        // Allow skipping of missing banks only if the account has a nonnegative balance
        if allow_skipping_banks {
            let bank_is_available = available_banks_opt
                .as_ref()
                .unwrap()
                .contains(&position.token_index);
            if !bank_is_available {
                require_msg_typed!(
                    position.indexed_position >= 0,
                    MangoError::InvalidBank,
                    "the bank for token index {} is a required health account when the account has a negative balance in it",
                    position.token_index
                );
                continue;
            }
        }

        let bank_oracle_result =
            retriever.bank_and_oracle(&account.fixed.group, i, position.token_index);

        // Allow skipping of bad-oracle banks if the account has a nonnegative balance
        if allow_skipping_banks
            && bank_oracle_result.is_oracle_error()
            && position.indexed_position >= 0
        {
            // Ignore the asset because the oracle is bad, decreasing total health
            continue;
        }
        let (bank, oracle_price) = bank_oracle_result?;

        let native = position.native(bank);
        let prices = Prices {
            oracle: oracle_price,
            stable: bank.stable_price(),
        };
        // Use the liab price for computing weight scaling, because it's pessimistic and
        // causes the most unfavorable scaling.
        let liab_price = prices.liab(HealthType::Init);

        let (maint_asset_weight, maint_liab_weight) = bank.maint_weights(now_ts);

        token_infos.push(TokenInfo {
            token_index: bank.token_index,
            maint_asset_weight,
            init_asset_weight: bank.init_asset_weight,
            init_scaled_asset_weight: bank.scaled_init_asset_weight(liab_price),
            maint_liab_weight,
            init_liab_weight: bank.init_liab_weight,
            init_scaled_liab_weight: bank.scaled_init_liab_weight(liab_price),
            prices,
            balance_spot: native,
            allow_asset_liquidation: bank.allows_asset_liquidation(),
        });
    }

    // Fill the TokenInfo balance with free funds in serum3 oo accounts and build Serum3Infos.
    let mut serum3_infos = Vec::with_capacity(account.active_serum3_orders().count());
    for (i, serum_account) in account.active_serum3_orders().enumerate() {
        let oo = retriever.serum_oo(i, &serum_account.open_orders)?;

        // find the TokenInfos for the market's base and quote tokens
        // and potentially skip the whole serum contribution if they are not available
        let info_index_results = (
            find_token_info_index(&token_infos, serum_account.base_token_index),
            find_token_info_index(&token_infos, serum_account.quote_token_index),
        );
        let (base_info_index, quote_info_index) = match info_index_results {
            (Ok(base), Ok(quote)) => (base, quote),
            _ => {
                require_msg_typed!(
                    allow_skipping_banks,
                    MangoError::InvalidBank,
                    "serum market {} misses health accounts for bank {} or {}",
                    serum_account.market_index,
                    serum_account.base_token_index,
                    serum_account.quote_token_index,
                );
                continue;
            }
        };

        // add the amounts that are freely settleable immediately to token balances
        let base_free = I80F48::from(oo.native_coin_free);
        let quote_free = I80F48::from(oo.native_pc_free);
        let base_info = &mut token_infos[base_info_index];
        base_info.balance_spot += base_free;
        let quote_info = &mut token_infos[quote_info_index];
        quote_info.balance_spot += quote_free;

        serum3_infos.push(Serum3Info::new(
            serum_account,
            oo,
            base_info_index,
            quote_info_index,
        ));
    }

    // health contribution from perp accounts
    let mut perp_infos = Vec::with_capacity(account.active_perp_positions().count());
    for (i, perp_position) in account.active_perp_positions().enumerate() {
        let (perp_market, oracle_price) = retriever.perp_market_and_oracle_price(
            &account.fixed.group,
            i,
            perp_position.market_index,
        )?;

        // Ensure the settle token is available in the health cache
        if allow_skipping_banks {
            find_token_info_index(&token_infos, perp_market.settle_token_index)?;
        }

        perp_infos.push(PerpInfo::new(
            perp_position,
            perp_market,
            Prices {
                oracle: oracle_price,
                stable: perp_market.stable_price(),
            },
        )?);
    }

    Ok(HealthCache {
        token_infos,
        serum3_infos,
        perp_infos,
        being_liquidated: account.fixed.being_liquidated(),
    })
}

#[cfg(test)]
mod tests {
    use super::super::test::*;
    use super::*;
    use crate::state::*;
    use serum_dex::state::OpenOrders;
    use std::str::FromStr;

    #[test]
    fn test_precision() {
        // I80F48 can only represent until 1/2^48
        assert_ne!(
            I80F48::from_num(1_u128) / I80F48::from_num(2_u128.pow(48)),
            0
        );
        assert_eq!(
            I80F48::from_num(1_u128) / I80F48::from_num(2_u128.pow(49)),
            0
        );

        // I80F48 can only represent until 14 decimal points
        assert_ne!(
            I80F48::from_str(format!("0.{}1", "0".repeat(13)).as_str()).unwrap(),
            0
        );
        assert_eq!(
            I80F48::from_str(format!("0.{}1", "0".repeat(14)).as_str()).unwrap(),
            0
        );
    }

    fn health_eq(a: I80F48, b: f64) -> bool {
        if (a - I80F48::from_num(b)).abs() < 0.001 {
            true
        } else {
            println!("health is {}, but expected {}", a, b);
            false
        }
    }

    // Run a health test that includes all the side values (like referrer_rebates_accrued)
    #[test]
    fn test_health0() {
        let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
        let mut account = MangoAccountValue::from_bytes(&buffer).unwrap();

        let group = Pubkey::new_unique();

        let (mut bank1, mut oracle1) = mock_bank_and_oracle(group, 0, 1.0, 0.2, 0.1);
        let (mut bank2, mut oracle2) = mock_bank_and_oracle(group, 4, 5.0, 0.5, 0.3);
        bank1
            .data()
            .deposit(
                account.ensure_token_position(0).unwrap().0,
                I80F48::from(100),
                DUMMY_NOW_TS,
            )
            .unwrap();
        bank2
            .data()
            .withdraw_without_fee(
                account.ensure_token_position(4).unwrap().0,
                I80F48::from(10),
                DUMMY_NOW_TS,
            )
            .unwrap();

        let mut oo1 = TestAccount::<OpenOrders>::new_zeroed();
        let serum3account = account.create_serum3_orders(2).unwrap();
        serum3account.open_orders = oo1.pubkey;
        serum3account.base_token_index = 4;
        serum3account.quote_token_index = 0;
        oo1.data().native_pc_total = 21;
        oo1.data().native_coin_total = 18;
        oo1.data().native_pc_free = 1;
        oo1.data().native_coin_free = 3;
        oo1.data().referrer_rebates_accrued = 2;

        let mut perp1 = mock_perp_market(group, oracle2.pubkey, 5.0, 9, (0.2, 0.1), (0.05, 0.02));
        let perpaccount = account.ensure_perp_position(9, 0).unwrap().0;
        perpaccount.record_trade(perp1.data(), 3, -I80F48::from(310u16));
        perpaccount.bids_base_lots = 7;
        perpaccount.asks_base_lots = 11;
        perpaccount.taker_base_lots = 1;
        perpaccount.taker_quote_lots = 2;

        let oracle2_ai = oracle2.as_account_info();

        let ais = vec![
            bank1.as_account_info(),
            bank2.as_account_info(),
            oracle1.as_account_info(),
            oracle2_ai.clone(),
            perp1.as_account_info(),
            oracle2_ai,
            oo1.as_account_info(),
        ];

        let retriever = ScanningAccountRetriever::new_with_staleness(&ais, &group, None).unwrap();

        // for bank1/oracle1
        // including open orders (scenario: bids execute)
        let serum1 = 1.0 + (20.0 + 15.0 * 5.0);
        // and perp (scenario: bids execute)
        let perp1 =
            (3.0 + 7.0 + 1.0) * 10.0 * 5.0 * 0.8 + (-310.0 + 2.0 * 100.0 - 7.0 * 10.0 * 5.0);
        let health1 = (100.0 + serum1 + perp1) * 0.8;
        // for bank2/oracle2
        let health2 = (-10.0 + 3.0) * 5.0 * 1.5;
        assert!(health_eq(
            compute_health(&account.borrow(), HealthType::Init, &retriever, 0).unwrap(),
            health1 + health2
        ));
    }

    #[derive(Default)]
    struct BankSettings {
        deposits: u64,
        borrows: u64,
        deposit_weight_scale_start_quote: u64,
        borrow_weight_scale_start_quote: u64,
        potential_serum_tokens: u64,
    }

    #[derive(Default)]
    struct TestHealth1Case {
        token1: i64,
        token2: i64,
        token3: i64,
        oo_1_2: (u64, u64),
        oo_1_3: (u64, u64),
        perp1: (i64, i64, i64, i64),
        expected_health: f64,
        bank_settings: [BankSettings; 3],
        extra: Option<fn(&mut MangoAccountValue)>,
    }
    fn test_health1_runner(testcase: &TestHealth1Case) {
        let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
        let mut account = MangoAccountValue::from_bytes(&buffer).unwrap();

        let group = Pubkey::new_unique();

        let (mut bank1, mut oracle1) = mock_bank_and_oracle(group, 0, 1.0, 0.2, 0.1);
        let (mut bank2, mut oracle2) = mock_bank_and_oracle(group, 4, 5.0, 0.5, 0.3);
        let (mut bank3, mut oracle3) = mock_bank_and_oracle(group, 5, 10.0, 0.5, 0.3);
        bank1
            .data()
            .change_without_fee(
                account.ensure_token_position(0).unwrap().0,
                I80F48::from(testcase.token1),
                DUMMY_NOW_TS,
            )
            .unwrap();
        bank2
            .data()
            .change_without_fee(
                account.ensure_token_position(4).unwrap().0,
                I80F48::from(testcase.token2),
                DUMMY_NOW_TS,
            )
            .unwrap();
        bank3
            .data()
            .change_without_fee(
                account.ensure_token_position(5).unwrap().0,
                I80F48::from(testcase.token3),
                DUMMY_NOW_TS,
            )
            .unwrap();
        for (settings, bank) in testcase
            .bank_settings
            .iter()
            .zip([&mut bank1, &mut bank2, &mut bank3].iter_mut())
        {
            let bank = bank.data();
            bank.indexed_deposits = I80F48::from(settings.deposits) / bank.deposit_index;
            bank.indexed_borrows = I80F48::from(settings.borrows) / bank.borrow_index;
            bank.potential_serum_tokens = settings.potential_serum_tokens;
            if settings.deposit_weight_scale_start_quote > 0 {
                bank.deposit_weight_scale_start_quote =
                    settings.deposit_weight_scale_start_quote as f64;
            }
            if settings.borrow_weight_scale_start_quote > 0 {
                bank.borrow_weight_scale_start_quote =
                    settings.borrow_weight_scale_start_quote as f64;
            }
        }

        let mut oo1 = TestAccount::<OpenOrders>::new_zeroed();
        let serum3account1 = account.create_serum3_orders(2).unwrap();
        serum3account1.open_orders = oo1.pubkey;
        serum3account1.base_token_index = 4;
        serum3account1.quote_token_index = 0;
        oo1.data().native_pc_total = testcase.oo_1_2.0;
        oo1.data().native_coin_total = testcase.oo_1_2.1;

        let mut oo2 = TestAccount::<OpenOrders>::new_zeroed();
        let serum3account2 = account.create_serum3_orders(3).unwrap();
        serum3account2.open_orders = oo2.pubkey;
        serum3account2.base_token_index = 5;
        serum3account2.quote_token_index = 0;
        oo2.data().native_pc_total = testcase.oo_1_3.0;
        oo2.data().native_coin_total = testcase.oo_1_3.1;

        let mut perp1 = mock_perp_market(group, oracle2.pubkey, 5.0, 9, (0.2, 0.1), (0.05, 0.02));
        let perpaccount = account.ensure_perp_position(9, 0).unwrap().0;
        perpaccount.record_trade(
            perp1.data(),
            testcase.perp1.0,
            I80F48::from(testcase.perp1.1),
        );
        perpaccount.bids_base_lots = testcase.perp1.2;
        perpaccount.asks_base_lots = testcase.perp1.3;

        if let Some(extra_fn) = testcase.extra {
            extra_fn(&mut account);
        }

        let oracle2_ai = oracle2.as_account_info();
        let ais = vec![
            bank1.as_account_info(),
            bank2.as_account_info(),
            bank3.as_account_info(),
            oracle1.as_account_info(),
            oracle2_ai.clone(),
            oracle3.as_account_info(),
            perp1.as_account_info(),
            oracle2_ai,
            oo1.as_account_info(),
            oo2.as_account_info(),
        ];

        let retriever = ScanningAccountRetriever::new_with_staleness(&ais, &group, None).unwrap();

        assert!(health_eq(
            compute_health(&account.borrow(), HealthType::Init, &retriever, 0).unwrap(),
            testcase.expected_health
        ));
    }

    // Check some specific health constellations
    #[test]
    fn test_health1() {
        let base_price = 5.0;
        let base_lots_to_quote = 10.0 * base_price;
        let testcases = vec![
            TestHealth1Case { // 0
                token1: 100,
                token2: -10,
                oo_1_2: (20, 15),
                perp1: (3, -131, 7, 11),
                expected_health:
                    // for token1
                    0.8 * (100.0
                    // including open orders (scenario: bids execute)
                    + (20.0 + 15.0 * base_price)
                    // including perp (scenario: bids execute)
                    + (3.0 + 7.0) * base_lots_to_quote * 0.8 + (-131.0 - 7.0 * base_lots_to_quote))
                    // for token2
                    - 10.0 * base_price * 1.5,
                ..Default::default()
            },
            TestHealth1Case { // 1
                token1: -100,
                token2: 10,
                oo_1_2: (20, 15),
                perp1: (-10, -131, 7, 11),
                expected_health:
                    // for token1
                    1.2 * (-100.0
                    // for perp (scenario: asks execute)
                    + (-10.0 - 11.0) * base_lots_to_quote * 1.2 + (-131.0 + 11.0 * base_lots_to_quote))
                    // for token2, including open orders (scenario: asks execute)
                    + (10.0 * base_price + (20.0 + 15.0 * base_price)) * 0.5,
                ..Default::default()
            },
            TestHealth1Case {
                // 2: weighted positive perp pnl
                perp1: (-1, 100, 0, 0),
                expected_health: 0.8 * 0.95 * (100.0 - 1.2 * 1.0 * base_lots_to_quote),
                ..Default::default()
            },
            TestHealth1Case {
                // 3: negative perp pnl is not weighted (only the settle token weight)
                perp1: (1, -100, 0, 0),
                expected_health: 1.2 * (-100.0 + 0.8 * 1.0 * base_lots_to_quote),
                ..Default::default()
            },
            TestHealth1Case {
                // 4: perp health
                perp1: (10, 100, 0, 0),
                expected_health: 0.8 * 0.95 * (100.0 + 0.8 * 10.0 * base_lots_to_quote),
                ..Default::default()
            },
            TestHealth1Case {
                // 5: perp health
                perp1: (30, -100, 0, 0),
                expected_health: 0.8 * 0.95 * (-100.0 + 0.8 * 30.0 * base_lots_to_quote),
                ..Default::default()
            },
            TestHealth1Case { // 6, reserved oo funds
                token1: -100,
                token2: -10,
                token3: -10,
                oo_1_2: (1, 1),
                oo_1_3: (1, 1),
                expected_health:
                    // tokens
                    -100.0 * 1.2 - 10.0 * 5.0 * 1.5 - 10.0 * 10.0 * 1.5
                    // oo_1_2 (-> token1)
                    + (1.0 + 5.0) * 1.2
                    // oo_1_3 (-> token1)
                    + (1.0 + 10.0) * 1.2,
                ..Default::default()
            },
            TestHealth1Case { // 7, reserved oo funds cross the zero balance level
                token1: -14,
                token2: -10,
                token3: -10,
                oo_1_2: (1, 1),
                oo_1_3: (1, 1),
                expected_health:
                    // tokens
                    -14.0 * 1.2 - 10.0 * 5.0 * 1.5 - 10.0 * 10.0 * 1.5
                    // oo_1_2 (-> token1)
                    + 3.0 * 1.2 + 3.0 * 0.8
                    // oo_1_3 (-> token1)
                    + 8.0 * 1.2 + 3.0 * 0.8,
                ..Default::default()
            },
            TestHealth1Case { // 8, reserved oo funds in a non-quote currency
                token1: -100,
                token2: -100,
                token3: -1,
                oo_1_2: (0, 0),
                oo_1_3: (10, 1),
                expected_health:
                    // tokens
                    -100.0 * 1.2 - 100.0 * 5.0 * 1.5 - 10.0 * 1.5
                    // oo_1_3 (-> token3)
                    + 10.0 * 1.5 + 10.0 * 0.5,
                ..Default::default()
            },
            TestHealth1Case { // 9, like 8 but oo_1_2 flips the oo_1_3 target
                token1: -100,
                token2: -100,
                token3: -1,
                oo_1_2: (100, 0),
                oo_1_3: (10, 1),
                expected_health:
                    // tokens
                    -100.0 * 1.2 - 100.0 * 5.0 * 1.5 - 10.0 * 1.5
                    // oo_1_2 (-> token1)
                    + 80.0 * 1.2 + 20.0 * 0.8
                    // oo_1_3 (-> token1)
                    + 20.0 * 0.8,
                ..Default::default()
            },
            TestHealth1Case { // 10, checking collateral limit
                token1: 100,
                token2: 100,
                token3: 100,
                bank_settings: [
                    BankSettings {
                        deposits: 100,
                        deposit_weight_scale_start_quote: 1000,
                        ..BankSettings::default()
                    },
                    BankSettings {
                        deposits: 1500,
                        deposit_weight_scale_start_quote: 1000 * 5,
                        ..BankSettings::default()
                    },
                    BankSettings {
                        deposits: 10000,
                        deposit_weight_scale_start_quote: 1000 * 10,
                        ..BankSettings::default()
                    },
                ],
                expected_health:
                    // token1
                    0.8 * 100.0
                    // token2
                    + 0.5 * 100.0 * 5.0 * (5000.0 / (1500.0 * 5.0))
                    // token3
                    + 0.5 * 100.0 * 10.0 * (10000.0 / (10000.0 * 10.0)),
                ..Default::default()
            },
            TestHealth1Case { // 11, checking borrow limit
                token1: -100,
                token2: -100,
                token3: -100,
                bank_settings: [
                    BankSettings {
                        borrows: 100,
                        borrow_weight_scale_start_quote: 1000,
                        ..BankSettings::default()
                    },
                    BankSettings {
                        borrows: 1500,
                        borrow_weight_scale_start_quote: 1000 * 5,
                        ..BankSettings::default()
                    },
                    BankSettings {
                        borrows: 10000,
                        borrow_weight_scale_start_quote: 1000 * 10,
                        ..BankSettings::default()
                    },
                ],
                expected_health:
                    // token1
                    -1.2 * 100.0
                    // token2
                    - 1.5 * 100.0 * 5.0 * (1500.0 * 5.0 / 5000.0)
                    // token3
                    - 1.5 * 100.0 * 10.0 * (10000.0 * 10.0 / 10000.0),
                ..Default::default()
            },
            TestHealth1Case {
                // 12: positive perp health offsets token borrow
                token1: -100,
                perp1: (1, 100, 0, 0),
                expected_health: 0.8 * (-100.0 + 0.95 * (100.0 + 0.8 * 1.0 * base_lots_to_quote)),
                ..Default::default()
            },
            TestHealth1Case {
                // 13: negative perp health offsets token deposit
                token1: 100,
                perp1: (-1, -100, 0, 0),
                expected_health: 1.2 * (100.0 - 100.0 - 1.2 * 1.0 * base_lots_to_quote),
                ..Default::default()
            },
            TestHealth1Case {
                // 14, reserved oo funds with max bid/min ask
                token1: -100,
                token2: -10,
                token3: 0,
                oo_1_2: (1, 1),
                oo_1_3: (11, 1),
                expected_health:
                    // tokens
                    -100.0 * 1.2 - 10.0 * 5.0 * 1.5
                    // oo_1_2 (-> token1)
                    + (1.0 + 3.0) * 1.2
                    // oo_1_3 (-> token3)
                    + (11.0 / 12.0 + 1.0) * 10.0 * 0.5,
                extra: Some(|account: &mut MangoAccountValue| {
                    let s2 = account.serum3_orders_mut(2).unwrap();
                    s2.lowest_placed_ask = 3.0;
                    let s3 = account.serum3_orders_mut(3).unwrap();
                    s3.highest_placed_bid_inv = 1.0 / 12.0;
                }),
                ..Default::default()
            },
            TestHealth1Case {
                // 15, reserved oo funds with max bid/min ask not crossing oracle
                token1: -100,
                token2: -10,
                token3: 0,
                oo_1_2: (1, 1),
                oo_1_3: (11, 1),
                expected_health:
                    // tokens
                    -100.0 * 1.2 - 10.0 * 5.0 * 1.5
                    // oo_1_2 (-> token1)
                    + (1.0 + 5.0) * 1.2
                    // oo_1_3 (-> token3)
                    + (11.0 / 10.0 + 1.0) * 10.0 * 0.5,
                extra: Some(|account: &mut MangoAccountValue| {
                    let s2 = account.serum3_orders_mut(2).unwrap();
                    s2.lowest_placed_ask = 6.0;
                    let s3 = account.serum3_orders_mut(3).unwrap();
                    s3.highest_placed_bid_inv = 1.0 / 9.0;
                }),
                ..Default::default()
            },
            TestHealth1Case {
                // 16, base case for 17
                token1: 100,
                token2: 100,
                token3: 100,
                oo_1_2: (0, 100),
                oo_1_3: (0, 100),
                expected_health:
                    // tokens
                    100.0 * 0.8 + 100.0 * 5.0 * 0.5 + 100.0 * 10.0 * 0.5
                    // oo_1_2 (-> token2)
                    + 100.0 * 5.0 * 0.5
                    // oo_1_3 (-> token1)
                    + 100.0 * 10.0 * 0.5,
                ..Default::default()
            },
            TestHealth1Case {
                // 17, potential_serum_tokens counts for deposit weight scaling
                token1: 100,
                token2: 100,
                token3: 100,
                oo_1_2: (0, 100),
                oo_1_3: (0, 100),
                bank_settings: [
                    BankSettings {
                        ..BankSettings::default()
                    },
                    BankSettings {
                        deposits: 100,
                        deposit_weight_scale_start_quote: 100 * 5,
                        potential_serum_tokens: 100,
                        ..BankSettings::default()
                    },
                    BankSettings {
                        deposits: 600,
                        deposit_weight_scale_start_quote: 500 * 10,
                        potential_serum_tokens: 100,
                        ..BankSettings::default()
                    },
                ],
                expected_health:
                    // tokens
                    100.0 * 0.8 + 100.0 * 5.0 * 0.5 * (100.0 / 200.0) + 100.0 * 10.0 * 0.5 * (500.0 / 700.0)
                    // oo_1_2 (-> token2)
                    + 100.0 * 5.0 * 0.5 * (100.0 / 200.0)
                    // oo_1_3 (-> token1)
                    + 100.0 * 10.0 * 0.5 * (500.0 / 700.0),
                ..Default::default()
            },
        ];

        for (i, testcase) in testcases.iter().enumerate() {
            println!("checking testcase {}", i);
            test_health1_runner(testcase);
        }
    }

    #[test]
    fn test_health_with_skips() {
        let testcase = TestHealth1Case {
            // 6, reserved oo funds
            token1: 100,
            token2: 10,
            token3: -10,
            oo_1_2: (5, 1),
            oo_1_3: (0, 0),
            ..Default::default()
        };

        let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
        let mut account = MangoAccountValue::from_bytes(&buffer).unwrap();

        let group = Pubkey::new_unique();
        account.fixed.group = group;

        let (mut bank1, mut oracle1) = mock_bank_and_oracle(group, 0, 1.0, 0.2, 0.1);
        let (mut bank2, mut oracle2) = mock_bank_and_oracle(group, 4, 5.0, 0.5, 0.3);
        let (mut bank3, mut oracle3) = mock_bank_and_oracle(group, 5, 10.0, 0.5, 0.3);
        bank1
            .data()
            .change_without_fee(
                account.ensure_token_position(0).unwrap().0,
                I80F48::from(testcase.token1),
                DUMMY_NOW_TS,
            )
            .unwrap();
        bank2
            .data()
            .change_without_fee(
                account.ensure_token_position(4).unwrap().0,
                I80F48::from(testcase.token2),
                DUMMY_NOW_TS,
            )
            .unwrap();
        bank3
            .data()
            .change_without_fee(
                account.ensure_token_position(5).unwrap().0,
                I80F48::from(testcase.token3),
                DUMMY_NOW_TS,
            )
            .unwrap();

        let mut oo1 = TestAccount::<OpenOrders>::new_zeroed();
        let serum3account1 = account.create_serum3_orders(2).unwrap();
        serum3account1.open_orders = oo1.pubkey;
        serum3account1.base_token_index = 4;
        serum3account1.quote_token_index = 0;
        oo1.data().native_pc_total = testcase.oo_1_2.0;
        oo1.data().native_coin_total = testcase.oo_1_2.1;

        fn compute_health_with_retriever<'a, 'info>(
            ais: &[AccountInfo],
            account: &MangoAccountValue,
            group: Pubkey,
            kind: bool,
        ) -> Result<I80F48> {
            let hc = if kind {
                let retriever =
                    ScanningAccountRetriever::new_with_staleness(&ais, &group, None).unwrap();
                new_health_cache_skipping_missing_banks_and_bad_oracles(
                    &account.borrow(),
                    &retriever,
                    DUMMY_NOW_TS,
                )?
            } else {
                let retriever = new_fixed_order_account_retriever_with_optional_banks(
                    &ais,
                    &account.borrow(),
                    (0, 0),
                )
                .unwrap();
                new_health_cache_skipping_missing_banks_and_bad_oracles(
                    &account.borrow(),
                    &retriever,
                    DUMMY_NOW_TS,
                )?
            };
            Ok(hc.health(HealthType::Init))
        }

        for retriever_kind in [false, true] {
            // baseline with everything
            {
                let ais = vec![
                    bank1.as_account_info(),
                    bank2.as_account_info(),
                    bank3.as_account_info(),
                    oracle1.as_account_info(),
                    oracle2.as_account_info(),
                    oracle3.as_account_info(),
                    oo1.as_account_info(),
                ];

                let health =
                    compute_health_with_retriever(&ais, &account, group, retriever_kind).unwrap();
                assert!(health_eq(
                    health,
                    0.8 * 100.0 + 0.5 * 5.0 * (10.0 + 2.0) - 1.5 * 10.0 * 10.0
                ));
            }

            // missing bank1
            {
                let ais = vec![
                    bank2.as_account_info(),
                    bank3.as_account_info(),
                    oracle2.as_account_info(),
                    oracle3.as_account_info(),
                    oo1.as_account_info(),
                ];

                let health =
                    compute_health_with_retriever(&ais, &account, group, retriever_kind).unwrap();
                assert!(health_eq(health, 0.5 * 5.0 * 10.0 - 1.5 * 10.0 * 10.0));
            }

            // missing bank2
            {
                let ais = vec![
                    bank1.as_account_info(),
                    bank3.as_account_info(),
                    oracle1.as_account_info(),
                    oracle3.as_account_info(),
                    oo1.as_account_info(),
                ];

                let health =
                    compute_health_with_retriever(&ais, &account, group, retriever_kind).unwrap();
                assert!(health_eq(health, 0.8 * 100.0 - 1.5 * 10.0 * 10.0));
            }

            // missing bank1 and 2
            {
                let ais = vec![
                    bank3.as_account_info(),
                    oracle3.as_account_info(),
                    oo1.as_account_info(),
                ];

                let health =
                    compute_health_with_retriever(&ais, &account, group, retriever_kind).unwrap();
                assert!(health_eq(health, -1.5 * 10.0 * 10.0));
            }

            // missing bank3
            {
                let ais = vec![
                    bank1.as_account_info(),
                    bank2.as_account_info(),
                    oracle1.as_account_info(),
                    oracle2.as_account_info(),
                    oo1.as_account_info(),
                ];

                // bank3 has a negative balance and can't be skipped!
                assert!(
                    compute_health_with_retriever(&ais, &account, group, retriever_kind).is_err()
                );
            }
        }
    }
}
