//! Integration tests against a real cTrader demo server.
//!
//! All tests are `#[ignore]` by default — they require live credentials
//! via environment variables and network access to `demo.ctraderapi.com:5035`.
//!
//! # Required environment variables
//!
//! - `CTRADER_CLIENT_ID` — OAuth2 application client ID
//! - `CTRADER_CLIENT_SECRET` — OAuth2 application client secret
//! - `CTRADER_ACCESS_TOKEN` — OAuth2 access token
//! - `CTRADER_ACCOUNT_ID` — cTID trader account ID
//!
//! # Running
//!
//! ```bash
//! CTRADER_CLIENT_ID=xxx CTRADER_CLIENT_SECRET=xxx \
//!   CTRADER_ACCESS_TOKEN=xxx CTRADER_ACCOUNT_ID=12345 \
//!   cargo test -p ctrader-openapi --test demo_integration -- --ignored
//! ```

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

use std::time::Duration;

use ctrader_openapi::config::{CTraderConfig, CTraderEnvironment};
use ctrader_openapi::orders;
use ctrader_openapi::proto::{ProtoOaOrderType, ProtoOaTradeSide};
use ctrader_openapi::CTraderClient;

/// Convenience error type for integration tests.
type TestResult = Result<(), Box<dyn std::error::Error>>;

/// Build a `CTraderConfig` from environment variables.
///
/// Returns `None` if any required variable is missing or unparseable,
/// which causes tests to skip gracefully.
fn demo_config() -> Option<CTraderConfig> {
    let client_id = std::env::var("CTRADER_CLIENT_ID").ok()?;
    let client_secret = std::env::var("CTRADER_CLIENT_SECRET").ok()?;
    let access_token = std::env::var("CTRADER_ACCESS_TOKEN").ok()?;
    let account_id: i64 = std::env::var("CTRADER_ACCOUNT_ID").ok()?.parse().ok()?;

    Some(CTraderConfig {
        client_id,
        client_secret,
        access_token,
        account_id,
        environment: CTraderEnvironment::Demo,
        heartbeat_interval_secs: 10,
        request_timeout_ms: 5000,
        max_reconnect_attempts: 3,
    })
}

// ── Test 1: Connect and authenticate ────────────────────────────────

#[tokio::test]
#[ignore]
async fn test_demo_connect_and_authenticate() -> TestResult {
    let config = match demo_config() {
        Some(c) => c,
        None => {
            eprintln!("skipping: CTRADER_* env vars not set");
            return Ok(());
        }
    };

    let account_id = config.account_id;
    let client = CTraderClient::connect(config).await?;

    // Verify we got a valid client with the right account ID
    assert_eq!(client.account_id(), account_id);

    // Symbol mapper must have been loaded during connect
    assert!(
        !client.symbols().is_empty(),
        "symbol list should not be empty after connect"
    );

    Ok(())
}

// ── Test 2: Load and verify symbols ─────────────────────────────────

#[tokio::test]
#[ignore]
async fn test_demo_load_symbols() -> TestResult {
    let config = match demo_config() {
        Some(c) => c,
        None => {
            eprintln!("skipping: CTRADER_* env vars not set");
            return Ok(());
        }
    };

    let client = CTraderClient::connect(config).await?;

    // Symbols are loaded during connect — verify EURUSD exists
    let eurusd_id = client.symbol_id("EURUSD");
    assert!(
        eurusd_id.is_ok(),
        "EURUSD should be in the symbol list, got: {eurusd_id:?}"
    );

    let eurusd_info = client.symbols().resolve("EURUSD");
    assert!(eurusd_info.is_some(), "EURUSD should resolve to SymbolInfo");

    if let Some(info) = eurusd_info {
        assert!(info.enabled, "EURUSD should be enabled for trading");
        assert!(info.symbol_id > 0, "EURUSD symbol ID should be positive");
    }

    // Verify total symbol count is reasonable (ICMarkets demo typically has 200+)
    assert!(
        client.symbols().len() > 10,
        "expected >10 symbols, got {}",
        client.symbols().len()
    );

    Ok(())
}

// ── Test 3: Account info ────────────────────────────────────────────

#[tokio::test]
#[ignore]
async fn test_demo_account_info() -> TestResult {
    let config = match demo_config() {
        Some(c) => c,
        None => {
            eprintln!("skipping: CTRADER_* env vars not set");
            return Ok(());
        }
    };

    let client = CTraderClient::connect(config).await?;
    let info = client.get_account_info().await?;

    // Demo accounts have positive balance
    assert!(
        info.balance > 0,
        "demo account balance should be > 0, got {}",
        info.balance
    );

    // Deposit asset ID should be valid
    assert!(
        info.deposit_asset_id > 0,
        "deposit_asset_id should be positive, got {}",
        info.deposit_asset_id
    );

    // Registration timestamp should be non-zero
    assert!(
        info.registration_timestamp > 0,
        "registration_timestamp should be positive"
    );

    eprintln!(
        "account info: balance={}, leverage=1:{}, deposit_asset={}",
        info.balance,
        info.leverage_in_cents / 100,
        info.deposit_asset_id
    );

    Ok(())
}

// ── Test 4: Submit and cancel a limit order ─────────────────────────

#[tokio::test]
#[ignore]
async fn test_demo_submit_and_cancel_limit_order() -> TestResult {
    let config = match demo_config() {
        Some(c) => c,
        None => {
            eprintln!("skipping: CTRADER_* env vars not set");
            return Ok(());
        }
    };

    let client = CTraderClient::connect(config).await?;

    // Subscribe to execution events before placing order
    let mut exec_rx = client.subscribe_executions();

    // Submit a far-from-market BUY LIMIT on EURUSD at a very low price.
    // 0.01 lots = 1000 volume, price 0.50000 is far enough from market.
    let order_result = client
        .submit_order(
            "EURUSD",
            ProtoOaTradeSide::Buy,
            1000,                    // 0.01 lots
            ProtoOaOrderType::Limit, // limit order
            Some(0.50000),           // far-from-market limit price
            None,                    // no stop price
            None,                    // no stop loss
            None,                    // no take profit
            Some("foxhunt-demo-test".into()),
        )
        .await;

    let order_response = match order_result {
        Ok(resp) => resp,
        Err(e) => {
            eprintln!("order submission failed (may be expected on some accounts): {e}");
            return Ok(());
        }
    };

    // Try to extract the order ID from the execution event response
    let order_id = orders::extract_order_id(&order_response);
    eprintln!(
        "order response payload_type={}",
        order_response.payload_type
    );

    // Also check the broadcast channel for the execution event
    let broadcast_event = tokio::time::timeout(Duration::from_secs(5), exec_rx.recv()).await;
    if let Ok(Ok(event)) = broadcast_event {
        if let Some(exec_info) = orders::parse_execution_event(&event) {
            eprintln!(
                "execution event: order_id={}, side={}, volume={}",
                exec_info.order_id, exec_info.trade_side, exec_info.volume
            );
        }
    }

    // If we got an order ID, cancel it
    if let Some(oid) = order_id {
        eprintln!("cancelling order {oid}");
        let cancel_result = client.cancel_order(oid).await;
        match cancel_result {
            Ok(_) => eprintln!("order {oid} cancelled successfully"),
            Err(e) => eprintln!("cancel failed (order may have been rejected): {e}"),
        }
    } else {
        // Even without an extracted order ID, reconcile to check
        eprintln!("could not extract order ID from response, checking via reconcile");
        let reconcile = client.get_positions().await;
        if let Ok(r) = reconcile {
            eprintln!(
                "reconcile: {} positions, {} pending orders",
                r.positions.len(),
                r.orders.len()
            );
        }
    }

    Ok(())
}

// ── Test 5: Reconcile positions ─────────────────────────────────────

#[tokio::test]
#[ignore]
async fn test_demo_reconcile_positions() -> TestResult {
    let config = match demo_config() {
        Some(c) => c,
        None => {
            eprintln!("skipping: CTRADER_* env vars not set");
            return Ok(());
        }
    };

    let client = CTraderClient::connect(config).await?;
    let result = client.get_positions().await?;

    // Result should be valid (positions/orders may be empty on fresh demo)
    eprintln!(
        "reconcile result: {} open positions, {} pending orders",
        result.positions.len(),
        result.orders.len()
    );

    // Verify positions have valid structure if any exist
    for pos in &result.positions {
        assert!(pos.position_id > 0, "position ID should be positive");
    }

    // Verify orders have valid structure if any exist
    for ord in &result.orders {
        assert!(ord.order_id > 0, "order ID should be positive");
    }

    Ok(())
}

// ── Test 6: Spot subscription ───────────────────────────────────────

#[tokio::test]
#[ignore]
async fn test_demo_spot_subscription() -> TestResult {
    let config = match demo_config() {
        Some(c) => c,
        None => {
            eprintln!("skipping: CTRADER_* env vars not set");
            return Ok(());
        }
    };

    let client = CTraderClient::connect(config).await?;

    // Subscribe to EURUSD spots
    let mut spot_rx = client.subscribe_spots(&["EURUSD"]).await?;

    // Wait for at least one tick within 10 seconds
    let tick_result = tokio::time::timeout(Duration::from_secs(10), spot_rx.recv()).await;

    match tick_result {
        Ok(Ok(msg)) => {
            assert_eq!(
                msg.payload_type,
                ctrader_openapi::proto::PT_SPOT_EVENT,
                "expected SpotEvent payload type"
            );
            eprintln!("received spot tick (payload_type={})", msg.payload_type);
        }
        Ok(Err(e)) => {
            // Broadcast channel lagged or closed — acceptable in test
            eprintln!("spot rx error (may be lag): {e}");
        }
        Err(_elapsed) => {
            // Market may be closed (weekend) — not a hard failure
            eprintln!("no spot tick received within 10s (market may be closed)");
        }
    }

    // Unsubscribe cleanly
    let unsub_result = client.unsubscribe_spots(&["EURUSD"]).await;
    if let Err(e) = unsub_result {
        eprintln!("unsubscribe warning: {e}");
    }

    Ok(())
}
