use anchor_lang::{InstructionData, ToAccountMetas};
use solana_program::{instruction::Instruction, pubkey::Pubkey};

use crate::{
    constants::{
        INSTRUCTIONS_SYSVAR_ID, KAMINO_FARMS_PROGRAM_ID, KLEND_PROGRAM_ID, SYSTEM_PROGRAM_ID,
    },
    shared_accounts::{BaseAccounts, KaminoAccounts, WithdrawOrderAccounts},
};

pub struct FulfillBorrowKaminoParams {
    pub amount_base_units: u64,
    pub withdraw_order: Pubkey,
    pub owner: Pubkey,
    pub order_payer: Pubkey,
    pub admin: Pubkey,
    /// Whether `admin` is signing this transaction. The on-chain handler reads
    /// `admin.is_signer` at runtime via `is_admin_signer` to decide whether to
    /// skip the time-lock delay. Anchor's auto-derived metas mark `admin` as a
    /// non-signer (it's an `UncheckedAccount`), so callers acting as admin
    /// must set this to `true` to include `admin` in the message's required
    /// signer set.
    pub admin_is_signer: bool,
    pub payer: Pubkey,
    pub destination: Pubkey,
    pub liquidity_mint: Pubkey,
    pub liquidity_token_program: Pubkey,
    pub kamino_market: Pubkey,
    pub kamino_reserve: Pubkey,
    pub kamino_reserve_liquidity_supply: Pubkey,
    pub borrow_reserve_liquidity_fee_receiver: Pubkey,
    pub kamino_user_farm_state: Option<Pubkey>,
    pub kamino_reserve_farm_state: Option<Pubkey>,
}

pub fn fulfill_borrow_kamino(params: &FulfillBorrowKaminoParams) -> Instruction {
    let vault = pyra_accounts::get_vault(&params.owner);
    let vault_liquidity_ata = pyra_accounts::get_associated_token_address(
        &vault,
        &params.liquidity_mint,
        &params.liquidity_token_program,
    );
    let kamino_obligation =
        pyra_accounts::get_kamino_obligation(&params.owner, &params.kamino_market);
    let kamino_market_authority =
        pyra_accounts::get_kamino_lending_market_authority(&params.kamino_market);

    let destination_spl = pyra_accounts::get_associated_token_address(
        &params.destination,
        &params.liquidity_mint,
        &params.liquidity_token_program,
    );
    let kamino_referrer_token_state =
        pyra_accounts::get_kamino_referrer_token_state(&params.kamino_reserve);

    let mut accounts = crate::pyra_program::client::accounts::FulfillBorrowKamino {
        borrow_base_accounts: BaseAccounts {
            payer: params.payer,
            vault,
            vault_ata: vault_liquidity_ata,
            mint: params.liquidity_mint,
            token_program: params.liquidity_token_program,
            associated_token_program: pyra_accounts::ASSOCIATED_TOKEN_PROGRAM_ID,
            system_program: SYSTEM_PROGRAM_ID,
        },
        borrow_kamino: KaminoAccounts {
            program: KLEND_PROGRAM_ID,
            market: params.kamino_market,
            market_authority: kamino_market_authority,
            reserve: params.kamino_reserve,
            reserve_liquidity_supply: params.kamino_reserve_liquidity_supply,
            farms_program: KAMINO_FARMS_PROGRAM_ID,
            user_farm_state: params.kamino_user_farm_state,
            reserve_farm_state: params.kamino_reserve_farm_state,
            instructions_sysvar_account: INSTRUCTIONS_SYSVAR_ID,
        },
        kamino_referrer_token_state,
        kamino_reserve_liquidity_fee_receiver: params.borrow_reserve_liquidity_fee_receiver,
        kamino_obligation,
        borrow_time_lock: WithdrawOrderAccounts {
            withdraw_order: params.withdraw_order,
            order_payer: params.order_payer,
            admin: params.admin,
            destination: params.destination,
        },
        destination_spl,
    }
    .to_account_metas(None);

    if params.admin_is_signer {
        // `FulfillBorrowKamino` always emits an `admin` meta via the
        // `borrow_time_lock` composite. Crash loudly if that invariant ever
        // breaks so callers don't silently ship a non-signing transaction.
        #[allow(clippy::expect_used)]
        let meta = accounts
            .iter_mut()
            .find(|m| m.pubkey == params.admin)
            .expect("FulfillBorrowKamino metas must include admin");
        meta.is_signer = true;
    }

    let data = crate::pyra_program::client::args::FulfillBorrowKamino {
        amount_base_units: params.amount_base_units,
    }
    .data();

    Instruction {
        program_id: pyra_accounts::PYRA_PROGRAM_ID,
        accounts,
        data,
    }
}

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

    fn base_params(admin_is_signer: bool) -> FulfillBorrowKaminoParams {
        let token_program = pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
        FulfillBorrowKaminoParams {
            withdraw_order: Pubkey::new_unique(),
            owner: Pubkey::new_unique(),
            order_payer: Pubkey::new_unique(),
            admin: Pubkey::new_unique(),
            admin_is_signer,
            payer: Pubkey::new_unique(),
            destination: Pubkey::new_unique(),
            liquidity_mint: Pubkey::new_unique(),
            liquidity_token_program: token_program,
            kamino_market: Pubkey::new_unique(),
            kamino_reserve: Pubkey::new_unique(),
            kamino_reserve_liquidity_supply: Pubkey::new_unique(),
            borrow_reserve_liquidity_fee_receiver: Pubkey::new_unique(),
            amount_base_units: 1_000_000,
            kamino_user_farm_state: None,
            kamino_reserve_farm_state: None,
        }
    }

    #[test]
    fn admin_not_signer_keeps_admin_meta_unsigned() {
        let params = base_params(false);
        let ix = fulfill_borrow_kamino(&params);

        assert_eq!(ix.program_id, pyra_accounts::PYRA_PROGRAM_ID);
        // 22 fixed + 2 optional farm-state slots (passed as None, but Anchor
        // still emits them in the meta list).
        assert_eq!(ix.accounts.len(), 24);

        let admin_meta = ix
            .accounts
            .iter()
            .find(|m| m.pubkey == params.admin)
            .unwrap();
        assert!(!admin_meta.is_signer);

        // payer is always a signer
        let payer_meta = ix.accounts.iter().find(|m| m.pubkey == params.payer).unwrap();
        assert!(payer_meta.is_signer);
        assert!(payer_meta.is_writable);

        let withdraw_order_meta = ix
            .accounts
            .iter()
            .find(|m| m.pubkey == params.withdraw_order)
            .unwrap();
        assert!(withdraw_order_meta.is_writable);

        let destination_spl = pyra_accounts::get_associated_token_address(
            &params.destination,
            &params.liquidity_mint,
            &params.liquidity_token_program,
        );
        assert!(ix.accounts.iter().any(|m| m.pubkey == destination_spl));

        // Referrer token state is derived from the reserve, not caller-supplied.
        let expected_referrer =
            pyra_accounts::get_kamino_referrer_token_state(&params.kamino_reserve);
        assert!(ix.accounts.iter().any(|m| m.pubkey == expected_referrer));

        assert!(!ix.data.is_empty());
    }

    #[test]
    fn admin_is_signer_flips_admin_meta() {
        let params = base_params(true);
        let ix = fulfill_borrow_kamino(&params);

        let admin_meta = ix
            .accounts
            .iter()
            .find(|m| m.pubkey == params.admin)
            .unwrap();
        assert!(admin_meta.is_signer);
    }
}
