//! Mock cTrader server integration tests.
//!
//! Tests the cTrader OpenAPI crate components (codec, rate limiter, config,
//! order builders, symbol resolution, proto helpers) using purely local
//! constructs -- no real broker connection required.

#![deny(clippy::unwrap_used, clippy::expect_used)]

use bytes::{BufMut, BytesMut};
use prost::Message;
use tokio_util::codec::{Decoder, Encoder};

use ctrader_openapi::codec::CTraderCodec;
use ctrader_openapi::config::{CTraderConfig, CTraderEnvironment};
use ctrader_openapi::error::RateLimitBucket;
use ctrader_openapi::proto::{self, ProtoMessage};
use ctrader_openapi::rate_limiter::RateLimiter;
use ctrader_openapi::symbols::{lots_to_volume, volume_to_lots};

// ══════════════════════════════════════════════════════════════════════
//  1. Codec roundtrip tests (extended)
// ══════════════════════════════════════════════════════════════════════

/// Helper: encode a ProtoMessage into a BytesMut, returning an error string on failure.
fn encode_msg(msg: ProtoMessage) -> std::result::Result<BytesMut, String> {
    let mut codec = CTraderCodec::new();
    let mut buf = BytesMut::new();
    codec.encode(msg, &mut buf).map_err(|e| e.to_string())?;
    Ok(buf)
}

/// Helper: decode a ProtoMessage from a BytesMut, returning an error string on failure.
fn decode_msg(buf: &mut BytesMut) -> std::result::Result<Option<ProtoMessage>, String> {
    let mut codec = CTraderCodec::new();
    codec.decode(buf).map_err(|e| e.to_string())
}

#[test]
fn codec_roundtrip_application_auth_req() {
    let inner = proto::ProtoOaApplicationAuthReq {
        payload_type: Some(proto::PT_APP_AUTH_REQ as i32),
        client_id: "test-client-id".into(),
        client_secret: "test-client-secret".into(),
    };

    let original = ProtoMessage {
        payload_type: proto::PT_APP_AUTH_REQ,
        payload: Some(inner.encode_to_vec()),
        client_msg_id: Some("msg-001".into()),
    };

    let mut buf = encode_msg(original.clone()).ok().unwrap_or_default();
    let decoded = decode_msg(&mut buf)
        .ok()
        .flatten()
        .unwrap_or_else(|| ProtoMessage {
            payload_type: 0,
            payload: None,
            client_msg_id: None,
        });

    assert_eq!(decoded.payload_type, original.payload_type);
    assert_eq!(decoded.payload, original.payload);
    assert_eq!(decoded.client_msg_id, original.client_msg_id);

    // Verify the inner payload decodes correctly
    let inner_decoded = proto::ProtoOaApplicationAuthReq::decode(
        decoded.payload.as_deref().unwrap_or_default(),
    );
    assert!(inner_decoded.is_ok());
    let inner_decoded = inner_decoded.unwrap_or_else(|_| proto::ProtoOaApplicationAuthReq {
        payload_type: None,
        client_id: String::new(),
        client_secret: String::new(),
    });
    assert_eq!(inner_decoded.client_id, "test-client-id");
    assert_eq!(inner_decoded.client_secret, "test-client-secret");
}

#[test]
fn codec_roundtrip_account_auth_req() {
    let inner = proto::ProtoOaAccountAuthReq {
        payload_type: Some(proto::PT_ACCOUNT_AUTH_REQ as i32),
        ctid_trader_account_id: 999_888,
        access_token: "abc-token-xyz".into(),
    };

    let original = ProtoMessage {
        payload_type: proto::PT_ACCOUNT_AUTH_REQ,
        payload: Some(inner.encode_to_vec()),
        client_msg_id: Some("msg-002".into()),
    };

    let mut buf = encode_msg(original.clone()).ok().unwrap_or_default();
    let decoded = decode_msg(&mut buf)
        .ok()
        .flatten()
        .unwrap_or_else(|| ProtoMessage {
            payload_type: 0,
            payload: None,
            client_msg_id: None,
        });

    assert_eq!(decoded.payload_type, proto::PT_ACCOUNT_AUTH_REQ);
    assert_eq!(decoded.client_msg_id.as_deref(), Some("msg-002"));

    let inner_decoded =
        proto::ProtoOaAccountAuthReq::decode(decoded.payload.as_deref().unwrap_or_default());
    assert!(inner_decoded.is_ok());
}

#[test]
fn codec_roundtrip_new_order_req() {
    let inner = proto::ProtoOaNewOrderReq {
        payload_type: Some(proto::PT_NEW_ORDER_REQ as i32),
        ctid_trader_account_id: 12345,
        symbol_id: 1,
        order_type: proto::ProtoOaOrderType::Market as i32,
        trade_side: proto::ProtoOaTradeSide::Buy as i32,
        volume: 100_000,
        limit_price: None,
        stop_price: None,
        time_in_force: None,
        expiration_timestamp: None,
        stop_loss: Some(1.08000),
        take_profit: Some(1.12000),
        comment: Some("integration test order".into()),
        base_slippage_price: None,
        slippage_in_points: None,
        label: Some("test-label".into()),
        position_id: None,
        client_order_id: Some("clord-123".into()),
        relative_stop_loss: None,
        relative_take_profit: None,
        guaranteed_stop_loss: Some(false),
        trailing_stop_loss: Some(false),
        stop_trigger_method: None,
    };

    let original = ProtoMessage {
        payload_type: proto::PT_NEW_ORDER_REQ,
        payload: Some(inner.encode_to_vec()),
        client_msg_id: Some("order-msg-001".into()),
    };

    let mut buf = encode_msg(original.clone()).ok().unwrap_or_default();
    let decoded = decode_msg(&mut buf)
        .ok()
        .flatten()
        .unwrap_or_else(|| ProtoMessage {
            payload_type: 0,
            payload: None,
            client_msg_id: None,
        });

    assert_eq!(decoded.payload_type, proto::PT_NEW_ORDER_REQ);
    assert_eq!(decoded.payload, original.payload);

    let inner_decoded =
        proto::ProtoOaNewOrderReq::decode(decoded.payload.as_deref().unwrap_or_default());
    assert!(inner_decoded.is_ok());
    let inner_decoded = inner_decoded.unwrap_or_else(|_| proto::ProtoOaNewOrderReq::default());
    assert_eq!(inner_decoded.volume, 100_000);
    assert_eq!(
        inner_decoded.comment.as_deref(),
        Some("integration test order")
    );
}

#[test]
fn codec_roundtrip_empty_payload() {
    // Heartbeat has no payload and no client_msg_id
    let original = ProtoMessage {
        payload_type: proto::PT_HEARTBEAT_EVENT,
        payload: None,
        client_msg_id: None,
    };

    let mut buf = encode_msg(original.clone()).ok().unwrap_or_default();
    let decoded = decode_msg(&mut buf)
        .ok()
        .flatten()
        .unwrap_or_else(|| ProtoMessage {
            payload_type: 0,
            payload: None,
            client_msg_id: None,
        });

    assert_eq!(decoded.payload_type, proto::PT_HEARTBEAT_EVENT);
    assert!(decoded.payload.is_none());
    assert!(decoded.client_msg_id.is_none());
}

#[test]
fn codec_roundtrip_large_payload() {
    // Create a payload with 100KB of data
    let large_payload = vec![0xABu8; 100 * 1024];
    let original = ProtoMessage {
        payload_type: 9999,
        payload: Some(large_payload.clone()),
        client_msg_id: Some("large-payload-test".into()),
    };

    let mut buf = encode_msg(original.clone()).ok().unwrap_or_default();
    let decoded = decode_msg(&mut buf)
        .ok()
        .flatten()
        .unwrap_or_else(|| ProtoMessage {
            payload_type: 0,
            payload: None,
            client_msg_id: None,
        });

    assert_eq!(decoded.payload_type, 9999);
    assert_eq!(
        decoded.payload.as_ref().map(|p| p.len()),
        Some(100 * 1024)
    );
    assert_eq!(decoded.payload, original.payload);
}

#[test]
fn codec_rejects_oversized_frame_on_decode() {
    let mut codec = CTraderCodec::new();
    let mut buf = BytesMut::new();
    // Write a length that exceeds the 16 MiB MAX_FRAME_SIZE
    buf.put_u32(16 * 1024 * 1024 + 1);
    buf.extend_from_slice(&[0u8; 64]);

    let result = codec.decode(&mut buf);
    assert!(result.is_err());
}

#[test]
fn codec_partial_frame_returns_none_then_completes() {
    let mut codec = CTraderCodec::new();
    let original = ProtoMessage {
        payload_type: proto::PT_APP_AUTH_RES,
        payload: None,
        client_msg_id: Some("partial-test".into()),
    };

    let mut full_buf = BytesMut::new();
    let encode_result = codec.encode(original.clone(), &mut full_buf);
    assert!(encode_result.is_ok());

    // Feed only 2 bytes -- not enough for length prefix
    let mut partial = full_buf.split_to(2);
    let remainder = full_buf;
    let decode_result = codec.decode(&mut partial);
    assert!(decode_result.is_ok());
    assert!(
        decode_result.ok().flatten().is_none(),
        "should return None for partial length prefix"
    );

    // Now combine the partial data with the remainder
    let mut combined = BytesMut::new();
    combined.extend_from_slice(&partial);
    combined.extend_from_slice(&remainder);

    let decode_result = codec.decode(&mut combined);
    assert!(decode_result.is_ok());
    let decoded = decode_result.ok().flatten();
    assert!(decoded.is_some(), "should decode complete frame");
    let decoded = decoded.unwrap_or_else(|| ProtoMessage {
        payload_type: 0,
        payload: None,
        client_msg_id: None,
    });
    assert_eq!(decoded.payload_type, original.payload_type);
    assert_eq!(decoded.client_msg_id, original.client_msg_id);
}

#[test]
fn codec_multiple_messages_in_single_buffer() {
    let mut codec = CTraderCodec::new();
    let msg1 = ProtoMessage {
        payload_type: proto::PT_HEARTBEAT_EVENT,
        payload: None,
        client_msg_id: None,
    };
    let msg2 = ProtoMessage {
        payload_type: proto::PT_NEW_ORDER_REQ,
        payload: Some(vec![10, 20, 30]),
        client_msg_id: Some("multi-1".into()),
    };
    let msg3 = ProtoMessage {
        payload_type: proto::PT_CANCEL_ORDER_REQ,
        payload: Some(vec![40, 50]),
        client_msg_id: Some("multi-2".into()),
    };

    let mut buf = BytesMut::new();
    assert!(codec.encode(msg1.clone(), &mut buf).is_ok());
    assert!(codec.encode(msg2.clone(), &mut buf).is_ok());
    assert!(codec.encode(msg3.clone(), &mut buf).is_ok());

    let d1 = codec.decode(&mut buf).ok().flatten();
    assert!(d1.is_some());
    assert_eq!(
        d1.as_ref().map(|m| m.payload_type),
        Some(proto::PT_HEARTBEAT_EVENT)
    );

    let d2 = codec.decode(&mut buf).ok().flatten();
    assert!(d2.is_some());
    assert_eq!(
        d2.as_ref().map(|m| m.payload_type),
        Some(proto::PT_NEW_ORDER_REQ)
    );

    let d3 = codec.decode(&mut buf).ok().flatten();
    assert!(d3.is_some());
    assert_eq!(
        d3.as_ref().map(|m| m.payload_type),
        Some(proto::PT_CANCEL_ORDER_REQ)
    );

    // Buffer should be empty now
    let d4 = codec.decode(&mut buf).ok().flatten();
    assert!(d4.is_none());
}

// ══════════════════════════════════════════════════════════════════════
//  2. Rate limiter tests (extended)
// ══════════════════════════════════════════════════════════════════════

#[tokio::test]
async fn rate_limiter_non_historical_burst_capacity() {
    let limiter = RateLimiter::new();
    // Should be able to acquire 50 tokens (full burst) without blocking
    for i in 0..50 {
        let result = limiter.acquire(RateLimitBucket::NonHistorical).await;
        assert!(result.is_ok(), "failed at iteration {i}");
    }
}

#[tokio::test]
async fn rate_limiter_historical_burst_capacity() {
    let limiter = RateLimiter::new();
    // Should be able to acquire 5 tokens (full burst) without blocking
    for i in 0..5 {
        let result = limiter.acquire(RateLimitBucket::Historical).await;
        assert!(result.is_ok(), "failed at iteration {i}");
    }
}

#[tokio::test]
async fn rate_limiter_bucket_independence() {
    let limiter = RateLimiter::new();

    // Exhaust non-historical bucket
    for _ in 0..50 {
        let result = limiter.acquire(RateLimitBucket::NonHistorical).await;
        assert!(result.is_ok());
    }

    // Historical bucket should still have tokens
    for _ in 0..5 {
        let result = limiter.acquire(RateLimitBucket::Historical).await;
        assert!(result.is_ok());
    }
}

#[tokio::test]
async fn rate_limiter_bucket_independence_reverse() {
    let limiter = RateLimiter::new();

    // Exhaust historical bucket first
    for _ in 0..5 {
        let result = limiter.acquire(RateLimitBucket::Historical).await;
        assert!(result.is_ok());
    }

    // Non-historical bucket should still have all 50 tokens
    for _ in 0..50 {
        let result = limiter.acquire(RateLimitBucket::NonHistorical).await;
        assert!(result.is_ok());
    }
}

#[tokio::test]
async fn rate_limiter_concurrent_access() {
    use std::sync::Arc;

    let limiter = Arc::new(RateLimiter::new());
    let mut handles = Vec::new();

    // Spawn 10 tasks that each try to acquire 5 tokens concurrently
    for _ in 0..10 {
        let limiter = Arc::clone(&limiter);
        handles.push(tokio::spawn(async move {
            for _ in 0..5 {
                let result = limiter.acquire(RateLimitBucket::NonHistorical).await;
                assert!(result.is_ok());
            }
        }));
    }

    for handle in handles {
        let _ = handle.await;
    }
    // If we get here without deadlock or panic, concurrency is safe
}

#[tokio::test]
async fn rate_limiter_refill_after_sleep() {
    let limiter = RateLimiter::new();

    // Exhaust the historical bucket (capacity 5)
    for _ in 0..5 {
        let result = limiter.acquire(RateLimitBucket::Historical).await;
        assert!(result.is_ok());
    }

    // Sleep long enough for at least 1 token to refill (5 tokens/s => ~200ms per token)
    tokio::time::sleep(std::time::Duration::from_millis(250)).await;

    // Should succeed after refill
    let result = limiter.acquire(RateLimitBucket::Historical).await;
    assert!(result.is_ok());
}

#[tokio::test]
async fn rate_limiter_clone_shares_state() {
    let limiter = RateLimiter::new();
    let limiter2 = limiter.clone();

    // Exhaust via the first handle
    for _ in 0..50 {
        let result = limiter.acquire(RateLimitBucket::NonHistorical).await;
        assert!(result.is_ok());
    }

    // Sleep to let 1 token refill
    tokio::time::sleep(std::time::Duration::from_millis(50)).await;

    // The clone should see the same exhausted state, but after refill
    // at 50 tokens/s, 50ms = ~2.5 tokens refilled => at least 1 available
    let result = limiter2.acquire(RateLimitBucket::NonHistorical).await;
    assert!(result.is_ok());
}

// ══════════════════════════════════════════════════════════════════════
//  3. Config validation tests
// ══════════════════════════════════════════════════════════════════════

#[test]
fn config_demo_environment_host_port() {
    let env = CTraderEnvironment::Demo;
    assert_eq!(env.host(), "demo.ctraderapi.com");
    assert_eq!(env.port(), 5035);
}

#[test]
fn config_live_environment_host_port() {
    let env = CTraderEnvironment::Live;
    assert_eq!(env.host(), "live.ctraderapi.com");
    assert_eq!(env.port(), 5035);
}

#[test]
fn config_default_environment_is_demo() {
    let env = CTraderEnvironment::default();
    assert_eq!(env, CTraderEnvironment::Demo);
}

#[test]
fn config_default_values_via_serde() {
    let json = r#"{
        "client_id": "test-id",
        "client_secret": "test-secret",
        "access_token": "test-token",
        "account_id": 12345
    }"#;

    let config: std::result::Result<CTraderConfig, _> = serde_json::from_str(json);
    assert!(config.is_ok());
    let config = config.unwrap_or_else(|_| CTraderConfig {
        client_id: String::new(),
        client_secret: String::new(),
        access_token: String::new(),
        account_id: 0,
        environment: CTraderEnvironment::Demo,
        heartbeat_interval_secs: 0,
        request_timeout_ms: 0,
        max_reconnect_attempts: 0,
    });

    assert_eq!(config.environment, CTraderEnvironment::Demo);
    assert_eq!(config.heartbeat_interval_secs, 10);
    assert_eq!(config.request_timeout_ms, 5000);
    assert_eq!(config.max_reconnect_attempts, 5);
}

#[test]
fn config_custom_values_via_serde() {
    let json = r#"{
        "client_id": "my-app",
        "client_secret": "secret-123",
        "access_token": "tok-abc",
        "account_id": 99999,
        "environment": "live",
        "heartbeat_interval_secs": 5,
        "request_timeout_ms": 3000,
        "max_reconnect_attempts": 10
    }"#;

    let config: std::result::Result<CTraderConfig, _> = serde_json::from_str(json);
    assert!(config.is_ok());
    let config = config.unwrap_or_else(|_| CTraderConfig {
        client_id: String::new(),
        client_secret: String::new(),
        access_token: String::new(),
        account_id: 0,
        environment: CTraderEnvironment::Demo,
        heartbeat_interval_secs: 0,
        request_timeout_ms: 0,
        max_reconnect_attempts: 0,
    });

    assert_eq!(config.client_id, "my-app");
    assert_eq!(config.client_secret, "secret-123");
    assert_eq!(config.access_token, "tok-abc");
    assert_eq!(config.account_id, 99999);
    assert_eq!(config.environment, CTraderEnvironment::Live);
    assert_eq!(config.heartbeat_interval_secs, 5);
    assert_eq!(config.request_timeout_ms, 3000);
    assert_eq!(config.max_reconnect_attempts, 10);
}

#[test]
fn config_serialization_roundtrip() {
    let original = CTraderConfig {
        client_id: "roundtrip-id".into(),
        client_secret: "roundtrip-secret".into(),
        access_token: "roundtrip-token".into(),
        account_id: 42,
        environment: CTraderEnvironment::Live,
        heartbeat_interval_secs: 7,
        request_timeout_ms: 2000,
        max_reconnect_attempts: 3,
    };

    let json = serde_json::to_string(&original);
    assert!(json.is_ok());
    let json = json.unwrap_or_default();

    let restored: std::result::Result<CTraderConfig, _> = serde_json::from_str(&json);
    assert!(restored.is_ok());
    let restored = restored.unwrap_or_else(|_| CTraderConfig {
        client_id: String::new(),
        client_secret: String::new(),
        access_token: String::new(),
        account_id: 0,
        environment: CTraderEnvironment::Demo,
        heartbeat_interval_secs: 0,
        request_timeout_ms: 0,
        max_reconnect_attempts: 0,
    });

    assert_eq!(restored.client_id, original.client_id);
    assert_eq!(restored.account_id, original.account_id);
    assert_eq!(restored.environment, original.environment);
    assert_eq!(
        restored.heartbeat_interval_secs,
        original.heartbeat_interval_secs
    );
    assert_eq!(restored.request_timeout_ms, original.request_timeout_ms);
    assert_eq!(
        restored.max_reconnect_attempts,
        original.max_reconnect_attempts
    );
}

#[test]
fn config_both_environments_use_same_port() {
    assert_eq!(CTraderEnvironment::Demo.port(), CTraderEnvironment::Live.port());
}

#[test]
fn config_environments_have_different_hosts() {
    assert_ne!(
        CTraderEnvironment::Demo.host(),
        CTraderEnvironment::Live.host()
    );
}

// ══════════════════════════════════════════════════════════════════════
//  4. Order builder tests (extended)
// ══════════════════════════════════════════════════════════════════════

#[test]
fn order_builder_market_buy() {
    let msg = ctrader_openapi::orders::new_order(
        12345,
        1,
        proto::ProtoOaTradeSide::Buy,
        100_000,
        proto::ProtoOaOrderType::Market,
        None,
        None,
        None,
        None,
        Some("market buy".into()),
    );

    assert_eq!(msg.payload_type, proto::PT_NEW_ORDER_REQ);
    assert!(msg.payload.is_some());

    let decoded =
        proto::ProtoOaNewOrderReq::decode(msg.payload.as_deref().unwrap_or_default());
    assert!(decoded.is_ok());
    let decoded = decoded.unwrap_or_default();
    assert_eq!(decoded.ctid_trader_account_id, 12345);
    assert_eq!(decoded.symbol_id, 1);
    assert_eq!(decoded.trade_side, proto::ProtoOaTradeSide::Buy as i32);
    assert_eq!(decoded.volume, 100_000);
    assert_eq!(
        decoded.order_type,
        proto::ProtoOaOrderType::Market as i32
    );
    assert_eq!(decoded.comment.as_deref(), Some("market buy"));
    assert!(decoded.limit_price.is_none());
    assert!(decoded.stop_price.is_none());
}

#[test]
fn order_builder_market_sell() {
    let msg = ctrader_openapi::orders::new_order(
        12345,
        2,
        proto::ProtoOaTradeSide::Sell,
        50_000,
        proto::ProtoOaOrderType::Market,
        None,
        None,
        None,
        None,
        None,
    );

    assert_eq!(msg.payload_type, proto::PT_NEW_ORDER_REQ);
    let decoded =
        proto::ProtoOaNewOrderReq::decode(msg.payload.as_deref().unwrap_or_default());
    assert!(decoded.is_ok());
    let decoded = decoded.unwrap_or_default();
    assert_eq!(decoded.trade_side, proto::ProtoOaTradeSide::Sell as i32);
    assert_eq!(decoded.volume, 50_000);
    assert!(decoded.comment.is_none());
}

#[test]
fn order_builder_limit_order() {
    let msg = ctrader_openapi::orders::new_order(
        12345,
        1,
        proto::ProtoOaTradeSide::Buy,
        200_000,
        proto::ProtoOaOrderType::Limit,
        Some(1.10000),
        None,
        Some(1.08000),
        Some(1.15000),
        Some("limit order test".into()),
    );

    assert_eq!(msg.payload_type, proto::PT_NEW_ORDER_REQ);
    let decoded =
        proto::ProtoOaNewOrderReq::decode(msg.payload.as_deref().unwrap_or_default());
    assert!(decoded.is_ok());
    let decoded = decoded.unwrap_or_default();
    assert_eq!(
        decoded.order_type,
        proto::ProtoOaOrderType::Limit as i32
    );
    assert!((decoded.limit_price.unwrap_or(0.0) - 1.10000).abs() < 1e-10);
    assert!(decoded.stop_price.is_none());
    assert!((decoded.stop_loss.unwrap_or(0.0) - 1.08000).abs() < 1e-10);
    assert!((decoded.take_profit.unwrap_or(0.0) - 1.15000).abs() < 1e-10);
}

#[test]
fn order_builder_stop_order() {
    let msg = ctrader_openapi::orders::new_order(
        12345,
        3,
        proto::ProtoOaTradeSide::Sell,
        150_000,
        proto::ProtoOaOrderType::Stop,
        None,
        Some(1.09000),
        Some(1.11000),
        Some(1.05000),
        None,
    );

    assert_eq!(msg.payload_type, proto::PT_NEW_ORDER_REQ);
    let decoded =
        proto::ProtoOaNewOrderReq::decode(msg.payload.as_deref().unwrap_or_default());
    assert!(decoded.is_ok());
    let decoded = decoded.unwrap_or_default();
    assert_eq!(
        decoded.order_type,
        proto::ProtoOaOrderType::Stop as i32
    );
    assert!((decoded.stop_price.unwrap_or(0.0) - 1.09000).abs() < 1e-10);
    assert!((decoded.stop_loss.unwrap_or(0.0) - 1.11000).abs() < 1e-10);
    assert!((decoded.take_profit.unwrap_or(0.0) - 1.05000).abs() < 1e-10);
    assert!(decoded.limit_price.is_none());
}

#[test]
fn order_builder_cancel_order() {
    let msg = ctrader_openapi::orders::cancel_order(12345, 999);
    assert_eq!(msg.payload_type, proto::PT_CANCEL_ORDER_REQ);

    let decoded =
        proto::ProtoOaCancelOrderReq::decode(msg.payload.as_deref().unwrap_or_default());
    assert!(decoded.is_ok());
    let decoded = decoded.unwrap_or_default();
    assert_eq!(decoded.ctid_trader_account_id, 12345);
    assert_eq!(decoded.order_id, 999);
}

#[test]
fn order_builder_amend_order() {
    let msg = ctrader_openapi::orders::amend_order(
        12345,
        888,
        Some(300_000),
        Some(1.12000),
        None,
        Some(1.09000),
        Some(1.16000),
    );
    assert_eq!(msg.payload_type, proto::PT_AMEND_ORDER_REQ);

    let decoded =
        proto::ProtoOaAmendOrderReq::decode(msg.payload.as_deref().unwrap_or_default());
    assert!(decoded.is_ok());
    let decoded = decoded.unwrap_or_default();
    assert_eq!(decoded.order_id, 888);
    assert_eq!(decoded.volume, Some(300_000));
    assert!((decoded.limit_price.unwrap_or(0.0) - 1.12000).abs() < 1e-10);
    assert!(decoded.stop_price.is_none());
    assert!((decoded.stop_loss.unwrap_or(0.0) - 1.09000).abs() < 1e-10);
    assert!((decoded.take_profit.unwrap_or(0.0) - 1.16000).abs() < 1e-10);
}

#[test]
fn order_builder_close_position() {
    let msg = ctrader_openapi::orders::close_position(12345, 777, 50_000);
    assert_eq!(msg.payload_type, proto::PT_CLOSE_POSITION_REQ);

    let decoded =
        proto::ProtoOaClosePositionReq::decode(msg.payload.as_deref().unwrap_or_default());
    assert!(decoded.is_ok());
    let decoded = decoded.unwrap_or_default();
    assert_eq!(decoded.ctid_trader_account_id, 12345);
    assert_eq!(decoded.position_id, 777);
    assert_eq!(decoded.volume, 50_000);
}

#[test]
fn order_builder_zero_volume() {
    let msg = ctrader_openapi::orders::new_order(
        12345,
        1,
        proto::ProtoOaTradeSide::Buy,
        0,
        proto::ProtoOaOrderType::Market,
        None,
        None,
        None,
        None,
        None,
    );

    let decoded =
        proto::ProtoOaNewOrderReq::decode(msg.payload.as_deref().unwrap_or_default());
    assert!(decoded.is_ok());
    assert_eq!(decoded.unwrap_or_default().volume, 0);
}

#[test]
fn order_builder_very_large_volume() {
    let large_volume: i64 = 10_000_000_000; // 100,000 lots
    let msg = ctrader_openapi::orders::new_order(
        12345,
        1,
        proto::ProtoOaTradeSide::Buy,
        large_volume,
        proto::ProtoOaOrderType::Market,
        None,
        None,
        None,
        None,
        None,
    );

    let decoded =
        proto::ProtoOaNewOrderReq::decode(msg.payload.as_deref().unwrap_or_default());
    assert!(decoded.is_ok());
    assert_eq!(decoded.unwrap_or_default().volume, large_volume);
}

#[test]
fn order_builder_very_large_price() {
    let msg = ctrader_openapi::orders::new_order(
        12345,
        1,
        proto::ProtoOaTradeSide::Buy,
        100_000,
        proto::ProtoOaOrderType::Limit,
        Some(999_999.99),
        None,
        None,
        None,
        None,
    );

    let decoded =
        proto::ProtoOaNewOrderReq::decode(msg.payload.as_deref().unwrap_or_default());
    assert!(decoded.is_ok());
    let decoded = decoded.unwrap_or_default();
    assert!((decoded.limit_price.unwrap_or(0.0) - 999_999.99).abs() < 1e-6);
}

#[test]
fn order_builder_negative_account_id() {
    // Negative account IDs should be handled gracefully (proto uses i64)
    let msg = ctrader_openapi::orders::new_order(
        -1,
        1,
        proto::ProtoOaTradeSide::Buy,
        100_000,
        proto::ProtoOaOrderType::Market,
        None,
        None,
        None,
        None,
        None,
    );

    let decoded =
        proto::ProtoOaNewOrderReq::decode(msg.payload.as_deref().unwrap_or_default());
    assert!(decoded.is_ok());
    assert_eq!(decoded.unwrap_or_default().ctid_trader_account_id, -1);
}

// ══════════════════════════════════════════════════════════════════════
//  5. Symbol resolution / volume conversion tests
// ══════════════════════════════════════════════════════════════════════

#[test]
fn lots_to_volume_standard_lot() {
    assert_eq!(lots_to_volume(1.0), 100_000);
}

#[test]
fn lots_to_volume_mini_lot() {
    assert_eq!(lots_to_volume(0.1), 10_000);
}

#[test]
fn lots_to_volume_micro_lot() {
    assert_eq!(lots_to_volume(0.01), 1_000);
}

#[test]
fn lots_to_volume_zero() {
    assert_eq!(lots_to_volume(0.0), 0);
}

#[test]
fn lots_to_volume_fractional() {
    assert_eq!(lots_to_volume(1.5), 150_000);
}

#[test]
fn lots_to_volume_large() {
    assert_eq!(lots_to_volume(100.0), 10_000_000);
}

#[test]
fn volume_to_lots_roundtrip() {
    let lots = 2.35;
    let volume = lots_to_volume(lots);
    let back = volume_to_lots(volume);
    assert!((back - lots).abs() < 1e-10);
}

#[test]
fn volume_to_lots_zero() {
    assert!((volume_to_lots(0) - 0.0).abs() < 1e-10);
}

#[test]
fn volume_to_lots_micro() {
    let lots = volume_to_lots(1_000);
    assert!((lots - 0.01).abs() < 1e-10);
}

// ══════════════════════════════════════════════════════════════════════
//  6. Proto helper tests
// ══════════════════════════════════════════════════════════════════════

#[test]
fn payload_type_name_known_types() {
    assert_eq!(proto::payload_type_name(proto::PT_HEARTBEAT_EVENT), "HeartbeatEvent");
    assert_eq!(proto::payload_type_name(proto::PT_ERROR_RES), "ErrorRes");
    assert_eq!(proto::payload_type_name(proto::PT_APP_AUTH_REQ), "ApplicationAuthReq");
    assert_eq!(proto::payload_type_name(proto::PT_APP_AUTH_RES), "ApplicationAuthRes");
    assert_eq!(proto::payload_type_name(proto::PT_ACCOUNT_AUTH_REQ), "AccountAuthReq");
    assert_eq!(proto::payload_type_name(proto::PT_ACCOUNT_AUTH_RES), "AccountAuthRes");
    assert_eq!(proto::payload_type_name(proto::PT_NEW_ORDER_REQ), "NewOrderReq");
    assert_eq!(proto::payload_type_name(proto::PT_CANCEL_ORDER_REQ), "CancelOrderReq");
    assert_eq!(proto::payload_type_name(proto::PT_EXECUTION_EVENT), "ExecutionEvent");
    assert_eq!(proto::payload_type_name(proto::PT_SPOT_EVENT), "SpotEvent");
    assert_eq!(proto::payload_type_name(proto::PT_SYMBOLS_LIST_REQ), "SymbolsListReq");
    assert_eq!(proto::payload_type_name(proto::PT_SYMBOLS_LIST_RES), "SymbolsListRes");
    assert_eq!(proto::payload_type_name(proto::PT_TRADER_REQ), "TraderReq");
    assert_eq!(proto::payload_type_name(proto::PT_TRADER_RES), "TraderRes");
    assert_eq!(proto::payload_type_name(proto::PT_RECONCILE_REQ), "ReconcileReq");
    assert_eq!(proto::payload_type_name(proto::PT_RECONCILE_RES), "ReconcileRes");
}

#[test]
fn payload_type_name_unknown() {
    assert_eq!(proto::payload_type_name(0), "Unknown");
    assert_eq!(proto::payload_type_name(99999), "Unknown");
}

#[test]
fn is_event_returns_true_for_events() {
    assert!(proto::is_event(proto::PT_HEARTBEAT_EVENT));
    assert!(proto::is_event(proto::PT_EXECUTION_EVENT));
    assert!(proto::is_event(proto::PT_SPOT_EVENT));
    assert!(proto::is_event(proto::PT_ORDER_ERROR_EVENT));
    assert!(proto::is_event(proto::PT_CLIENT_DISCONNECT_EVENT));
    assert!(proto::is_event(proto::PT_ACCOUNTS_TOKEN_INVALIDATED));
    assert!(proto::is_event(proto::PT_ACCOUNT_DISCONNECT_EVENT));
}

#[test]
fn is_event_returns_false_for_req_res() {
    assert!(!proto::is_event(proto::PT_APP_AUTH_REQ));
    assert!(!proto::is_event(proto::PT_APP_AUTH_RES));
    assert!(!proto::is_event(proto::PT_ACCOUNT_AUTH_REQ));
    assert!(!proto::is_event(proto::PT_NEW_ORDER_REQ));
    assert!(!proto::is_event(proto::PT_SYMBOLS_LIST_REQ));
    assert!(!proto::is_event(proto::PT_SYMBOLS_LIST_RES));
    assert!(!proto::is_event(proto::PT_TRADER_REQ));
    assert!(!proto::is_event(proto::PT_RECONCILE_RES));
    assert!(!proto::is_event(proto::PT_OA_ERROR_RES));
}

#[test]
fn payload_type_constants_match_proto_enum() {
    // Verify our constants match the proto enum values
    assert_eq!(proto::PT_HEARTBEAT_EVENT, 51);
    assert_eq!(proto::PT_ERROR_RES, 50);
    assert_eq!(proto::PT_APP_AUTH_REQ, 2100);
    assert_eq!(proto::PT_APP_AUTH_RES, 2101);
    assert_eq!(proto::PT_ACCOUNT_AUTH_REQ, 2102);
    assert_eq!(proto::PT_ACCOUNT_AUTH_RES, 2103);
    assert_eq!(proto::PT_VERSION_REQ, 2104);
    assert_eq!(proto::PT_VERSION_RES, 2105);
    assert_eq!(proto::PT_NEW_ORDER_REQ, 2106);
    assert_eq!(proto::PT_CANCEL_ORDER_REQ, 2108);
    assert_eq!(proto::PT_AMEND_ORDER_REQ, 2109);
    assert_eq!(proto::PT_CLOSE_POSITION_REQ, 2111);
    assert_eq!(proto::PT_SYMBOLS_LIST_REQ, 2114);
    assert_eq!(proto::PT_SYMBOLS_LIST_RES, 2115);
    assert_eq!(proto::PT_EXECUTION_EVENT, 2126);
    assert_eq!(proto::PT_ORDER_ERROR_EVENT, 2132);
    assert_eq!(proto::PT_SPOT_EVENT, 2131);
    assert_eq!(proto::PT_OA_ERROR_RES, 2142);
    assert_eq!(proto::PT_GET_ACCOUNTS_REQ, 2149);
    assert_eq!(proto::PT_GET_ACCOUNTS_RES, 2150);
}

// ══════════════════════════════════════════════════════════════════════
//  7. Execution event parsing tests
// ══════════════════════════════════════════════════════════════════════

#[test]
fn parse_execution_event_returns_none_for_wrong_type() {
    let msg = ProtoMessage {
        payload_type: proto::PT_HEARTBEAT_EVENT,
        payload: None,
        client_msg_id: None,
    };
    assert!(ctrader_openapi::orders::parse_execution_event(&msg).is_none());
}

#[test]
fn parse_execution_event_returns_none_for_missing_payload() {
    let msg = ProtoMessage {
        payload_type: proto::PT_EXECUTION_EVENT,
        payload: None,
        client_msg_id: None,
    };
    assert!(ctrader_openapi::orders::parse_execution_event(&msg).is_none());
}

#[test]
fn parse_execution_event_returns_none_for_invalid_payload() {
    let msg = ProtoMessage {
        payload_type: proto::PT_EXECUTION_EVENT,
        payload: Some(vec![0xFF, 0xFF, 0xFF]),
        client_msg_id: None,
    };
    // Should return None rather than panicking on bad payload
    assert!(ctrader_openapi::orders::parse_execution_event(&msg).is_none());
}

#[test]
fn extract_order_id_returns_none_for_non_execution() {
    let msg = ProtoMessage {
        payload_type: proto::PT_NEW_ORDER_REQ,
        payload: Some(vec![1, 2, 3]),
        client_msg_id: None,
    };
    assert!(ctrader_openapi::orders::extract_order_id(&msg).is_none());
}

// ══════════════════════════════════════════════════════════════════════
//  8. Error type tests
// ══════════════════════════════════════════════════════════════════════

#[test]
fn error_display_rate_limit_non_historical() {
    let err = ctrader_openapi::error::CTraderError::RateLimitExceeded {
        bucket: RateLimitBucket::NonHistorical,
    };
    let msg = err.to_string();
    assert!(msg.contains("non-historical"));
}

#[test]
fn error_display_rate_limit_historical() {
    let err = ctrader_openapi::error::CTraderError::RateLimitExceeded {
        bucket: RateLimitBucket::Historical,
    };
    let msg = err.to_string();
    assert!(msg.contains("historical"));
}

#[test]
fn error_display_unknown_symbol() {
    let err = ctrader_openapi::error::CTraderError::UnknownSymbol("XYZABC".into());
    let msg = err.to_string();
    assert!(msg.contains("XYZABC"));
}

#[test]
fn error_display_timeout() {
    let err =
        ctrader_openapi::error::CTraderError::Timeout(std::time::Duration::from_secs(5));
    let msg = err.to_string();
    assert!(msg.contains("5"));
}

#[test]
fn error_display_server_error() {
    let err = ctrader_openapi::error::CTraderError::ServerError {
        code: "NO_QUOTES".into(),
        description: "No liquidity".into(),
    };
    let msg = err.to_string();
    assert!(msg.contains("NO_QUOTES"));
    assert!(msg.contains("No liquidity"));
}

#[test]
fn error_from_io_error() {
    let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
    let ctrader_err: ctrader_openapi::error::CTraderError = io_err.into();
    let msg = ctrader_err.to_string();
    assert!(msg.contains("refused"));
}

#[test]
fn rate_limit_bucket_display() {
    let nh = format!("{}", RateLimitBucket::NonHistorical);
    assert!(nh.contains("50/s"));
    let h = format!("{}", RateLimitBucket::Historical);
    assert!(h.contains("5/s"));
}

// ══════════════════════════════════════════════════════════════════════
//  9. Proto message construction tests (auth, error)
// ══════════════════════════════════════════════════════════════════════

#[test]
fn construct_oa_error_res() {
    let oa_err = proto::ProtoOaErrorRes {
        payload_type: Some(proto::PT_OA_ERROR_RES as i32),
        ctid_trader_account_id: Some(12345),
        error_code: "CH_CLIENT_AUTH_FAILURE".into(),
        description: Some("Invalid credentials".into()),
        maintenance_end_timestamp: None,
        retry_after: None,
    };

    let bytes = oa_err.encode_to_vec();
    let decoded = proto::ProtoOaErrorRes::decode(bytes.as_slice());
    assert!(decoded.is_ok());
    let decoded = decoded.unwrap_or_default();
    assert_eq!(decoded.error_code, "CH_CLIENT_AUTH_FAILURE");
    assert_eq!(decoded.description.as_deref(), Some("Invalid credentials"));
    assert_eq!(decoded.ctid_trader_account_id, Some(12345));
}

#[test]
fn construct_app_auth_res() {
    let res = proto::ProtoOaApplicationAuthRes {
        payload_type: Some(proto::PT_APP_AUTH_RES as i32),
    };

    let bytes = res.encode_to_vec();
    let envelope = ProtoMessage {
        payload_type: proto::PT_APP_AUTH_RES,
        payload: Some(bytes),
        client_msg_id: None,
    };

    assert_eq!(envelope.payload_type, 2101);
    assert!(envelope.payload.is_some());
}

#[test]
fn construct_account_auth_res() {
    let res = proto::ProtoOaAccountAuthRes {
        payload_type: Some(proto::PT_ACCOUNT_AUTH_RES as i32),
        ctid_trader_account_id: 99999,
    };

    let bytes = res.encode_to_vec();
    let decoded = proto::ProtoOaAccountAuthRes::decode(bytes.as_slice());
    assert!(decoded.is_ok());
    let decoded = decoded.unwrap_or_default();
    assert_eq!(decoded.ctid_trader_account_id, 99999);
}

#[test]
fn construct_version_req_res() {
    // VersionReq has no extra fields
    let req = proto::ProtoOaVersionReq {
        payload_type: Some(proto::PT_VERSION_REQ as i32),
    };
    let bytes = req.encode_to_vec();
    assert!(!bytes.is_empty());

    let res = proto::ProtoOaVersionRes {
        payload_type: Some(proto::PT_VERSION_RES as i32),
        version: "4.0".into(),
    };
    let bytes = res.encode_to_vec();
    let decoded = proto::ProtoOaVersionRes::decode(bytes.as_slice());
    assert!(decoded.is_ok());
    assert_eq!(decoded.unwrap_or_default().version, "4.0");
}

// ══════════════════════════════════════════════════════════════════════
// 10. Symbol list proto construction test
// ══════════════════════════════════════════════════════════════════════

#[test]
fn construct_symbols_list_response() {
    let symbols = vec![
        proto::ProtoOaLightSymbol {
            symbol_id: 1,
            symbol_name: Some("EURUSD".into()),
            enabled: Some(true),
            base_asset_id: Some(1),
            quote_asset_id: Some(2),
            symbol_category_id: None,
            description: Some("Euro vs US Dollar".into()),
            sorting_number: None,
        },
        proto::ProtoOaLightSymbol {
            symbol_id: 2,
            symbol_name: Some("GBPUSD".into()),
            enabled: Some(true),
            base_asset_id: Some(3),
            quote_asset_id: Some(2),
            symbol_category_id: None,
            description: Some("British Pound vs US Dollar".into()),
            sorting_number: None,
        },
        proto::ProtoOaLightSymbol {
            symbol_id: 3,
            symbol_name: Some("USDJPY".into()),
            enabled: Some(false),
            base_asset_id: Some(2),
            quote_asset_id: Some(4),
            symbol_category_id: None,
            description: None,
            sorting_number: None,
        },
    ];

    let res = proto::ProtoOaSymbolsListRes {
        payload_type: Some(proto::PT_SYMBOLS_LIST_RES as i32),
        ctid_trader_account_id: 12345,
        symbol: symbols,
        archived_symbol: vec![],
    };

    let bytes = res.encode_to_vec();
    let decoded = proto::ProtoOaSymbolsListRes::decode(bytes.as_slice());
    assert!(decoded.is_ok());
    let decoded = decoded.unwrap_or_default();
    assert_eq!(decoded.symbol.len(), 3);

    let first = decoded.symbol.first();
    assert!(first.is_some());
    let first = first.unwrap_or(&proto::ProtoOaLightSymbol {
        symbol_id: 0,
        symbol_name: None,
        enabled: None,
        base_asset_id: None,
        quote_asset_id: None,
        symbol_category_id: None,
        description: None,
        sorting_number: None,
    });
    assert_eq!(first.symbol_id, 1);
    assert_eq!(first.symbol_name.as_deref(), Some("EURUSD"));
    assert_eq!(first.enabled, Some(true));
}

// ══════════════════════════════════════════════════════════════════════
// 11. Spot event proto construction test
// ══════════════════════════════════════════════════════════════════════

#[test]
fn construct_spot_event() {
    let spot = proto::ProtoOaSpotEvent {
        payload_type: Some(proto::PT_SPOT_EVENT as i32),
        ctid_trader_account_id: 12345,
        symbol_id: 1,
        bid: Some(110_000), // 1.10000 in 1/100000 units
        ask: Some(110_020), // 1.10020
        trendbar: vec![],
        session_close: None,
        timestamp: Some(1_700_000_000_000),
    };

    let bytes = spot.encode_to_vec();
    let decoded = proto::ProtoOaSpotEvent::decode(bytes.as_slice());
    assert!(decoded.is_ok());
    let decoded = decoded.unwrap_or_default();
    assert_eq!(decoded.symbol_id, 1);
    assert_eq!(decoded.bid, Some(110_000));
    assert_eq!(decoded.ask, Some(110_020));
    assert_eq!(decoded.timestamp, Some(1_700_000_000_000));
}

// ══════════════════════════════════════════════════════════════════════
// 12. Codec with realistic auth sequence
// ══════════════════════════════════════════════════════════════════════

#[test]
fn codec_full_auth_sequence_roundtrip() {
    let mut codec = CTraderCodec::new();
    let mut buf = BytesMut::new();

    // Step 1: Encode ApplicationAuthReq
    let app_auth = proto::ProtoOaApplicationAuthReq {
        payload_type: Some(proto::PT_APP_AUTH_REQ as i32),
        client_id: "my-client-id".into(),
        client_secret: "my-secret".into(),
    };
    let req1 = ProtoMessage {
        payload_type: proto::PT_APP_AUTH_REQ,
        payload: Some(app_auth.encode_to_vec()),
        client_msg_id: Some("auth-1".into()),
    };
    assert!(codec.encode(req1, &mut buf).is_ok());

    // Step 2: Encode ApplicationAuthRes (mock server response)
    let app_auth_res = proto::ProtoOaApplicationAuthRes {
        payload_type: Some(proto::PT_APP_AUTH_RES as i32),
    };
    let res1 = ProtoMessage {
        payload_type: proto::PT_APP_AUTH_RES,
        payload: Some(app_auth_res.encode_to_vec()),
        client_msg_id: Some("auth-1".into()),
    };
    assert!(codec.encode(res1, &mut buf).is_ok());

    // Step 3: Encode AccountAuthReq
    let acct_auth = proto::ProtoOaAccountAuthReq {
        payload_type: Some(proto::PT_ACCOUNT_AUTH_REQ as i32),
        ctid_trader_account_id: 12345,
        access_token: "tok-abc".into(),
    };
    let req2 = ProtoMessage {
        payload_type: proto::PT_ACCOUNT_AUTH_REQ,
        payload: Some(acct_auth.encode_to_vec()),
        client_msg_id: Some("auth-2".into()),
    };
    assert!(codec.encode(req2, &mut buf).is_ok());

    // Step 4: Encode AccountAuthRes (mock server response)
    let acct_auth_res = proto::ProtoOaAccountAuthRes {
        payload_type: Some(proto::PT_ACCOUNT_AUTH_RES as i32),
        ctid_trader_account_id: 12345,
    };
    let res2 = ProtoMessage {
        payload_type: proto::PT_ACCOUNT_AUTH_RES,
        payload: Some(acct_auth_res.encode_to_vec()),
        client_msg_id: Some("auth-2".into()),
    };
    assert!(codec.encode(res2, &mut buf).is_ok());

    // Now decode all 4 messages in order
    let d1 = codec.decode(&mut buf).ok().flatten();
    assert_eq!(
        d1.as_ref().map(|m| m.payload_type),
        Some(proto::PT_APP_AUTH_REQ)
    );
    assert_eq!(
        d1.as_ref().and_then(|m| m.client_msg_id.as_deref()),
        Some("auth-1")
    );

    let d2 = codec.decode(&mut buf).ok().flatten();
    assert_eq!(
        d2.as_ref().map(|m| m.payload_type),
        Some(proto::PT_APP_AUTH_RES)
    );

    let d3 = codec.decode(&mut buf).ok().flatten();
    assert_eq!(
        d3.as_ref().map(|m| m.payload_type),
        Some(proto::PT_ACCOUNT_AUTH_REQ)
    );

    let d4 = codec.decode(&mut buf).ok().flatten();
    assert_eq!(
        d4.as_ref().map(|m| m.payload_type),
        Some(proto::PT_ACCOUNT_AUTH_RES)
    );

    // Buffer should be empty
    assert!(codec.decode(&mut buf).ok().flatten().is_none());
}
