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

use crate::constants::{
    KAMINO_FARMS_PROGRAM_ID, KLEND_PROGRAM_ID, RENT_SYSVAR_ID, SYSTEM_PROGRAM_ID,
};

/// Which farm slot on the reserve to initialise the obligation's
/// `UserState` against. A reserve can have either, both, or neither farm
/// configured — read `reserve.farm_collateral` / `reserve.farm_debt` to
/// decide which kind to init for, and pass the matching pubkey as
/// `reserve_farm_state`.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum KaminoReserveFarmKind {
    Collateral,
    Debt,
}

impl KaminoReserveFarmKind {
    /// Encodes to the on-chain `mode: u8` arg.
    ///
    /// **`Collateral = 0`, `Debt = 1` is klend's external contract**, not an
    /// internal SDK choice — these byte values mirror the on-chain
    /// `ReserveFarmKind` enum dispatched on by `init_obligation_farms_for_reserve`.
    /// Renumbering would silently break on-chain dispatch; the discriminator
    /// pin test (`klend_discriminator_pinned`) and `mode_byte_changes_with_kind`
    /// guard against that.
    pub fn as_mode(self) -> u8 {
        match self {
            Self::Collateral => 0,
            Self::Debt => 1,
        }
    }
}

pub struct InitKaminoObligationFarmParams {
    pub owner: Pubkey,
    pub payer: Pubkey,
    pub lending_market: Pubkey,
    pub reserve: Pubkey,
    /// The farm-state pubkey stored on the reserve for the chosen `kind`.
    /// Read from `reserve.farm_collateral` (if `kind == Collateral`) or
    /// `reserve.farm_debt` (if `kind == Debt`). The klend handler validates
    /// `reserve.get_farm(kind) == reserve_farm_state` on chain.
    pub reserve_farm_state: Pubkey,
    pub kind: KaminoReserveFarmKind,
}

/// Initialise a Kamino Farms `UserState` for an obligation's stake in a
/// reserve's farm.
///
/// Required once per `(obligation, reserve_farm_state)` before the
/// obligation interacts with any klend V2 ix on a reserve whose farm is
/// configured — otherwise the trailing `refresh_farms!` CPI inside the ix
/// fails with `FarmAccountsMissing`. If a reserve has both `farm_collateral`
/// and `farm_debt`, two calls are needed (one per `KaminoReserveFarmKind`).
///
/// Permissionless: `payer` is the only signer; the obligation's stored
/// owner (the Pyra vault) is checked via `has_one`, not required to sign.
///
/// **Not idempotent** — the klend CPI errors if the `UserState` PDA is
/// already initialised. Callers should `getAccountInfo` on
/// [`pyra_accounts::get_kamino_obligation_farm_user_state`] first and skip
/// this ix when the account already exists.
pub fn init_kamino_obligation_farm(params: &InitKaminoObligationFarmParams) -> Instruction {
    let vault = pyra_accounts::get_vault(&params.owner);
    let obligation = pyra_accounts::get_kamino_obligation(&params.owner, &params.lending_market);
    let lending_market_authority =
        pyra_accounts::get_kamino_lending_market_authority(&params.lending_market);
    let obligation_farm = pyra_accounts::get_kamino_obligation_farm_user_state(
        &params.reserve_farm_state,
        &obligation,
    );

    let accounts = crate::klend_program::client::accounts::InitObligationFarmsForReserve {
        payer: params.payer,
        // The on-chain handler checks `has_one = owner` against the
        // obligation. Pyra obligations store the vault as their owner, so
        // we pass the vault here — NOT the end-user wallet.
        owner: vault,
        obligation,
        lending_market_authority,
        reserve: params.reserve,
        reserve_farm_state: params.reserve_farm_state,
        obligation_farm,
        lending_market: params.lending_market,
        farms_program: KAMINO_FARMS_PROGRAM_ID,
        rent: RENT_SYSVAR_ID,
        system_program: SYSTEM_PROGRAM_ID,
    }
    .to_account_metas(None);

    let data = crate::klend_program::client::args::InitObligationFarmsForReserve {
        mode: params.kind.as_mode(),
    }
    .data();

    Instruction {
        program_id: KLEND_PROGRAM_ID,
        accounts,
        data,
    }
}

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

    fn base_params(kind: KaminoReserveFarmKind) -> InitKaminoObligationFarmParams {
        InitKaminoObligationFarmParams {
            owner: Pubkey::new_unique(),
            payer: Pubkey::new_unique(),
            lending_market: Pubkey::new_unique(),
            reserve: Pubkey::new_unique(),
            reserve_farm_state: Pubkey::new_unique(),
            kind,
        }
    }

    #[test]
    fn builds_klend_instruction_with_expected_account_count() {
        let params = base_params(KaminoReserveFarmKind::Debt);
        let ix = init_kamino_obligation_farm(&params);

        assert_eq!(ix.program_id, KLEND_PROGRAM_ID);
        // payer, owner, obligation, lending_market_authority, reserve,
        // reserve_farm_state, obligation_farm, lending_market,
        // farms_program, rent, system_program = 11
        assert_eq!(ix.accounts.len(), 11);
    }

    #[test]
    fn payer_is_only_signer() {
        let params = base_params(KaminoReserveFarmKind::Debt);
        let ix = init_kamino_obligation_farm(&params);

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

        // No other account signs — this is the permissionless property the
        // protocol relies on.
        let signers: Vec<_> = ix.accounts.iter().filter(|m| m.is_signer).collect();
        assert_eq!(signers.len(), 1);
    }

    #[test]
    fn obligation_owner_meta_is_the_vault_not_the_user() {
        let params = base_params(KaminoReserveFarmKind::Debt);
        let ix = init_kamino_obligation_farm(&params);

        let vault = pyra_accounts::get_vault(&params.owner);
        assert!(ix.accounts.iter().any(|m| m.pubkey == vault));
        // The end-user wallet itself must NOT appear as an account meta.
        assert!(ix.accounts.iter().all(|m| m.pubkey != params.owner));
    }

    #[test]
    fn obligation_farm_pda_uses_reserve_farm_state_and_obligation() {
        let params = base_params(KaminoReserveFarmKind::Collateral);
        let ix = init_kamino_obligation_farm(&params);

        let obligation =
            pyra_accounts::get_kamino_obligation(&params.owner, &params.lending_market);
        let expected = pyra_accounts::get_kamino_obligation_farm_user_state(
            &params.reserve_farm_state,
            &obligation,
        );
        // Pin the obligation_farm to slot 6 of klend's 11-account layout for
        // `init_obligation_farms_for_reserve` (payer, owner, obligation,
        // lending_market_authority, reserve, reserve_farm_state,
        // obligation_farm, lending_market, farms_program, rent,
        // system_program). Presence-anywhere would silently rot if klend
        // reorders accounts in a future IDL.
        assert_eq!(ix.accounts[6].pubkey, expected);
    }

    #[test]
    fn mode_byte_changes_with_kind() {
        let collateral = init_kamino_obligation_farm(&base_params(KaminoReserveFarmKind::Collateral));
        let debt = init_kamino_obligation_farm(&base_params(KaminoReserveFarmKind::Debt));

        // Last byte of the data is the mode arg.
        let collateral_mode = collateral.data.last().copied().unwrap();
        let debt_mode = debt.data.last().copied().unwrap();
        assert_eq!(collateral_mode, 0);
        assert_eq!(debt_mode, 1);
    }

    #[test]
    fn klend_discriminator_pinned() {
        let params = base_params(KaminoReserveFarmKind::Debt);
        let ix = init_kamino_obligation_farm(&params);

        // klend `init_obligation_farms_for_reserve` discriminator + mode = 1
        let expected: [u8; 9] = [136, 63, 15, 186, 211, 152, 168, 164, 1];
        assert_eq!(ix.data, expected);
    }
}
