//! Message preparation for external wallet signing.

use crate::order_id::compute_order_item_id_at_index;
use crate::sdk_compat::serialize_for_sdk_signing;
use crate::types::*;
use crate::{Error, Result};
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use serde_json::json;

/// Threshold for switching to parallel preparation.
const PARALLEL_THRESHOLD: usize = 10;

/// Prepared message for external signing.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PreparedMessage {
    /// Raw canonical BULK-SDK message bytes to sign.
    #[serde(with = "serde_bytes")]
    pub message_bytes: Vec<u8>,
    /// Optional pre-computed order ID for single order transactions.
    pub order_id: Option<String>,
    /// Optional pre-computed order IDs for multi-order transactions.
    pub order_ids: Option<Vec<String>>,
    /// Compact tagged actions.
    pub actions: Vec<serde_json::Value>,
    /// Account pubkey (base58).
    pub account: String,
    /// Signer pubkey (base58).
    pub signer: String,
    /// Nonce.
    pub nonce: u64,
}

impl PreparedMessage {
    #[inline]
    pub fn message_base58(&self) -> String {
        bs58::encode(&self.message_bytes).into_string()
    }

    #[inline]
    pub fn message_base64(&self) -> String {
        use base64::{engine::general_purpose::STANDARD, Engine};
        STANDARD.encode(&self.message_bytes)
    }

    #[inline]
    pub fn message_hex(&self) -> String {
        hex::encode(&self.message_bytes)
    }
}

/// Prepare a single order item transaction.
pub fn prepare_message(
    item: OrderItem,
    account: &Pubkey,
    signer: Option<&Pubkey>,
    nonce: Option<u64>,
) -> Result<PreparedMessage> {
    let action = Action::Order { orders: vec![item] };
    prepare_action(&action, account, signer, nonce)
}

/// Prepare an atomic multi-item order transaction.
pub fn prepare_group(
    items: Vec<OrderItem>,
    account: &Pubkey,
    signer: Option<&Pubkey>,
    nonce: Option<u64>,
) -> Result<PreparedMessage> {
    if items.is_empty() {
        return Err(Error::EmptyOrders);
    }
    let action = Action::Order { orders: items };
    prepare_action(&action, account, signer, nonce)
}

/// Prepare a faucet transaction.
pub fn prepare_faucet(
    account: &Pubkey,
    signer: Option<&Pubkey>,
    nonce: Option<u64>,
) -> Result<PreparedMessage> {
    let action = Action::Faucet(Faucet::new(*account));
    prepare_action(&action, account, signer, nonce)
}

/// Prepare an agent wallet creation/deletion transaction.
pub fn prepare_agent_wallet(
    agent: &Pubkey,
    delete: bool,
    account: &Pubkey,
    signer: Option<&Pubkey>,
    nonce: Option<u64>,
) -> Result<PreparedMessage> {
    let action = Action::AgentWalletCreation(AgentWallet {
        agent: *agent,
        delete,
    });
    prepare_action(&action, account, signer, nonce)
}

/// Prepare a user settings transaction.
pub fn prepare_user_settings(
    settings: UserSettings,
    account: &Pubkey,
    signer: Option<&Pubkey>,
    nonce: Option<u64>,
) -> Result<PreparedMessage> {
    let action = Action::UpdateUserSettings(settings);
    prepare_action(&action, account, signer, nonce)
}

/// Prepare a sub-account creation transaction.
pub fn prepare_create_sub_account(
    sub_account: CreateSubAccount,
    account: &Pubkey,
    signer: Option<&Pubkey>,
    nonce: Option<u64>,
) -> Result<PreparedMessage> {
    let action = Action::CreateSubAccount(sub_account);
    prepare_action(&action, account, signer, nonce)
}

/// Prepare a sub-account removal transaction.
pub fn prepare_remove_sub_account(
    to_remove: Pubkey,
    account: &Pubkey,
    signer: Option<&Pubkey>,
    nonce: Option<u64>,
) -> Result<PreparedMessage> {
    let action = Action::RemoveSubAccount(RemoveSubAccount { to_remove });
    prepare_action(&action, account, signer, nonce)
}

/// Prepare a sub-account rename transaction.
pub fn prepare_rename_sub_account(
    rename: RenameSubAccount,
    account: &Pubkey,
    signer: Option<&Pubkey>,
    nonce: Option<u64>,
) -> Result<PreparedMessage> {
    let action = Action::RenameSubAccount(rename);
    prepare_action(&action, account, signer, nonce)
}

/// Prepare a margin transfer transaction.
pub fn prepare_transfer(
    transfer: Transfer,
    account: &Pubkey,
    signer: Option<&Pubkey>,
    nonce: Option<u64>,
) -> Result<PreparedMessage> {
    let action = Action::Transfer(transfer);
    prepare_action(&action, account, signer, nonce)
}

/// Prepare a portfolio withdraw transaction.
pub fn prepare_withdraw(
    withdraw: Withdraw,
    account: &Pubkey,
    signer: Option<&Pubkey>,
    nonce: Option<u64>,
) -> Result<PreparedMessage> {
    let action = Action::Withdraw(withdraw);
    prepare_action(&action, account, signer, nonce)
}

/// Prepare a withdraw-lock recovery transaction.
pub fn prepare_withdraw_lock_recover(
    recover: WithdrawLockRecover,
    account: &Pubkey,
    signer: Option<&Pubkey>,
    nonce: Option<u64>,
) -> Result<PreparedMessage> {
    let action = Action::WithdrawLockRecover(recover);
    prepare_action(&action, account, signer, nonce)
}

/// Prepare a multisig creation transaction.
pub fn prepare_create_multisig(
    create_multisig: CreateMultisig,
    account: &Pubkey,
    signer: Option<&Pubkey>,
    nonce: Option<u64>,
) -> Result<PreparedMessage> {
    let action = Action::CreateMultisig(create_multisig);
    prepare_action(&action, account, signer, nonce)
}

/// Prepare a multisig proposal transaction.
pub fn prepare_multisig_propose(
    propose: MultisigPropose,
    account: &Pubkey,
    signer: Option<&Pubkey>,
    nonce: Option<u64>,
) -> Result<PreparedMessage> {
    let action = Action::MultisigPropose(propose);
    prepare_action(&action, account, signer, nonce)
}

/// Prepare a multisig approve transaction.
pub fn prepare_multisig_approve(
    approve: MultisigApprove,
    account: &Pubkey,
    signer: Option<&Pubkey>,
    nonce: Option<u64>,
) -> Result<PreparedMessage> {
    let action = Action::MultisigApprove(approve);
    prepare_action(&action, account, signer, nonce)
}

/// Prepare a multisig reject transaction.
pub fn prepare_multisig_reject(
    reject: MultisigReject,
    account: &Pubkey,
    signer: Option<&Pubkey>,
    nonce: Option<u64>,
) -> Result<PreparedMessage> {
    let action = Action::MultisigReject(reject);
    prepare_action(&action, account, signer, nonce)
}

/// Prepare a multisig cancel transaction.
pub fn prepare_multisig_cancel(
    cancel: MultisigCancel,
    account: &Pubkey,
    signer: Option<&Pubkey>,
    nonce: Option<u64>,
) -> Result<PreparedMessage> {
    let action = Action::MultisigCancel(cancel);
    prepare_action(&action, account, signer, nonce)
}

/// Prepare a multisig execute transaction.
pub fn prepare_multisig_execute(
    execute: MultisigExecute,
    account: &Pubkey,
    signer: Option<&Pubkey>,
    nonce: Option<u64>,
) -> Result<PreparedMessage> {
    let action = Action::MultisigExecute(execute);
    prepare_action(&action, account, signer, nonce)
}

/// Prepare a multisig policy update transaction.
pub fn prepare_update_multisig_policy(
    update: UpdateMultisigPolicy,
    account: &Pubkey,
    signer: Option<&Pubkey>,
    nonce: Option<u64>,
) -> Result<PreparedMessage> {
    let action = Action::UpdateMultisigPolicy(update);
    prepare_action(&action, account, signer, nonce)
}

/// Low-level action preparation.
pub fn prepare_action(
    action: &Action,
    account: &Pubkey,
    signer: Option<&Pubkey>,
    nonce: Option<u64>,
) -> Result<PreparedMessage> {
    let signer_pubkey = signer.unwrap_or(account);
    let nonce = nonce.unwrap_or_else(crate::nonce::current_timestamp_millis);

    let mut message_bytes = Vec::with_capacity(512);
    serialize_for_sdk_signing(action, nonce, account, &mut message_bytes)?;

    let actions = action_to_json(action)?;
    let order_id = compute_action_order_id(action, nonce, account);
    let order_ids = compute_action_order_ids(action, nonce, account);

    Ok(PreparedMessage {
        message_bytes,
        order_id,
        order_ids,
        actions,
        account: account.to_base58(),
        signer: signer_pubkey.to_base58(),
        nonce,
    })
}

fn compute_action_order_id(action: &Action, nonce: u64, account: &Pubkey) -> Option<String> {
    match action {
        Action::Order { orders } if orders.len() == 1 => {
            let mut scratch = Vec::with_capacity(96);
            compute_order_item_id_at_index(&orders[0], 0, nonce, account, &mut scratch)
                .map(|id| id.to_base58())
        }
        _ => None,
    }
}

fn compute_action_order_ids(action: &Action, nonce: u64, account: &Pubkey) -> Option<Vec<String>> {
    match action {
        Action::Order { orders } if orders.len() > 1 => {
            let mut scratch = Vec::with_capacity(96);
            let mut ids = Vec::with_capacity(orders.len());
            for (idx, item) in orders.iter().enumerate() {
                if let Some(id) =
                    compute_order_item_id_at_index(item, idx as u32, nonce, account, &mut scratch)
                {
                    ids.push(id.to_base58());
                }
            }
            if ids.is_empty() {
                None
            } else {
                Some(ids)
            }
        }
        _ => None,
    }
}

/// Prepare multiple independent order item transactions.
pub fn prepare_all(
    items: Vec<OrderItem>,
    account: &Pubkey,
    signer: Option<&Pubkey>,
    base_nonce: Option<u64>,
) -> Result<Vec<PreparedMessage>> {
    if items.is_empty() {
        return Ok(vec![]);
    }

    let base = base_nonce.unwrap_or_else(crate::nonce::current_timestamp_millis);
    let signer_pubkey = signer.unwrap_or(account);

    if items.len() < PARALLEL_THRESHOLD {
        items
            .into_iter()
            .enumerate()
            .map(|(i, item)| prepare_single_item(item, account, signer_pubkey, base + i as u64))
            .collect()
    } else {
        items
            .into_par_iter()
            .enumerate()
            .map(|(i, item)| prepare_single_item(item, account, signer_pubkey, base + i as u64))
            .collect()
    }
}

fn prepare_single_item(
    item: OrderItem,
    account: &Pubkey,
    signer: &Pubkey,
    nonce: u64,
) -> Result<PreparedMessage> {
    let mut scratch = Vec::with_capacity(96);
    let order_id = compute_order_item_id_at_index(&item, 0, nonce, account, &mut scratch)
        .map(|id| id.to_base58());
    let action = Action::Order { orders: vec![item] };

    let mut message_bytes = Vec::with_capacity(512);
    serialize_for_sdk_signing(&action, nonce, account, &mut message_bytes)?;
    let actions = action_to_json(&action)?;

    Ok(PreparedMessage {
        message_bytes,
        order_id,
        order_ids: None,
        actions,
        account: account.to_base58(),
        signer: signer.to_base58(),
        nonce,
    })
}

/// Finalize a prepared message with a base58 signature.
pub fn finalize_transaction(prepared: PreparedMessage, signature: &str) -> SignedTransaction {
    SignedTransaction {
        actions: prepared.actions,
        nonce: prepared.nonce,
        account: prepared.account,
        signer: prepared.signer,
        signature: signature.to_string(),
        order_id: prepared.order_id,
        order_ids: prepared.order_ids,
    }
}

/// Finalize a prepared message with raw signature bytes.
pub fn finalize_transaction_bytes(
    prepared: PreparedMessage,
    signature: &[u8],
) -> SignedTransaction {
    let signature_b58 = bs58::encode(signature).into_string();
    finalize_transaction(prepared, &signature_b58)
}

/// Finalize many prepared messages with aligned signatures.
pub fn finalize_all(
    prepared: Vec<PreparedMessage>,
    signatures: Vec<&str>,
) -> Result<Vec<SignedTransaction>> {
    if prepared.len() != signatures.len() {
        return Err(Error::SignatureMismatch {
            expected: prepared.len(),
            got: signatures.len(),
        });
    }

    Ok(prepared
        .into_iter()
        .zip(signatures)
        .map(|(p, sig)| finalize_transaction(p, sig))
        .collect())
}

fn action_to_json(action: &Action) -> Result<Vec<serde_json::Value>> {
    match action {
        Action::Order { orders } => orders.iter().map(order_item_to_json).collect(),
        Action::Faucet(faucet) => {
            let mut faucet_obj = json!({ "u": faucet.user.to_base58() });
            if let Some(amount) = faucet.amount {
                faucet_obj["amount"] = json!(amount);
            }
            Ok(vec![json!({ "faucet": faucet_obj })])
        }
        Action::AgentWalletCreation(agent) => Ok(vec![json!({
            "agentWalletCreation": {
                "a": agent.agent.to_base58(),
                "d": agent.delete
            }
        })]),
        Action::UpdateUserSettings(settings) => {
            let mut ordered = settings.max_leverage.clone();
            ordered.sort_unstable_by(|left, right| left.0.cmp(&right.0));
            let mut m = serde_json::Map::with_capacity(ordered.len());
            for (symbol, lev) in ordered {
                m.insert(symbol, json!(lev));
            }
            Ok(vec![json!({ "updateUserSettings": { "m": m } })])
        }
        Action::Oracle { oracles } => Ok(oracles
            .iter()
            .map(|o| {
                json!({
                    "px": {
                        "t": o.timestamp,
                        "c": o.asset,
                        "px": o.price
                    }
                })
            })
            .collect()),
        Action::PythOracle { oracles } => {
            let entries: Vec<_> = oracles
                .iter()
                .map(|o| {
                    json!({
                        "t": o.timestamp,
                        "fi": o.feed_index,
                        "px": o.price,
                        "e": o.exponent
                    })
                })
                .collect();
            Ok(vec![json!({ "o": { "oracles": entries } })])
        }
        Action::WhitelistFaucet(action) => Ok(vec![json!({
            "whitelistFaucet": {
                "target": action.target.to_base58(),
                "whitelist": action.whitelist
            }
        })]),
        Action::CreateSubAccount(action) => {
            let mut obj = json!({ "name": action.name });
            if let Some(amount) = action.margin_amount {
                obj["marginAmount"] = json!(amount);
            }
            Ok(vec![json!({ "createSubAccount": obj })])
        }
        Action::RemoveSubAccount(action) => Ok(vec![json!({
            "removeSubAccount": {
                "toRemove": action.to_remove.to_base58()
            }
        })]),
        Action::RenameSubAccount(action) => Ok(vec![json!({
            "renameSubAccount": {
                "account": action.account.to_base58(),
                "name": action.name
            }
        })]),
        Action::Transfer(transfer) => Ok(vec![json!({
            "transfer": {
                "k": match transfer.kind {
                    TransferKind::Internal => "internal",
                    TransferKind::External => "external",
                },
                "from": transfer.from.to_base58(),
                "to": transfer.to.to_base58(),
                "marginAmount": transfer.margin_amount,
            }
        })]),
        Action::Withdraw(withdraw) => Ok(vec![json!({
            "withdraw": {
                "u": withdraw.user.to_base58(),
                "v": withdraw.vault.to_base58(),
                "rta": withdraw.recipient_token_account.to_base58(),
                "a": withdraw.amount,
                "b": withdraw.blockhash.to_base58(),
            }
        })]),
        Action::WithdrawLockRecover(recover) => Ok(vec![json!({
            "withdrawLockRecover": {
                "u": recover.user.to_base58(),
                "h": recover.hash.to_base58(),
            }
        })]),
        Action::CreateMultisig(action) => Ok(vec![json!({
            "createMultisig": {
                "signers": action.signers.iter().map(Pubkey::to_base58).collect::<Vec<_>>(),
                "threshold": action.threshold,
                "timeLockSecs": action.time_lock_secs,
                "proposalLifetimeSecs": action.proposal_lifetime_secs,
            }
        })]),
        Action::MultisigPropose(action) => {
            let mut inner_actions = Vec::new();
            for inner in &action.actions {
                inner_actions.extend(action_to_json(inner)?);
            }
            Ok(vec![json!({
                "msp": {
                    "m": action.multisig.to_base58(),
                    "a": inner_actions,
                }
            })])
        }
        Action::MultisigApprove(action) => Ok(vec![json!({
            "msa": {
                "m": action.multisig.to_base58(),
                "p": action.proposal_id,
            }
        })]),
        Action::MultisigReject(action) => Ok(vec![json!({
            "msr": {
                "m": action.multisig.to_base58(),
                "p": action.proposal_id,
            }
        })]),
        Action::MultisigCancel(action) => Ok(vec![json!({
            "msc": {
                "m": action.multisig.to_base58(),
                "p": action.proposal_id,
            }
        })]),
        Action::MultisigExecute(action) => Ok(vec![json!({
            "mse": {
                "m": action.multisig.to_base58(),
                "p": action.proposal_id,
            }
        })]),
        Action::UpdateMultisigPolicy(action) => Ok(vec![json!({
            "msu": {
                "m": action.multisig.to_base58(),
                "signers": action.signers.iter().map(Pubkey::to_base58).collect::<Vec<_>>(),
                "threshold": action.threshold,
                "timeLockSecs": action.time_lock_secs,
                "proposalLifetimeSecs": action.proposal_lifetime_secs,
            }
        })]),
    }
}

fn order_item_to_json(item: &OrderItem) -> Result<serde_json::Value> {
    match item {
        OrderItem::Order(order) => match &order.order_type {
            OrderType::Limit { tif } => {
                let tif_str = match tif {
                    TimeInForce::Gtc => "GTC",
                    TimeInForce::Ioc => "IOC",
                    TimeInForce::Alo => "ALO",
                };
                Ok(json!({
                    "l": {
                        "c": order.symbol,
                        "b": order.is_buy,
                        "px": order.price,
                        "sz": order.size,
                        "tif": tif_str,
                        "r": order.reduce_only,
                        "i": order.iso
                    }
                }))
            }
            OrderType::Trigger {
                is_market,
                trigger_px: _,
            } => {
                if !is_market {
                    return Err(Error::InvalidOrder(
                        "trigger orders are not supported by BULK API; use market".to_string(),
                    ));
                }
                Ok(json!({
                    "m": {
                        "c": order.symbol,
                        "b": order.is_buy,
                        "sz": order.size,
                        "r": order.reduce_only,
                        "i": order.iso
                    }
                }))
            }
        },
        OrderItem::Modify(modify) => Ok(json!({
            "mod": {
                "oid": modify.order_id.to_base58(),
                "c": modify.symbol,
                "sz": modify.amount
            }
        })),
        OrderItem::Cancel(cancel) => Ok(json!({
            "cx": {
                "c": cancel.symbol,
                "oid": cancel.order_id.to_base58()
            }
        })),
        OrderItem::CancelAll(cancel_all) => Ok(json!({
            "cxa": {
                "c": cancel_all.symbols
            }
        })),
        OrderItem::Stop(stop) => Ok(json!({
            "st": {
                "c": stop.symbol,
                "d": stop.is_buy,
                "sz": stop.size,
                "tr": stop.trigger_price,
                "lim": stop.limit_price,
                "i": stop.iso
            }
        })),
        OrderItem::TakeProfit(tp) => Ok(json!({
            "tp": {
                "c": tp.symbol,
                "d": tp.is_buy,
                "sz": tp.size,
                "tr": tp.trigger_price,
                "lim": tp.limit_price,
                "i": tp.iso
            }
        })),
        OrderItem::RangeOco(rng) => Ok(json!({
            "rng": {
                "c": rng.symbol,
                "d": rng.is_buy,
                "sz": rng.size,
                "pmin": rng.collar_min,
                "pmax": rng.collar_max,
                "lmin": rng.limit_min,
                "lmax": rng.limit_max,
                "i": rng.iso
            }
        })),
        OrderItem::TriggerBasket(trig) => {
            let nested: Result<Vec<_>> = trig.actions.iter().map(order_item_to_json).collect();
            Ok(json!({
                "trig": {
                    "c": trig.symbol,
                    "d": trig.is_buy,
                    "tr": trig.trigger_price,
                    "actions": nested?,
                    "i": trig.iso
                }
            }))
        }
        OrderItem::OnFill(of) => {
            let actions: Result<Vec<_>> = of.actions.iter().map(order_item_to_json).collect();
            Ok(json!({
                "of": {
                    "p": of.p,
                    "actions": actions?
                }
            }))
        }
        OrderItem::TrailingStop(trl) => Ok(json!({
            "trl": {
                "c": trl.symbol,
                "b": trl.is_buy,
                "sz": trl.size,
                "trb": trl.trail_bps,
                "stb": trl.step_bps,
                "lim": trl.limit_price,
                "i": trl.iso
            }
        })),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::Keypair;

    #[test]
    fn test_prepare_message() {
        let keypair = Keypair::generate();
        let account = keypair.pubkey();
        let order = Order::limit("BTC-USD", true, 100000.0, 0.1, TimeInForce::Gtc);
        let prepared = prepare_message(order.into(), &account, None, Some(1234567890)).unwrap();

        assert!(!prepared.message_bytes.is_empty());
        assert_eq!(prepared.nonce, 1234567890);
        assert_eq!(prepared.actions.len(), 1);
        assert!(prepared.actions[0].get("l").is_some());
    }

    #[test]
    fn test_prepare_modify_uses_compact_sdk_keys() {
        let keypair = Keypair::generate();
        let account = keypair.pubkey();
        let modify = Modify::new(Hash::random(), "BTC-USD", 0.25);
        let prepared =
            prepare_message(OrderItem::Modify(modify), &account, None, Some(1234567890)).unwrap();

        let mod_obj = prepared.actions[0].get("mod").unwrap();
        assert!(mod_obj.get("c").is_some());
        assert!(mod_obj.get("sz").is_some());
        assert!(mod_obj.get("symbol").is_none());
        assert!(mod_obj.get("amount").is_none());
    }

    #[test]
    fn test_prepare_group() {
        let keypair = Keypair::generate();
        let account = keypair.pubkey();
        let items: Vec<OrderItem> = vec![
            Order::limit("BTC-USD", true, 100000.0, 0.1, TimeInForce::Gtc).into(),
            Order::limit("BTC-USD", false, 99000.0, 0.1, TimeInForce::Gtc).into(),
        ];
        let prepared = prepare_group(items, &account, None, Some(1234567890)).unwrap();
        assert_eq!(prepared.actions.len(), 2);
        assert!(prepared.order_id.is_none());
        assert_eq!(prepared.order_ids.as_ref().map(Vec::len), Some(2));
    }

    #[test]
    fn test_prepare_all_parallel() {
        let keypair = Keypair::generate();
        let account = keypair.pubkey();
        let orders: Vec<OrderItem> = (0..20)
            .map(|i| {
                Order::limit(
                    "BTC-USD",
                    i % 2 == 0,
                    100000.0 + i as f64,
                    0.1,
                    TimeInForce::Gtc,
                )
                .into()
            })
            .collect();

        let prepared = prepare_all(orders, &account, None, Some(1000000)).unwrap();
        assert_eq!(prepared.len(), 20);
        for (i, p) in prepared.iter().enumerate() {
            assert_eq!(p.nonce, 1000000 + i as u64);
        }
    }

    #[test]
    fn test_finalize_transaction() {
        let keypair = Keypair::generate();
        let account = keypair.pubkey();
        let order = Order::limit("BTC-USD", true, 100000.0, 0.1, TimeInForce::Gtc);
        let prepared = prepare_message(order.into(), &account, None, Some(1234567890)).unwrap();
        let signed = finalize_transaction(prepared.clone(), "sig");

        assert_eq!(signed.nonce, prepared.nonce);
        assert_eq!(signed.actions, prepared.actions);
        assert_eq!(signed.signature, "sig");
        assert_eq!(signed.order_ids, prepared.order_ids);
    }

    #[test]
    fn test_prepare_rename_sub_account() {
        let keypair = Keypair::generate();
        let account = keypair.pubkey();
        let subaccount = Keypair::generate().pubkey();
        let prepared = prepare_rename_sub_account(
            RenameSubAccount::new(subaccount, "desk-2"),
            &account,
            None,
            Some(1234567890),
        )
        .unwrap();

        assert_eq!(prepared.actions.len(), 1);
        let obj = prepared.actions[0].get("renameSubAccount").unwrap();
        assert_eq!(
            obj.get("account").and_then(|v| v.as_str()),
            Some(subaccount.to_base58().as_str())
        );
        assert_eq!(obj.get("name").and_then(|v| v.as_str()), Some("desk-2"));
    }

    #[test]
    fn test_prepare_create_sub_account_with_margin_uses_current_sdk_shape() {
        let keypair = Keypair::generate();
        let account = keypair.pubkey();
        let prepared = prepare_create_sub_account(
            CreateSubAccount::with_margin("desk-1", 1000.0),
            &account,
            None,
            Some(1234567890),
        )
        .unwrap();

        let obj = prepared.actions[0].get("createSubAccount").unwrap();
        assert_eq!(obj.get("name").and_then(|v| v.as_str()), Some("desk-1"));
        assert!(obj.get("marginSymbol").is_none());
        assert_eq!(
            obj.get("marginAmount").and_then(|v| v.as_f64()),
            Some(1000.0)
        );
    }

    #[test]
    fn test_prepare_transfer_uses_current_sdk_shape() {
        let keypair = Keypair::generate();
        let account = keypair.pubkey();
        let to = Keypair::generate().pubkey();
        let prepared = prepare_transfer(
            Transfer::internal(account, to, 10.0),
            &account,
            None,
            Some(1234567890),
        )
        .unwrap();

        let obj = prepared.actions[0].get("transfer").unwrap();
        assert_eq!(obj.get("k").and_then(|v| v.as_str()), Some("internal"));
        assert!(obj.get("marginSymbol").is_none());
        assert_eq!(obj.get("marginAmount").and_then(|v| v.as_f64()), Some(10.0));
    }

    #[test]
    fn test_prepare_withdraw_matches_client_wire_shape() {
        let account = Pubkey::from_bytes([1; 32]);
        let vault = Pubkey::from_bytes([2; 32]);
        let recipient_token_account = Pubkey::from_bytes([3; 32]);
        let blockhash = Hash::from_bytes([4; 32]);
        let amount = 42u64;
        let nonce = 1234567890u64;
        let withdraw = Withdraw {
            user: account,
            vault,
            recipient_token_account,
            amount,
            blockhash,
        };
        let prepared = prepare_withdraw(withdraw, &account, None, Some(nonce)).unwrap();

        let mut expected = Vec::new();
        expected.extend_from_slice(&1u64.to_le_bytes());
        expected.extend_from_slice(&41u32.to_le_bytes());
        expected.extend_from_slice(account.as_bytes());
        expected.extend_from_slice(vault.as_bytes());
        expected.extend_from_slice(recipient_token_account.as_bytes());
        expected.extend_from_slice(&amount.to_le_bytes());
        expected.extend_from_slice(blockhash.as_bytes());
        expected.extend_from_slice(&nonce.to_le_bytes());
        expected.extend_from_slice(account.as_bytes());
        assert_eq!(prepared.message_bytes, expected);

        let obj = prepared.actions[0].get("withdraw").unwrap();
        assert_eq!(
            obj.get("u").and_then(|v| v.as_str()),
            Some(account.to_base58().as_str())
        );
        assert_eq!(
            obj.get("v").and_then(|v| v.as_str()),
            Some(vault.to_base58().as_str())
        );
        assert_eq!(obj.get("a").and_then(|v| v.as_u64()), Some(amount));
        assert!(obj.get("recipientTokenAccount").is_none());
    }

    #[test]
    fn test_prepare_withdraw_lock_recover_matches_client_wire_shape() {
        let account = Pubkey::from_bytes([1; 32]);
        let user = Pubkey::from_bytes([2; 32]);
        let hash = Hash::from_bytes([3; 32]);
        let nonce = 1234567890u64;
        let prepared = prepare_withdraw_lock_recover(
            WithdrawLockRecover { user, hash },
            &account,
            None,
            Some(nonce),
        )
        .unwrap();

        let mut expected = Vec::new();
        expected.extend_from_slice(&1u64.to_le_bytes());
        expected.extend_from_slice(&50u32.to_le_bytes());
        expected.extend_from_slice(user.as_bytes());
        expected.extend_from_slice(hash.as_bytes());
        expected.extend_from_slice(&nonce.to_le_bytes());
        expected.extend_from_slice(account.as_bytes());
        assert_eq!(prepared.message_bytes, expected);

        let obj = prepared.actions[0].get("withdrawLockRecover").unwrap();
        assert_eq!(
            obj.get("u").and_then(|v| v.as_str()),
            Some(user.to_base58().as_str())
        );
        assert_eq!(
            obj.get("h").and_then(|v| v.as_str()),
            Some(hash.to_base58().as_str())
        );
    }
}
