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

use crate::{
    collateral_swap_kamino as collateral_swap_kamino_single, start_swap_kamino,
    CollateralSwapKaminoParams, KaminoSwapReserveParams, StartSwapKaminoParams,
};

pub struct CollateralSwapKaminoOperationParams {
    pub min_repay_amount: u64,
    pub max_withdraw_amount: u64,
    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 `from` and `to` reserves must belong to this market — see
    /// [`CollateralSwapKaminoParams::kamino_market`].
    pub kamino_market: Pubkey,
    pub from: KaminoSwapReserveParams,
    pub to: KaminoSwapReserveParams,
    pub scope_prices: Option<Pubkey>,
    /// Router instructions (Titan / Jupiter / Raydium / etc.) that perform the
    /// actual token swap between [`start_swap_kamino`] and
    /// [`collateral_swap_kamino`]. They are inserted verbatim in order between
    /// the two Pyra instructions.
    pub swap_instructions: Vec<Instruction>,
}

/// Composite Kamino collateral swap: `start_swap_kamino` then the caller's
/// router instructions then `collateral_swap_kamino`. Caller is responsible
/// for prepending any required `refresh_kamino` instructions.
pub fn collateral_swap_kamino(params: &CollateralSwapKaminoOperationParams) -> 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 collateral_swap_params = CollateralSwapKaminoParams {
        min_repay_amount: params.min_repay_amount,
        max_withdraw_amount: params.max_withdraw_amount,
        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,
        to: params.to,
        scope_prices: params.scope_prices,
    };
    let collateral_swap_ix = collateral_swap_kamino_single(&collateral_swap_params);
    instructions.push(collateral_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>) -> CollateralSwapKaminoOperationParams {
        CollateralSwapKaminoOperationParams {
            min_repay_amount: 100,
            max_withdraw_amount: 1_000,
            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(),
            to: reserve_params(),
            scope_prices: None,
            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)];
        let params = base_params(routers.clone());
        let ixs = collateral_swap_kamino(&params);

        // start + 2 router + collateral_swap = 4
        assert_eq!(ixs.len(), 4);

        // First is start_swap_kamino (Pyra program)
        assert_eq!(ixs[0].program_id, pyra_accounts::PYRA_PROGRAM_ID);

        // Middle two are the router ixs verbatim
        assert_eq!(ixs[1].program_id, routers[0].program_id);
        assert_eq!(ixs[1].data, routers[0].data);
        assert_eq!(ixs[2].program_id, routers[1].program_id);
        assert_eq!(ixs[2].data, routers[1].data);

        // Last is the collateral_swap_kamino execute (Pyra program, different
        // discriminator than the start).
        assert_eq!(ixs[3].program_id, pyra_accounts::PYRA_PROGRAM_ID);
        assert_ne!(&ixs[0].data, &ixs[3].data);
    }

    #[test]
    fn empty_router_still_brackets_start_and_execute() {
        let ixs = collateral_swap_kamino(&base_params(vec![]));
        assert_eq!(ixs.len(), 2);
        assert_eq!(ixs[0].program_id, pyra_accounts::PYRA_PROGRAM_ID);
        assert_eq!(ixs[1].program_id, pyra_accounts::PYRA_PROGRAM_ID);
        assert_ne!(&ixs[0].data, &ixs[1].data);
    }
}
