//! Interactive Brokers (IBKR) REST exchange client.
//!
//! Implements the Exchange trait for IBKR's Client Portal API.
//! Supports both paper and live trading via OAuth2 Bearer token auth.
//! Includes fill polling and order management.

use crate::error::HorizonError;
use crate::exchanges::Exchange;
use crate::types::{Fill, OrderRequest, OrderSide, Position, Side};
use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::{Arc, Mutex};
use tracing::{debug, error, info, warn};
use zeroize::Zeroizing;

const LIVE_API_URL: &str = "https://api.ibkr.com/v1/api";
const PAPER_API_URL: &str = "https://paper-api.ibkr.com/v1/api";

/// Validate that a URL path parameter contains only safe characters.
fn validate_url_param(param: &str, name: &str) -> Result<(), HorizonError> {
    if param.is_empty() {
        return Err(HorizonError::Exchange(format!(
            "ibkr: {} is empty",
            name
        )));
    }
    if !param
        .bytes()
        .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
    {
        return Err(HorizonError::Exchange(format!(
            "ibkr: invalid characters in {}",
            name
        )));
    }
    Ok(())
}

pub struct IBKRExchange {
    api_url: String,
    access_token: Zeroizing<String>,
    account_id: String,
    client: reqwest::Client,
    runtime: Arc<tokio::runtime::Runtime>,
    fills: Mutex<Vec<Fill>>,
    next_fill_id: Mutex<u64>,
    last_fill_cursor: Mutex<Option<String>>,
    /// Tracks open order IDs per market (conid) for per-market cancellation.
    open_orders: Mutex<HashMap<String, Vec<String>>>,
    /// Seen fill IDs for deduplication.
    seen_fill_ids: Mutex<HashSet<String>>,
    /// FIFO order for seen fill IDs (oldest first for eviction).
    seen_fill_order: Mutex<VecDeque<String>>,
    is_paper: bool,
}

impl IBKRExchange {
    pub fn new(
        access_token: String,
        account_id: String,
        api_url: Option<String>,
        is_paper: bool,
        runtime: Arc<tokio::runtime::Runtime>,
    ) -> Self {
        let default_url = if is_paper {
            PAPER_API_URL.to_string()
        } else {
            LIVE_API_URL.to_string()
        };
        Self {
            api_url: api_url.unwrap_or(default_url),
            access_token: Zeroizing::new(access_token),
            account_id,
            client: reqwest::Client::builder()
                .timeout(std::time::Duration::from_secs(10))
                .connect_timeout(std::time::Duration::from_secs(5))
                .build()
                .unwrap_or_else(|_| reqwest::Client::new()),
            runtime,
            fills: Mutex::new(Vec::new()),
            next_fill_id: Mutex::new(1),
            last_fill_cursor: Mutex::new(None),
            open_orders: Mutex::new(HashMap::new()),
            seen_fill_ids: Mutex::new(HashSet::new()),
            seen_fill_order: Mutex::new(VecDeque::new()),
            is_paper,
        }
    }

    /// Add IBKR auth headers to a request builder.
    fn auth_headers(
        &self,
        builder: reqwest::RequestBuilder,
    ) -> reqwest::RequestBuilder {
        builder.header("Authorization", format!("Bearer {}", &*self.access_token))
    }

    /// Submit order to IBKR with retry on 429.
    async fn submit_order_async(&self, req: &OrderRequest) -> Result<String, HorizonError> {
        validate_url_param(&self.account_id, "account_id")?;
        let url = format!(
            "{}/iserver/account/{}/orders",
            self.api_url, self.account_id
        );

        if req.size <= 0.0 || req.size.is_nan() || req.size.is_infinite() {
            return Err(HorizonError::Exchange(format!(
                "invalid size for IBKR: {}",
                req.size
            )));
        }
        if req.price <= 0.0 || req.price.is_nan() || req.price.is_infinite() {
            return Err(HorizonError::Exchange(format!(
                "invalid price for IBKR: {}",
                req.price
            )));
        }

        // Parse market_id (conid) as integer for the JSON body
        let conid: i64 = req.market_id.parse::<i64>().map_err(|_| {
            HorizonError::Exchange(format!(
                "ibkr: market_id must be a valid integer conid, got '{}'",
                req.market_id
            ))
        })?;

        let side_str = match req.order_side {
            OrderSide::Buy => "BUY",
            OrderSide::Sell => "SELL",
        };

        let body = serde_json::json!({
            "orders": [{
                "conid": conid,
                "side": side_str,
                "orderType": "LMT",
                "price": req.price,
                "quantity": req.size,
                "tif": "GTC",
            }]
        });

        debug!(
            conid = conid,
            side = side_str,
            price = req.price,
            size = req.size,
            "ibkr: submitting order"
        );

        let mut backoff_ms: u64 = 1000;

        for attempt in 0..5u32 {
            let builder = self.client.post(&url).json(&body);
            let resp = self
                .auth_headers(builder)
                .send()
                .await
                .map_err(|e| HorizonError::Exchange(format!("ibkr order failed: {}", e)))?;

            let status_code = resp.status().as_u16();

            if status_code == 429 && attempt < 4 {
                warn!(
                    attempt = attempt + 1,
                    backoff_ms = backoff_ms,
                    "ibkr: rate limited (429), retrying"
                );
                tokio::time::sleep(tokio::time::Duration::from_millis(backoff_ms)).await;
                backoff_ms = (backoff_ms * 2).min(30_000);
                continue;
            }

            if status_code == 401 {
                let body_text = resp.text().await.unwrap_or_default();
                return Err(HorizonError::Exchange(format!(
                    "ibkr: unauthorized (401) — check access token: {}",
                    body_text
                )));
            }

            if status_code == 403 {
                let body_text = resp.text().await.unwrap_or_default();
                return Err(HorizonError::Exchange(format!(
                    "ibkr: forbidden (403) — check account permissions: {}",
                    body_text
                )));
            }

            if !resp.status().is_success() {
                let status = resp.status();
                let body_text = resp.text().await.unwrap_or_default();
                error!(status = %status, body = %body_text, "ibkr: order rejected");
                return Err(HorizonError::Exchange(format!(
                    "ibkr order rejected ({}): {}",
                    status, body_text
                )));
            }

            let json: serde_json::Value = resp
                .json()
                .await
                .map_err(|e| HorizonError::Exchange(format!("ibkr parse error: {}", e)))?;

            // IBKR may return a confirmation prompt instead of an order ID.
            // Response looks like: [{"id": "...", "message": ["...confirm..."]}]
            // We must reply with {"confirmed": true} to finalize the order.
            let json = if let Some(first) = json.as_array().and_then(|arr| arr.first()) {
                if first.get("message").is_some() || first.get("messageIds").is_some() {
                    let reply_id = first
                        .get("id")
                        .and_then(|v| {
                            v.as_str()
                                .map(|s| s.to_string())
                                .or_else(|| v.as_i64().map(|n| n.to_string()))
                                .or_else(|| v.as_u64().map(|n| n.to_string()))
                        })
                        .unwrap_or_default();

                    if reply_id.is_empty() {
                        return Err(HorizonError::Exchange(format!(
                            "ibkr: confirmation required but no reply id: {}",
                            json
                        )));
                    }

                    debug!(reply_id = %reply_id, "ibkr: order requires confirmation, sending reply");

                    let confirm_url = format!(
                        "{}/iserver/reply/{}",
                        self.api_url, reply_id
                    );
                    let confirm_body = serde_json::json!({"confirmed": true});
                    let confirm_builder = self.client.post(&confirm_url).json(&confirm_body);
                    let confirm_resp = self
                        .auth_headers(confirm_builder)
                        .send()
                        .await
                        .map_err(|e| {
                            HorizonError::Exchange(format!("ibkr confirm failed: {}", e))
                        })?;

                    if !confirm_resp.status().is_success() {
                        let status = confirm_resp.status();
                        let body_text = confirm_resp.text().await.unwrap_or_default();
                        return Err(HorizonError::Exchange(format!(
                            "ibkr confirm rejected ({}): {}",
                            status, body_text
                        )));
                    }

                    confirm_resp
                        .json()
                        .await
                        .map_err(|e| {
                            HorizonError::Exchange(format!("ibkr confirm parse error: {}", e))
                        })?
                } else {
                    json
                }
            } else {
                json
            };

            // IBKR returns an array of order responses; extract the first order_id
            let order_id = json
                .as_array()
                .and_then(|arr| arr.first())
                .and_then(|obj| {
                    obj.get("order_id")
                        .or_else(|| obj.get("orderId"))
                        .or_else(|| obj.get("id"))
                })
                .and_then(|v| {
                    v.as_str()
                        .map(|s| s.to_string())
                        .or_else(|| v.as_i64().map(|n| n.to_string()))
                        .or_else(|| v.as_u64().map(|n| n.to_string()))
                })
                .filter(|s| !s.is_empty())
                .ok_or_else(|| {
                    HorizonError::Exchange(format!(
                        "ibkr: response missing order id: {}",
                        json
                    ))
                })?;

            info!(order_id = %order_id, "ibkr: order accepted");
            return Ok(order_id);
        }

        Err(HorizonError::Exchange(
            "ibkr: retry attempts exhausted".into(),
        ))
    }

    /// Cancel order on IBKR.
    async fn cancel_order_async(&self, order_id: &str) -> Result<(), HorizonError> {
        validate_url_param(order_id, "order_id")?;
        validate_url_param(&self.account_id, "account_id")?;
        let url = format!(
            "{}/iserver/account/{}/order/{}",
            self.api_url, self.account_id, order_id
        );

        let builder = self.client.delete(&url);
        let resp = self
            .auth_headers(builder)
            .send()
            .await
            .map_err(|e| HorizonError::Exchange(format!("ibkr cancel failed: {}", e)))?;

        if resp.status().as_u16() == 404 {
            // Order already filled or canceled
            debug!(order_id = %order_id, "ibkr: order not found (may be filled)");
            return Ok(());
        }

        if !resp.status().is_success() && resp.status().as_u16() != 204 {
            let status = resp.status();
            let body = resp.text().await.unwrap_or_default();
            return Err(HorizonError::Exchange(format!(
                "ibkr cancel failed ({}): {}",
                status, body
            )));
        }

        debug!(order_id = %order_id, "ibkr: order canceled");
        Ok(())
    }

    /// Cancel all orders on IBKR by listing open orders and canceling each.
    async fn cancel_all_async(&self) -> Result<usize, HorizonError> {
        validate_url_param(&self.account_id, "account_id")?;

        // First, list open orders
        let list_url = format!(
            "{}/iserver/account/orders",
            self.api_url
        );

        let builder = self.client.get(&list_url);
        let resp = self
            .auth_headers(builder)
            .send()
            .await
            .map_err(|e| HorizonError::Exchange(format!("ibkr list orders failed: {}", e)))?;

        if !resp.status().is_success() {
            let status = resp.status();
            let body = resp.text().await.unwrap_or_default();
            return Err(HorizonError::Exchange(format!(
                "ibkr list orders failed ({}): {}",
                status, body
            )));
        }

        let json: serde_json::Value =
            resp.json().await.unwrap_or_else(|_| serde_json::Value::Null);

        // Extract order IDs from the response
        let order_ids: Vec<String> = json
            .get("orders")
            .or_else(|| json.as_array().map(|_| &json))
            .and_then(|v| v.as_array())
            .map(|arr| {
                arr.iter()
                    .filter_map(|o| {
                        o.get("orderId")
                            .or_else(|| o.get("order_id"))
                            .and_then(|v| {
                                v.as_str()
                                    .map(|s| s.to_string())
                                    .or_else(|| v.as_i64().map(|n| n.to_string()))
                                    .or_else(|| v.as_u64().map(|n| n.to_string()))
                            })
                    })
                    .collect()
            })
            .unwrap_or_default();

        let mut canceled = 0;
        for oid in &order_ids {
            match self.cancel_order_async(oid).await {
                Ok(()) => canceled += 1,
                Err(e) => {
                    debug!(
                        order_id = %oid,
                        error = %e,
                        "ibkr: cancel_all: individual cancel failed"
                    );
                }
            }
        }

        info!(count = canceled, "ibkr: canceled all orders");
        Ok(canceled)
    }

    /// Poll for fills from IBKR.
    async fn poll_fills_async(&self) -> Result<Vec<Fill>, HorizonError> {
        let url = format!("{}/iserver/account/trades", self.api_url);

        let builder = self.client.get(&url);
        let resp = self
            .auth_headers(builder)
            .send()
            .await
            .map_err(|e| HorizonError::Exchange(format!("ibkr: fill poll failed: {}", e)))?;

        if !resp.status().is_success() {
            let status = resp.status();
            debug!(status = %status, "ibkr: fill poll returned non-200");
            return Ok(Vec::new());
        }

        let json: serde_json::Value = resp.json().await.map_err(|e| {
            HorizonError::Exchange(format!("ibkr: fill parse error: {}", e))
        })?;

        let mut new_fills = Vec::new();

        if let Some(trades_arr) = json.as_array() {
            let mut next_id = self.next_fill_id.lock().unwrap_or_else(|e| e.into_inner());

            for trade_json in trades_arr {
                let execution_id = trade_json
                    .get("execution_id")
                    .or_else(|| trade_json.get("executionId"))
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    .to_string();

                let conid = trade_json
                    .get("conid")
                    .and_then(|v| {
                        v.as_u64()
                            .map(|n| n.to_string())
                            .or_else(|| v.as_str().map(|s| s.to_string()))
                    })
                    .unwrap_or_default();

                let side_str = trade_json
                    .get("side")
                    .and_then(|v| v.as_str())
                    .unwrap_or("B");

                let order_side = if side_str.eq_ignore_ascii_case("B")
                    || side_str.eq_ignore_ascii_case("BOT")
                    || side_str.eq_ignore_ascii_case("BUY")
                {
                    OrderSide::Buy
                } else {
                    OrderSide::Sell
                };

                let price = trade_json
                    .get("price")
                    .and_then(|v| {
                        v.as_f64().or_else(|| {
                            v.as_str().and_then(|s| s.parse::<f64>().ok())
                        })
                    })
                    .unwrap_or(0.0);

                let size = trade_json
                    .get("size")
                    .or_else(|| trade_json.get("shares"))
                    .and_then(|v| {
                        v.as_f64().or_else(|| {
                            v.as_str().and_then(|s| s.parse::<f64>().ok())
                        })
                    })
                    .unwrap_or(0.0);

                let commission = trade_json
                    .get("commission")
                    .and_then(|v| {
                        v.as_f64().or_else(|| {
                            v.as_str().and_then(|s| s.parse::<f64>().ok())
                        })
                    })
                    .unwrap_or(0.0);

                let ts = trade_json
                    .get("trade_time")
                    .or_else(|| trade_json.get("tradeTime"))
                    .and_then(|v| {
                        v.as_f64().or_else(|| {
                            v.as_str().and_then(|s| s.parse::<f64>().ok())
                        })
                    })
                    .unwrap_or(0.0);

                // Validate: skip fills with invalid price/size
                if !price.is_finite() || price <= 0.0 {
                    continue;
                }
                if !size.is_finite() || size <= 0.0 {
                    continue;
                }

                let order_id = trade_json
                    .get("order_ref")
                    .or_else(|| trade_json.get("orderRef"))
                    .or_else(|| trade_json.get("orderId"))
                    .or_else(|| trade_json.get("order_id"))
                    .and_then(|v| {
                        v.as_str()
                            .map(|s| s.to_string())
                            .or_else(|| v.as_i64().map(|n| n.to_string()))
                            .or_else(|| v.as_u64().map(|n| n.to_string()))
                    })
                    .unwrap_or_default();

                let fill_id = if execution_id.is_empty() {
                    let id = format!("ibkr_fill_{}", *next_id);
                    *next_id += 1;
                    id
                } else {
                    execution_id.clone()
                };

                // Dedup by fill ID
                {
                    let mut seen = self
                        .seen_fill_ids
                        .lock()
                        .unwrap_or_else(|e| e.into_inner());
                    if seen.contains(&fill_id) {
                        continue;
                    }
                    seen.insert(fill_id.clone());
                    if let Ok(mut order) = self.seen_fill_order.lock() {
                        order.push_back(fill_id.clone());
                        while seen.len() > 10_000 {
                            if let Some(oldest) = order.pop_front() {
                                seen.remove(&oldest);
                            } else {
                                break;
                            }
                        }
                    }
                }

                // Update cursor to latest execution ID
                if let Ok(mut cursor) = self.last_fill_cursor.lock() {
                    *cursor = Some(fill_id.clone());
                }

                new_fills.push(Fill {
                    fill_id,
                    order_id,
                    market_id: conid,
                    side: Side::Yes, // Default side for non-prediction-market contracts
                    order_side,
                    price,
                    size,
                    fee: commission.abs(),
                    timestamp: ts,
                    token_id: None,
                    exchange: "ibkr".to_string(),
                    is_maker: false,
                });
            }
        }

        if !new_fills.is_empty() {
            info!(count = new_fills.len(), "ibkr: polled new fills");
        }

        Ok(new_fills)
    }

    /// Fetch positions from IBKR for reconciliation.
    async fn fetch_positions_async(&self) -> Result<Vec<Position>, HorizonError> {
        validate_url_param(&self.account_id, "account_id")?;
        let url = format!(
            "{}/portfolio/{}/positions",
            self.api_url, self.account_id
        );

        let builder = self.client.get(&url);
        let resp = self
            .auth_headers(builder)
            .send()
            .await
            .map_err(|e| {
                HorizonError::Exchange(format!("ibkr: position fetch failed: {}", e))
            })?;

        if !resp.status().is_success() {
            let status = resp.status();
            debug!(status = %status, "ibkr: position fetch returned non-200");
            return Ok(Vec::new());
        }

        let json: serde_json::Value = resp.json().await.map_err(|e| {
            HorizonError::Exchange(format!("ibkr: position parse error: {}", e))
        })?;

        let mut positions = Vec::new();

        if let Some(arr) = json.as_array() {
            for p in arr {
                let conid = p
                    .get("conid")
                    .and_then(|v| {
                        v.as_u64()
                            .map(|n| n.to_string())
                            .or_else(|| v.as_str().map(|s| s.to_string()))
                    })
                    .unwrap_or_default();

                let qty = p
                    .get("position")
                    .and_then(|v| {
                        v.as_f64().or_else(|| {
                            v.as_str().and_then(|s| s.parse::<f64>().ok())
                        })
                    })
                    .unwrap_or(0.0);

                let avg_cost = p
                    .get("avgCost")
                    .and_then(|v| {
                        v.as_f64().or_else(|| {
                            v.as_str().and_then(|s| s.parse::<f64>().ok())
                        })
                    })
                    .unwrap_or(0.0);

                let unrealized_pnl = p
                    .get("unrealizedPnl")
                    .and_then(|v| {
                        v.as_f64().or_else(|| {
                            v.as_str().and_then(|s| s.parse::<f64>().ok())
                        })
                    })
                    .unwrap_or(0.0);

                if qty.abs() > 0.0 {
                    positions.push(Position {
                        market_id: conid,
                        side: Side::Yes,
                        size: qty,
                        avg_entry_price: avg_cost,
                        realized_pnl: 0.0,
                        unrealized_pnl,
                        token_id: None,
                        exchange: "ibkr".to_string(),
                    });
                }
            }
        }

        if !positions.is_empty() {
            info!(
                count = positions.len(),
                "ibkr: fetched positions for reconciliation"
            );
        }

        Ok(positions)
    }

    /// Send a session keepalive (tickle) to IBKR.
    #[allow(dead_code)]
    async fn tickle_async(&self) -> Result<(), HorizonError> {
        let url = format!("{}/tickle", self.api_url);

        let builder = self.client.post(&url);
        let resp = self
            .auth_headers(builder)
            .send()
            .await
            .map_err(|e| HorizonError::Exchange(format!("ibkr tickle failed: {}", e)))?;

        if !resp.status().is_success() {
            let status = resp.status();
            debug!(status = %status, "ibkr: tickle returned non-200");
        }

        Ok(())
    }
}

impl Exchange for IBKRExchange {
    fn submit_order(&self, req: &OrderRequest) -> Result<String, HorizonError> {
        let req_clone = req.clone();
        let order_id = self
            .runtime
            .block_on(self.submit_order_async(&req_clone))?;

        // Track order_id per market for per-market cancellation
        if let Ok(mut map) = self.open_orders.lock() {
            let ids = map.entry(req.market_id.clone()).or_default();
            ids.push(order_id.clone());
            // Cap open_orders at 10k per market
            while ids.len() > 10_000 {
                ids.remove(0);
            }
        }

        Ok(order_id)
    }

    fn cancel_order(&self, order_id: &str) -> Result<(), HorizonError> {
        let id = order_id.to_string();
        self.runtime.block_on(self.cancel_order_async(&id))?;

        // Remove from open_orders tracking
        if let Ok(mut map) = self.open_orders.lock() {
            for ids in map.values_mut() {
                ids.retain(|oid| oid != order_id);
            }
        }

        Ok(())
    }

    fn cancel_all(&self) -> Result<usize, HorizonError> {
        let count = self.runtime.block_on(self.cancel_all_async())?;

        if let Ok(mut map) = self.open_orders.lock() {
            map.clear();
        }

        Ok(count)
    }

    fn cancel_for_market(&self, market_id: &str) -> Result<usize, HorizonError> {
        let order_ids: Vec<String> = {
            let map = self
                .open_orders
                .lock()
                .unwrap_or_else(|e| e.into_inner());
            map.get(market_id).cloned().unwrap_or_default()
        };

        if order_ids.is_empty() {
            return Ok(0);
        }

        let mut successfully_canceled = Vec::new();
        for oid in &order_ids {
            match self.runtime.block_on(self.cancel_order_async(oid)) {
                Ok(()) => successfully_canceled.push(oid.clone()),
                Err(e) => {
                    debug!(
                        order_id = %oid,
                        error = %e,
                        "ibkr: cancel_for_market: order cancel failed (may already be filled)"
                    );
                }
            }
        }

        // Only remove successfully canceled IDs from tracking
        if let Ok(mut map) = self.open_orders.lock() {
            if let Some(ids) = map.get_mut(market_id) {
                ids.retain(|oid| !successfully_canceled.contains(oid));
                if ids.is_empty() {
                    map.remove(market_id);
                }
            }
        }

        let canceled = successfully_canceled.len();
        info!(
            market_id = %market_id,
            canceled = canceled,
            total = order_ids.len(),
            "ibkr: canceled orders for market"
        );
        Ok(canceled)
    }

    fn drain_fills(&self) -> Vec<Fill> {
        // Poll for new fills from the API
        match self.runtime.block_on(self.poll_fills_async()) {
            Ok(new_fills) => {
                if !new_fills.is_empty() {
                    // Remove filled order IDs from open_orders tracking
                    if let Ok(mut map) = self.open_orders.lock() {
                        for fill in &new_fills {
                            for ids in map.values_mut() {
                                ids.retain(|oid| oid != &fill.order_id);
                            }
                        }
                        map.retain(|_, ids| !ids.is_empty());
                    }

                    if let Ok(mut fills) = self.fills.lock() {
                        fills.extend(new_fills);
                    }
                }
            }
            Err(e) => {
                warn!(error = %e, "ibkr: fill polling error");
            }
        }

        match self.fills.lock() {
            Ok(mut fills) => std::mem::take(&mut *fills),
            Err(_) => {
                error!("ibkr: fills lock poisoned");
                Vec::new()
            }
        }
    }

    fn name(&self) -> &str {
        "ibkr"
    }

    fn fetch_positions(&self) -> Result<Vec<Position>, HorizonError> {
        self.runtime.block_on(self.fetch_positions_async())
    }
}

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

    #[test]
    fn test_validate_url_param_valid() {
        assert!(validate_url_param("abc-123_def", "test").is_ok());
    }

    #[test]
    fn test_validate_url_param_alphanumeric() {
        assert!(validate_url_param("U12345678", "account_id").is_ok());
    }

    #[test]
    fn test_validate_url_param_empty() {
        assert!(validate_url_param("", "test").is_err());
    }

    #[test]
    fn test_validate_url_param_special_chars() {
        assert!(validate_url_param("abc/../etc", "test").is_err());
        assert!(validate_url_param("abc def", "test").is_err());
    }

    #[test]
    fn test_validate_url_param_path_traversal() {
        assert!(validate_url_param("../secret", "test").is_err());
        assert!(validate_url_param("id;DROP", "test").is_err());
    }

    #[test]
    fn test_validate_url_param_unicode() {
        assert!(validate_url_param("abc\u{00e9}", "test").is_err());
    }

    #[test]
    fn test_default_urls() {
        assert_eq!(LIVE_API_URL, "https://api.ibkr.com/v1/api");
        assert_eq!(PAPER_API_URL, "https://paper-api.ibkr.com/v1/api");
    }
}
