/// Generated protobuf bindings for the `arborter.v1` trading service.
#[allow(missing_docs)]
pub mod arborter_pb {
    include!("../../../../proto/generated/xyz.aspens.arborter.v1.rs");
}

// `Display` impls + the inherent CLI-formatting helpers on the proto
// types live next door in `display.rs` so this file can focus on the
// call / signing logic.
mod display;

use crate::wallet::Wallet;
use alloy::primitives::{Address, U256};
use alloy::providers::ProviderBuilder;
use alloy::signers::local::PrivateKeySigner;
use alloy_chains::NamedChain;
use arborter_pb::arborter_service_client::ArborterServiceClient;
use arborter_pb::{Order, SendOrderRequest, SendOrderResponse};
use eyre::Result;
use prost::Message;
use url::Url;

use crate::commands::config::config_pb::GetConfigResponse;
use crate::evm::rpc::MidribV3;
use crate::grpc::create_channel;

// Internal RPC dispatcher: encodes the protobuf request fields the gRPC
// server expects. The argument list intentionally mirrors the
// `SendOrderRequest` protobuf shape — bundling fields into a struct here
// would split the protobuf-mapping site across two locations.
#[allow(clippy::too_many_arguments)]
async fn call_send_order(
    url: String,
    side: i32,
    quantity: String,
    price: Option<String>,
    market_id: String,
    base_account_address: String,
    quote_account_address: String,
    wallet: &Wallet,
    authorization: Option<arborter_pb::OrderAuthorization>,
    post_only: bool,
) -> Result<SendOrderResponse> {
    // Create a channel to connect to the gRPC server (with TLS support for HTTPS)
    let channel = create_channel(&url).await?;

    // Instantiate the client
    let mut client = ArborterServiceClient::new(channel);

    // Create the order for sending with original pair decimal values.
    // `post_only=false` is the proto3 default and is wire-skipped on encode,
    // so existing callers' signed envelopes are byte-identical to pre-feature
    // builds; only true post-only orders change the envelope digest (which
    // arborter's signature check transparently honors — same encoded bytes
    // hashed on both sides).
    let order_for_sending = Order {
        side,
        quantity: quantity.clone(), // Original pair decimal values
        price: price.clone(),       // Original pair decimal values
        market_id: market_id.clone(),
        base_account_address: base_account_address.clone(),
        quote_account_address: quote_account_address.clone(),
        execution_type: 0,
        matching_order_ids: vec![],
        post_only,
    };

    // Serialize the order to a byte vector for signing
    let mut buffer = Vec::new();
    order_for_sending.encode(&mut buffer)?;

    // Sign the order. EVM signatures are 65 bytes (r||s||v); Solana Ed25519 are 64
    // bytes. Send the full curve-native length — the arborter's curve-aware
    // verifier (`onchain::verify::is_signature_valid_with_curve`) requires
    // exactly 65 for Secp256k1 and 64 for Ed25519, with no length tolerance.
    let signature_bytes = wallet.sign_message(&buffer).await?;

    // Create the request with the original order and signature
    let request = SendOrderRequest {
        order: Some(order_for_sending),
        signature_hash: signature_bytes,
        authorization,
    };

    // Create a tonic request
    let request = tonic::Request::new(request);

    // Call the send_order endpoint
    let response = client.send_order(request).await?;

    // Get the response data
    let response_data = response.into_inner();

    // Print the response from the server
    tracing::info!("Response received: {}", response_data);

    Ok(response_data)
}

/// Query deposited (available) balance for a token on a specific chain
async fn query_deposited_balance(
    rpc_url: &str,
    token_address: &str,
    contract_address: &str,
    user_address: Address,
    chain_id: u32,
) -> Result<U256> {
    let contract_addr: Address = contract_address.parse()?;
    let token_addr: Address = token_address.parse()?;
    let rpc_url = Url::parse(rpc_url)?;

    // Try to get NamedChain, fallback to a default
    let named_chain = NamedChain::try_from(chain_id as u64).unwrap_or(NamedChain::BaseSepolia);

    let provider = ProviderBuilder::new()
        .with_chain(named_chain)
        .connect_http(rpc_url);
    let contract = MidribV3::new(contract_addr, &provider);
    let result = contract
        .tradeBalance(user_address, token_addr)
        .call()
        .await?;
    Ok(result)
}

/// Format a U256 balance with decimals for display
fn format_balance_for_display(balance: U256, decimals: u32) -> String {
    let balance_u128: u128 = balance.try_into().unwrap_or(u128::MAX);
    let divisor = 10_u128.pow(decimals);
    let integer_part = balance_u128 / divisor;
    let fractional_part = balance_u128 % divisor;
    format!(
        "{}.{:0width$}",
        integer_part,
        fractional_part,
        width = decimals as usize
    )
}

/// Convert a human-readable amount (e.g., `"1.001"`) to pair decimals format
/// as a decimal string suitable for the gRPC payload.
///
/// Thin wrapper over [`crate::decimals::parse_decimal_amount`]; kept as
/// a private alias so the existing `String`-returning order-encoding path
/// stays untouched.
fn convert_to_pair_decimals(amount: &str, decimals: u32) -> Result<String> {
    Ok(crate::decimals::parse_decimal_amount(amount, decimals)?.to_string())
}

/// Look up a market from the configuration
///
/// Supports multiple formats:
/// - Shorthand: `base_network/base_symbol::quote_network/quote_symbol`
///   e.g. `flare-coston2/fXRP::flare-coston2-quote/USDT0`
/// - Full market ID: `base_network::token_address::quote_network::token_address`
/// - Market name: e.g. `Base Sepolia USDC - OP Sepolia USDC`
///
/// Returns the market info or an error listing available markets.
pub fn lookup_market<'a>(
    config: &'a GetConfigResponse,
    market_id: &str,
) -> Result<&'a crate::commands::config::config_pb::Market> {
    // Strip surrounding quotes if present (e.g. from shell env vars)
    let market_id = market_id.trim_matches('"').trim_matches('\'');

    // Try shorthand format: base_network/symbol::quote_network/symbol
    if let Some((base, quote)) = market_id.split_once("::") {
        if let (Some((base_network, base_symbol)), Some((quote_network, quote_symbol))) =
            (base.split_once('/'), quote.split_once('/'))
        {
            if let Some(market) =
                config.get_market_by_tokens(base_network, base_symbol, quote_network, quote_symbol)
            {
                return Ok(market);
            }
        }
    }

    // Try exact market_id match
    if let Some(market) = config.get_market_by_id(market_id) {
        return Ok(market);
    }

    // Try market name match
    if let Some(market) = config.get_market(market_id) {
        return Ok(market);
    }

    let available_markets = config
        .config
        .as_ref()
        .map(|c| {
            c.markets
                .iter()
                .map(|m| {
                    format!(
                        "{}/{}::{}/{}",
                        m.base_chain_network,
                        m.base_chain_token_symbol,
                        m.quote_chain_network,
                        m.quote_chain_token_symbol
                    )
                })
                .collect::<Vec<_>>()
                .join(", ")
        })
        .unwrap_or_default();
    Err(eyre::eyre!(
        "Market '{}' not found in configuration. Available markets: {}",
        market_id,
        available_markets
    ))
}

/// Resolve the origin chain network for a (market, side) pair — the chain
/// where the user signs their lock instruction.
///
/// Mirrors `gasless::resolve_order` and the per-side branches in
/// `cancel_order::call_cancel_order_with_wallet`:
/// - `Side::Bid` (BUY) locks the quote token → origin = quote chain
/// - `Side::Ask` (SELL) locks the base token → origin = base chain
///
/// Returns `Side::Unspecified` as an error since the origin is undefined.
pub fn origin_network_for_side<'a>(
    config: &'a GetConfigResponse,
    market_id: &str,
    side: arborter_pb::Side,
) -> Result<&'a str> {
    let market = lookup_market(config, market_id)?;
    Ok(match side {
        arborter_pb::Side::Bid => &market.quote_chain_network,
        arborter_pb::Side::Ask => &market.base_chain_network,
        arborter_pb::Side::Unspecified => {
            return Err(eyre::eyre!("Side::Unspecified has no origin chain"))
        }
    })
}

/// Parse a string side ("buy"/"bid" or "sell"/"ask", case-insensitive) into
/// the typed enum. Used by binaries that take side as a CLI / REPL arg;
/// useful as a single source of truth for accepted aliases.
pub fn parse_side(s: &str) -> Result<arborter_pb::Side> {
    match s.to_lowercase().as_str() {
        "buy" | "bid" => Ok(arborter_pb::Side::Bid),
        "sell" | "ask" => Ok(arborter_pb::Side::Ask),
        other => Err(eyre::eyre!(
            "invalid side '{}' (expected 'buy'/'bid' or 'sell'/'ask')",
            other
        )),
    }
}

/// Derive the account address from a private key
pub fn derive_address(privkey: &str) -> Result<(Address, String)> {
    let signer = privkey.parse::<PrivateKeySigner>()?;
    let address = signer.address();
    let checksum = address.to_checksum(None);
    Ok((address, checksum))
}

/// Send an order using a curve-agnostic wallet.
///
/// Thin wrapper over [`send_order_with_wallets`] for the common case where
/// both legs of the market live on chains that share the same curve as the
/// supplied wallet (e.g. EVM↔EVM with an EVM wallet). For cross-chain
/// markets that span Secp256k1 + Ed25519 chains, callers must use
/// `send_order_with_wallets` and supply both wallets.
///
/// # Arguments
/// * `url` - The Aspens Market Stack URL
/// * `market_id` - The market identifier
/// * `side` - Order side (1 for BUY, 2 for SELL)
/// * `quantity` - The quantity to trade (human-readable, e.g., "1.5")
/// * `price` - Optional limit price (human-readable, e.g., "100.50")
/// * `wallet` - The user's wallet (EVM or Solana)
/// * `config` - The configuration response from the server
/// * `post_only` - Reject if the order would cross at submission. Limit
///   orders only; ignored semantics-wise for market orders (arborter
///   rejects post-only without a price).
// Public top-level API — the argument list is the documented entry
// point; introducing a builder/struct here would break every caller
// (CLI, REPL, examples) for no behavioural gain.
#[allow(clippy::too_many_arguments)]
pub async fn send_order_with_wallet(
    url: String,
    market_id: String,
    side: i32,
    quantity: String,
    price: Option<String>,
    wallet: &Wallet,
    config: GetConfigResponse,
    post_only: bool,
) -> Result<SendOrderResponse> {
    send_order_with_wallets(
        url,
        market_id,
        side,
        quantity,
        price,
        &[wallet],
        config,
        post_only,
    )
    .await
}

/// Send an order with one or more wallets, picking the right curve per chain.
///
/// Cross-chain orders need separate user-addresses on the base and quote
/// chains, which may use different curves (e.g. an EVM ↔ Solana market needs
/// both an EVM hex address and a Solana base58 pubkey). Pass all available
/// wallets in `wallets`; the function selects the matching curve for each
/// leg of the market by reading chain architectures from `config`. The
/// origin chain's wallet (the side that locks tokens for this order) is
/// used to sign both the gasless lock and the outer envelope.
///
/// Errors if a wallet of the right curve is missing for either chain.
///
/// `post_only`: see [`send_order_with_wallet`].
// Public top-level API — same rationale as `send_order_with_wallet`
// for keeping the argument list flat.
#[allow(clippy::too_many_arguments)]
pub async fn send_order_with_wallets(
    url: String,
    market_id: String,
    side: i32,
    quantity: String,
    price: Option<String>,
    wallets: &[&Wallet],
    config: GetConfigResponse,
    post_only: bool,
) -> Result<SendOrderResponse> {
    if wallets.is_empty() {
        return Err(eyre::eyre!(
            "send_order_with_wallets requires at least one wallet"
        ));
    }

    // Reject post-only without a price up front — arborter will reject it
    // anyway, but failing here saves a round-trip + signature work and
    // surfaces a clearer error to scripts.
    if post_only && price.is_none() {
        return Err(eyre::eyre!(
            "post_only is incompatible with market orders (no price); \
             pass an explicit limit price or set post_only=false"
        ));
    }

    // Look up market
    let market = lookup_market(&config, &market_id)?;
    let pair_decimals = market.pair_decimals as u32;

    // Convert amounts
    let quantity_raw = convert_to_pair_decimals(&quantity, pair_decimals)
        .map_err(|e| eyre::eyre!("Invalid quantity '{}': {}", quantity, e))?;
    let price_raw = price
        .as_ref()
        .map(|p| convert_to_pair_decimals(p, pair_decimals))
        .transpose()
        .map_err(|e| eyre::eyre!("Invalid price: {}", e))?;

    // Pick the wallet whose curve matches each chain's architecture. The
    // SDK's `chain_curve` helper is the single source of truth for the
    // arch→curve mapping; using it here keeps order routing aligned with
    // deposit / balance / cancel flows.
    let base_chain = config
        .get_chain(&market.base_chain_network)
        .ok_or_else(|| eyre::eyre!("base chain '{}' not in config", market.base_chain_network))?;
    let quote_chain = config
        .get_chain(&market.quote_chain_network)
        .ok_or_else(|| eyre::eyre!("quote chain '{}' not in config", market.quote_chain_network))?;
    let base_curve = crate::wallet::chain_curve(base_chain);
    let quote_curve = crate::wallet::chain_curve(quote_chain);
    let base_wallet = wallets
        .iter()
        .copied()
        .find(|w| w.curve() == base_curve)
        .ok_or_else(|| {
            eyre::eyre!(
                "no wallet of curve {:?} available for base chain '{}'",
                base_curve,
                market.base_chain_network
            )
        })?;
    let quote_wallet = wallets
        .iter()
        .copied()
        .find(|w| w.curve() == quote_curve)
        .ok_or_else(|| {
            eyre::eyre!(
                "no wallet of curve {:?} available for quote chain '{}'",
                quote_curve,
                market.quote_chain_network
            )
        })?;

    // The signing wallet is whichever side locks for this order:
    //   Bid (BUY)  → origin = quote chain  → user locks quote
    //   Ask (SELL) → origin = base  chain  → user locks base
    let signing_wallet = if side == 1 { quote_wallet } else { base_wallet };

    let base_account_address = base_wallet.address();
    let quote_account_address = quote_wallet.address();

    tracing::info!(
        "Sending order: market={}, side={}, quantity={} (raw: {}), price={:?} (raw: {:?}), \
         base_account={}, quote_account={}, signing_curve={:?}",
        market.name,
        if side == 1 { "BUY" } else { "SELL" },
        quantity,
        quantity_raw,
        price,
        price_raw,
        base_account_address,
        quote_account_address,
        signing_wallet.curve()
    );

    // Build the order authorization for the SendOrderRequest. Under the
    // optimistic ledger the arborter authenticates via the outer envelope
    // signature and reads only `order_id` + `amount_in` from this payload —
    // there is no per-order on-chain lock signature.
    let authorization = super::gasless::build_gasless_authorization(
        &config,
        market,
        side,
        signing_wallet,
        &quantity_raw,
        price_raw.as_deref(),
    )?;

    // Send using the resolved market_id from config
    let resolved_market_id = market.market_id.clone();
    let result = call_send_order(
        url,
        side,
        quantity_raw.clone(),
        price_raw.clone(),
        resolved_market_id,
        base_account_address,
        quote_account_address,
        signing_wallet,
        Some(authorization),
        post_only,
    )
    .await;

    // Enhance balance errors with actual balance info (EVM only — Solana balance
    // queries via Alloy don't apply). Use the EVM-side wallet's address since
    // that's the one whose balance the helper actually queries on-chain.
    if let Err(ref e) = result {
        let err_str = e.to_string().to_lowercase();
        if err_str.contains("insufficient") || err_str.contains("balance") {
            if let Some(evm_wallet) = wallets
                .iter()
                .copied()
                .find(|w| w.curve() == crate::wallet::CurveType::Secp256k1)
            {
                // Re-parse the EVM address for the balance enhancement helper.
                if let Ok(user_address) = evm_wallet.address().parse::<Address>() {
                    if let Some(enhanced) = enhance_balance_error(
                        &config,
                        market,
                        side,
                        &quantity_raw,
                        price_raw.as_deref(),
                        user_address,
                        pair_decimals,
                    )
                    .await
                    {
                        return Err(enhanced);
                    }
                }
            }
        }
    }

    result
}

/// Try to provide a more helpful error message for balance issues
async fn enhance_balance_error(
    config: &GetConfigResponse,
    market: &crate::commands::config::config_pb::Market,
    side: i32,
    quantity_raw: &str,
    price_raw: Option<&str>,
    user_address: Address,
    pair_decimals: u32,
) -> Option<eyre::Report> {
    // BUY: need quote token, SELL: need base token
    let (chain_network, token_symbol, token_decimals) = if side == 1 {
        (
            &market.quote_chain_network,
            &market.quote_chain_token_symbol,
            market.quote_chain_token_decimals as u32,
        )
    } else {
        (
            &market.base_chain_network,
            &market.base_chain_token_symbol,
            market.base_chain_token_decimals as u32,
        )
    };

    let chain = config.get_chain(chain_network)?;
    let trade_contract = chain.trade_contract.as_ref()?;
    let token = chain.tokens.get(token_symbol)?;

    let deposited_balance = query_deposited_balance(
        &chain.rpc_url,
        &token.address,
        &trade_contract.address,
        user_address,
        chain.chain_id,
    )
    .await
    .ok()?;

    let deposited_formatted = format_balance_for_display(deposited_balance, token_decimals);

    let required_amount = if side == 1 {
        // BUY: need quantity * price
        if let Some(p) = price_raw {
            let qty: u128 = quantity_raw.parse().unwrap_or(0);
            let prc: u128 = p.parse().unwrap_or(0);
            let pair_dec_factor = 10_u128.pow(pair_decimals);
            Some(U256::from(qty * prc / pair_dec_factor))
        } else {
            None
        }
    } else {
        // SELL: need quantity
        let qty: u128 = quantity_raw.parse().unwrap_or(0);
        Some(U256::from(qty))
    };

    // Only enhance if we can confirm the balance is actually insufficient
    let required = required_amount?;
    if deposited_balance >= required {
        return None;
    }

    let required_str = format_balance_for_display(required, token_decimals);

    Some(eyre::eyre!(
        "Insufficient deposited balance on {}.\n\
         Token: {}\n\
         Required: {} {}\n\
         Available: {} {}\n\n\
         Deposit more {} on {} before placing this order.",
        chain_network,
        token_symbol,
        required_str,
        token_symbol,
        deposited_formatted,
        token_symbol,
        token_symbol,
        chain_network
    ))
}

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

    #[test]
    fn test_send_order_response_order_id() {
        let response = SendOrderResponse {
            order_id: 12345,
            order_in_book: true,
            order: None,
            trades: vec![],
            transaction_hashes: vec![],
            current_orderbook: vec![],
        };

        assert_eq!(response.order_id, 12345);
    }

    #[test]
    fn test_send_order_response_order_id_zero() {
        let response = SendOrderResponse {
            order_id: 0,
            order_in_book: false,
            order: None,
            trades: vec![],
            transaction_hashes: vec![],
            current_orderbook: vec![],
        };

        assert_eq!(response.order_id, 0);
    }

    #[test]
    fn test_send_order_response_order_id_max() {
        let response = SendOrderResponse {
            order_id: u64::MAX,
            order_in_book: true,
            order: None,
            trades: vec![],
            transaction_hashes: vec![],
            current_orderbook: vec![],
        };

        assert_eq!(response.order_id, u64::MAX);
    }

    #[test]
    fn test_send_order_response_display_includes_order_id() {
        let response = SendOrderResponse {
            order_id: 98765,
            order_in_book: true,
            order: None,
            trades: vec![],
            transaction_hashes: vec![],
            current_orderbook: vec![],
        };

        let display_str = format!("{}", response);
        assert!(
            display_str.contains("order_id: 98765"),
            "Display output should contain order_id: {}",
            display_str
        );
    }

    #[test]
    fn test_send_order_response_with_order_and_order_id() {
        let order = Order {
            side: 1,
            quantity: "1000".to_string(),
            price: Some("50000".to_string()),
            market_id: "test_market".to_string(),
            base_account_address: "0x1234".to_string(),
            quote_account_address: "0x5678".to_string(),
            execution_type: 0,
            matching_order_ids: vec![],
            post_only: false,
        };

        let response = SendOrderResponse {
            order_id: 42,
            order_in_book: true,
            order: Some(order),
            trades: vec![],
            transaction_hashes: vec![],
            current_orderbook: vec![],
        };

        assert_eq!(response.order_id, 42);
        assert!(response.order.is_some());
        assert!(response.order_in_book);
    }

    #[test]
    fn test_convert_to_pair_decimals_integer() {
        // 6 decimals (like USDC)
        assert_eq!(convert_to_pair_decimals("1", 6).unwrap(), "1000000");
        assert_eq!(convert_to_pair_decimals("100", 6).unwrap(), "100000000");
        assert_eq!(convert_to_pair_decimals("0", 6).unwrap(), "0");
    }

    #[test]
    fn test_convert_to_pair_decimals_with_fraction() {
        // 6 decimals
        assert_eq!(convert_to_pair_decimals("1.5", 6).unwrap(), "1500000");
        assert_eq!(convert_to_pair_decimals("1.001", 6).unwrap(), "1001000");
        assert_eq!(convert_to_pair_decimals("0.5", 6).unwrap(), "500000");
        assert_eq!(convert_to_pair_decimals("0.000001", 6).unwrap(), "1");
    }

    #[test]
    fn test_convert_to_pair_decimals_truncates_extra_precision() {
        // 6 decimals - extra precision should be truncated
        assert_eq!(convert_to_pair_decimals("1.0000001", 6).unwrap(), "1000000");
        assert_eq!(convert_to_pair_decimals("1.1234567", 6).unwrap(), "1123456");
    }

    #[test]
    fn test_convert_to_pair_decimals_18_decimals() {
        // 18 decimals (like ETH)
        assert_eq!(
            convert_to_pair_decimals("1", 18).unwrap(),
            "1000000000000000000"
        );
        assert_eq!(
            convert_to_pair_decimals("0.1", 18).unwrap(),
            "100000000000000000"
        );
    }

    #[test]
    fn test_convert_to_pair_decimals_whitespace() {
        assert_eq!(convert_to_pair_decimals("  1.5  ", 6).unwrap(), "1500000");
    }
}

#[cfg(test)]
mod post_only_proto_tests {
    //! Wire-encoding pinning tests for the `post_only` Order field.
    //!
    //! The envelope signature in `call_send_order` is computed over the
    //! prost-encoded Order proto. Two invariants must hold or every
    //! signed order silently fails arborter's verifier:
    //!
    //!   1. **`post_only=false` is wire-identical** to a pre-feature
    //!      encoding (proto3 default scalars are wire-skipped). So
    //!      existing SDK users get byte-for-byte the same envelope
    //!      digest they always did.
    //!   2. **`post_only=true` produces a different encoded payload**
    //!      from `post_only=false` — confirming the field is actually
    //!      reaching the wire. If a build-script regression dropped
    //!      the field we'd silently send `false` always with no error.
    //!
    //! These are cheap regression tests: just prost-encode and compare.

    use super::*;
    use prost::Message;

    fn sample_order(post_only: bool) -> Order {
        Order {
            side: 1,
            quantity: "1000".to_string(),
            price: Some("50000".to_string()),
            market_id: "base::0xaa::quote::0xbb".to_string(),
            base_account_address: "0xb".to_string(),
            quote_account_address: "0xq".to_string(),
            execution_type: 0,
            matching_order_ids: vec![],
            post_only,
        }
    }

    #[test]
    fn post_only_false_is_wire_skipped() {
        // `post_only: false` is the proto3 default; prost must omit it.
        // Concretely: encoding with `post_only=false` produces the same
        // bytes a pre-feature SDK build would have. We can't import an
        // old `Order` to compare against, but the encode-and-decode
        // round-trip below proves the field doesn't appear on wire when
        // false: decoding into a struct that defaults the unknown field
        // to false would round-trip cleanly.
        let order_false = sample_order(false);
        let mut buf = Vec::new();
        order_false.encode(&mut buf).unwrap();

        let decoded = Order::decode(&*buf).unwrap();
        assert!(!decoded.post_only);
        assert_eq!(decoded, order_false);
    }

    #[test]
    fn post_only_true_changes_wire_encoding() {
        // Sanity: the field is actually reaching the wire when true.
        // If we ever drop the field from the proto build script, this
        // test catches it (both encodings would be identical).
        let mut buf_false = Vec::new();
        let mut buf_true = Vec::new();
        sample_order(false).encode(&mut buf_false).unwrap();
        sample_order(true).encode(&mut buf_true).unwrap();

        assert_ne!(
            buf_false, buf_true,
            "post_only=true must produce a different encoded payload than false"
        );
        assert!(
            buf_true.len() > buf_false.len(),
            "post_only=true should add bytes to the wire encoding (tag + bool)"
        );

        // And roundtripping post_only=true preserves the field.
        let decoded = Order::decode(&*buf_true).unwrap();
        assert!(decoded.post_only);
    }
}
