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,
    },
    pyra_program::client::accounts::RepaySwapKamino,
    shared_accounts::{KaminoAccounts, KaminoCTokenAccounts, KaminoOraclesAccounts, SwapAccounts},
    single::kamino::KaminoSwapReserveParams,
};

pub struct RepaySwapKaminoParams {
    pub min_repay_amount: u64,
    pub max_withdraw_amount: u64,
    /// Liquidator race-condition guard. Only consulted when `owner` is **not**
    /// a signer (i.e. `liquidity_provider` must equal the protocol LIQUIDATOR
    /// keypair). When `true`, the on-chain handler asserts the obligation is
    /// actually unhealthy (`starting_health == 0`) and aborts with
    /// `LiquidationHealthNotZero` otherwise — preventing two liquidators from
    /// racing on a position that just got cured. Owner-signed swaps should
    /// pass `false`.
    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. The obligation is derived from
    /// `(owner, market)` and Kamino enforces on chain that all reserves
    /// touched in a single ix share one obligation.
    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 fn repay_swap_kamino(params: &RepaySwapKaminoParams) -> Instruction {
    let vault = pyra_accounts::get_vault(&params.owner);
    let vault_ata_from = pyra_accounts::get_associated_token_address(
        &vault,
        &params.mint_from,
        &params.token_program_from,
    );
    let vault_ata_to = pyra_accounts::get_associated_token_address(
        &vault,
        &params.mint_to,
        &params.token_program_to,
    );
    let liquidity_provider_ata_from = pyra_accounts::get_associated_token_address(
        &params.liquidity_provider,
        &params.mint_from,
        &params.token_program_from,
    );
    let liquidity_provider_ata_to = pyra_accounts::get_associated_token_address(
        &params.liquidity_provider,
        &params.mint_to,
        &params.token_program_to,
    );
    let ledger = pyra_accounts::get_swap_ledger(&params.mint_from);
    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 accounts = RepaySwapKamino {
        repay_swap_accounts: SwapAccounts {
            payer: params.payer,
            liquidity_provider: params.liquidity_provider,
            liquidity_provider_ata_from,
            liquidity_provider_ata_to,
            owner: params.owner,
            vault,
            vault_ata_from,
            vault_ata_to,
            mint_from: params.mint_from,
            mint_to: params.mint_to,
            token_program_from: params.token_program_from,
            token_program_to: params.token_program_to,
            associated_token_program: pyra_accounts::ASSOCIATED_TOKEN_PROGRAM_ID,
            system_program: SYSTEM_PROGRAM_ID,
            instructions_sysvar_account: INSTRUCTIONS_SYSVAR_ID,
        },
        ledger,
        repay_swap_kamino_from_accounts: KaminoAccounts {
            program: KLEND_PROGRAM_ID,
            market: params.kamino_market,
            market_authority: kamino_market_authority,
            reserve: params.from.reserve,
            reserve_liquidity_supply: params.from.reserve_liquidity_supply,
            farms_program: KAMINO_FARMS_PROGRAM_ID,
            user_farm_state: params.from.user_farm_state,
            reserve_farm_state: params.from.reserve_farm_state,
            instructions_sysvar_account: INSTRUCTIONS_SYSVAR_ID,
        },
        repay_swap_kamino_from_c_token: KaminoCTokenAccounts {
            reserve_mint: params.from.reserve_collateral_mint,
            reserve_supply: params.from.reserve_collateral_supply,
            token_program: params.from.reserve_collateral_token_program,
        },
        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,
        kamino_obligation,
        repay_swap_kamino_oracles: KaminoOraclesAccounts {
            scope_prices: params.scope_prices,
        },
    }
    .to_account_metas(None);

    let data = crate::pyra_program::client::args::RepaySwapKamino {
        min_repay_amount: params.min_repay_amount,
        max_withdraw_amount: params.max_withdraw_amount,
        is_liquidation_attempt: params.is_liquidation_attempt,
    }
    .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() -> RepaySwapKaminoParams {
        let token_program = pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
        RepaySwapKaminoParams {
            min_repay_amount: 500_000,
            max_withdraw_amount: 1_500_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: token_program,
            token_program_to: token_program,
            kamino_market: Pubkey::new_unique(),
            from: 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: token_program,
                user_farm_state: None,
                reserve_farm_state: None,
            },
            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,
        }
    }

    #[test]
    fn test_repay_swap_kamino_instruction() {
        let params = base_params();
        let ix = repay_swap_kamino(&params);

        assert_eq!(ix.program_id, pyra_accounts::PYRA_PROGRAM_ID);
        // 36 accounts (all optional slots always present)
        assert_eq!(ix.accounts.len(), 36);

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

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

        let ledger = pyra_accounts::get_swap_ledger(&params.mint_from);
        let ledger_meta = ix.accounts.iter().find(|m| m.pubkey == ledger).unwrap();
        assert!(ledger_meta.is_writable);

        let obligation = pyra_accounts::get_kamino_obligation(&params.owner, &params.kamino_market);
        let obligation_meta = ix.accounts.iter().find(|m| m.pubkey == obligation).unwrap();
        assert!(obligation_meta.is_writable);

        // From-reserve and to-reserve both present and distinct
        assert!(ix.accounts.iter().any(|m| m.pubkey == params.from.reserve));
        assert!(ix
            .accounts
            .iter()
            .any(|m| m.pubkey == params.kamino_to_reserve));

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

    #[test]
    fn test_repay_swap_kamino_liquidation_flag_changes_data() {
        let normal = repay_swap_kamino(&base_params());

        let mut liq_params = base_params();
        liq_params.is_liquidation_attempt = true;
        let liq = repay_swap_kamino(&liq_params);

        // Liquidation flag is part of the instruction data, so the encoded data must differ.
        assert_ne!(normal.data, liq.data);
        // Account list is identical regardless of the flag.
        assert_eq!(normal.accounts.len(), liq.accounts.len());
    }

    /// Pin discriminator + arg layout. `repay_swap_kamino` uses a four-byte
    /// sub-discriminator ([0, 0, 0, 2]) of the swap-family dispatcher, two
    /// little-endian u64 args, then one bool (1 byte).
    #[test]
    fn repay_swap_kamino_data_layout_pinned() {
        let mut params = base_params();
        params.min_repay_amount = 0x0102_0304_0506_0708;
        params.max_withdraw_amount = 0x1112_1314_1516_1718;
        params.is_liquidation_attempt = true;
        let ix = repay_swap_kamino(&params);

        let mut expected: Vec<u8> = vec![0, 0, 0, 2];
        expected.extend_from_slice(&params.min_repay_amount.to_le_bytes());
        expected.extend_from_slice(&params.max_withdraw_amount.to_le_bytes());
        expected.push(1); // is_liquidation_attempt: true
        assert_eq!(ix.data, expected);
    }
}
