use solana_program::instruction::Instruction;
use solana_pubkey::Pubkey;

use crate::{
    repay_swap_kamino as repay_swap_kamino_single, start_swap_kamino, KaminoSwapReserveParams,
    RepaySwapKaminoParams, StartSwapKaminoParams,
};

pub struct RepaySwapKaminoOperationParams {
    pub min_repay_amount: u64,
    pub max_withdraw_amount: u64,
    /// Liquidator race-condition guard. See
    /// [`RepaySwapKaminoParams::is_liquidation_attempt`].
    pub is_liquidation_attempt: bool,
    pub payer: Pubkey,
    pub liquidity_provider: Pubkey,
    pub owner: Pubkey,
    pub mint_from: Pubkey,
    pub mint_to: Pubkey,
    pub token_program_from: Pubkey,
    pub token_program_to: Pubkey,
    /// Both the `from` reserve and the repay-target (`kamino_to_reserve`)
    /// must belong to this market — see
    /// [`RepaySwapKaminoParams::kamino_market`].
    pub kamino_market: Pubkey,
    /// The reserve being withdrawn from (collateral side).
    pub from: KaminoSwapReserveParams,
    /// The reserve being repaid into. Repay does not touch the c-token vault,
    /// so the c-token + collateral fields are not required here.
    pub kamino_to_reserve: Pubkey,
    pub kamino_to_reserve_liquidity_supply: Pubkey,
    pub kamino_to_user_farm_state: Option<Pubkey>,
    pub kamino_to_reserve_farm_state: Option<Pubkey>,
    pub scope_prices: Option<Pubkey>,
    pub price_update_from: Pubkey,
    pub price_update_to: Pubkey,
    /// Router instructions (Titan / Jupiter / Raydium / etc.) that perform the
    /// actual token swap between [`start_swap_kamino`] and
    /// [`repay_swap_kamino`]. They are inserted verbatim in order between
    /// the two Pyra instructions.
    pub swap_instructions: Vec<Instruction>,
}

/// Composite Kamino repay swap: `start_swap_kamino` then the caller's router
/// instructions then `repay_swap_kamino`. Caller is responsible for prepending
/// any required `refresh_kamino` instructions.
pub fn repay_swap_kamino(params: &RepaySwapKaminoOperationParams) -> Vec<Instruction> {
    let mut instructions =
        Vec::with_capacity(2usize.saturating_add(params.swap_instructions.len()));

    let start_swap_params = StartSwapKaminoParams {
        payer: params.payer,
        liquidity_provider: params.liquidity_provider,
        owner: params.owner,
        mint_from: params.mint_from,
        mint_to: params.mint_to,
        token_program_from: params.token_program_from,
        token_program_to: params.token_program_to,
    };
    let start_swap_ix = start_swap_kamino(&start_swap_params);
    instructions.push(start_swap_ix);

    instructions.extend(params.swap_instructions.iter().cloned());

    let repay_swap_params = RepaySwapKaminoParams {
        min_repay_amount: params.min_repay_amount,
        max_withdraw_amount: params.max_withdraw_amount,
        is_liquidation_attempt: params.is_liquidation_attempt,
        payer: params.payer,
        liquidity_provider: params.liquidity_provider,
        owner: params.owner,
        mint_from: params.mint_from,
        mint_to: params.mint_to,
        token_program_from: params.token_program_from,
        token_program_to: params.token_program_to,
        kamino_market: params.kamino_market,
        from: params.from,
        kamino_to_reserve: params.kamino_to_reserve,
        kamino_to_reserve_liquidity_supply: params.kamino_to_reserve_liquidity_supply,
        kamino_to_user_farm_state: params.kamino_to_user_farm_state,
        kamino_to_reserve_farm_state: params.kamino_to_reserve_farm_state,
        scope_prices: params.scope_prices,
    };
    let repay_swap_ix = repay_swap_kamino_single(&repay_swap_params);
    instructions.push(repay_swap_ix);

    instructions
}

#[cfg(test)]
#[allow(
    clippy::allow_attributes,
    clippy::allow_attributes_without_reason,
    clippy::unwrap_used
)]
mod tests {
    use super::*;
    use solana_program::instruction::AccountMeta;

    fn reserve_params() -> KaminoSwapReserveParams {
        KaminoSwapReserveParams {
            reserve: Pubkey::new_unique(),
            reserve_liquidity_supply: Pubkey::new_unique(),
            reserve_collateral_mint: Pubkey::new_unique(),
            reserve_collateral_supply: Pubkey::new_unique(),
            reserve_collateral_token_program: Pubkey::new_unique(),
            user_farm_state: None,
            reserve_farm_state: None,
        }
    }

    fn base_params(swap_instructions: Vec<Instruction>) -> RepaySwapKaminoOperationParams {
        RepaySwapKaminoOperationParams {
            min_repay_amount: 100,
            max_withdraw_amount: 1_000,
            is_liquidation_attempt: false,
            payer: Pubkey::new_unique(),
            liquidity_provider: Pubkey::new_unique(),
            owner: Pubkey::new_unique(),
            mint_from: Pubkey::new_unique(),
            mint_to: Pubkey::new_unique(),
            token_program_from: Pubkey::new_unique(),
            token_program_to: Pubkey::new_unique(),
            kamino_market: Pubkey::new_unique(),
            from: reserve_params(),
            kamino_to_reserve: Pubkey::new_unique(),
            kamino_to_reserve_liquidity_supply: Pubkey::new_unique(),
            kamino_to_user_farm_state: None,
            kamino_to_reserve_farm_state: None,
            scope_prices: None,
            price_update_from: Pubkey::new_unique(),
            price_update_to: Pubkey::new_unique(),
            swap_instructions,
        }
    }

    fn router_ix(marker: u8) -> Instruction {
        Instruction {
            program_id: Pubkey::new_unique(),
            accounts: vec![AccountMeta::new(Pubkey::new_unique(), false)],
            data: vec![marker],
        }
    }

    #[test]
    fn sandwiches_router_between_start_and_execute() {
        let routers = vec![router_ix(0xAA), router_ix(0xBB), router_ix(0xCC)];
        let params = base_params(routers.clone());
        let ixs = repay_swap_kamino(&params);

        assert_eq!(ixs.len(), 5);
        assert_eq!(ixs[0].program_id, pyra_accounts::PYRA_PROGRAM_ID);
        for (i, router) in routers.iter().enumerate() {
            assert_eq!(ixs[i.saturating_add(1)].program_id, router.program_id);
            assert_eq!(ixs[i.saturating_add(1)].data, router.data);
        }
        assert_eq!(ixs[4].program_id, pyra_accounts::PYRA_PROGRAM_ID);
        assert_ne!(&ixs[0].data, &ixs[4].data);
    }

    #[test]
    fn liquidation_flag_propagates_to_execute_ix() {
        let normal = repay_swap_kamino(&base_params(vec![]));
        let mut liq_params = base_params(vec![]);
        liq_params.is_liquidation_attempt = true;
        let liq = repay_swap_kamino(&liq_params);

        // Both have start + execute = 2 ixs
        assert_eq!(normal.len(), 2);
        assert_eq!(liq.len(), 2);
        // The flag must change the execute ix data
        assert_ne!(normal[1].data, liq[1].data);
        // …but not the start ix
        assert_eq!(normal[0].data, liq[0].data);
    }
}
