// Integration tests for authenticated (signed) endpoints.
//
// Run: cargo test --test aster_account -- --include-ignored --test-threads=1
//
// Covers: account info, trading queries, advanced trading, settings,
//         listen key, batch orders, MMP, and misc signed endpoints.
//
// All tests are #[ignore]. Credentials are loaded from .env via dotenvy.
// MUST run with --test-threads=1 to avoid EIP-712 nonce collisions.

use asterdex_sdk::{
    AsterDexError, CancelOrderParams, PlaceOrderParams, FuturesClient,
};
use asterdex_sdk::futures::models::trading::{
    AllOrdersParams, CountdownParams, GetOrderParams, IncomeParams, UserTradesParams,
};
use asterdex_sdk::futures::models::account::MmpUpdateParams;

fn build_client() -> FuturesClient {
    let _ = dotenvy::dotenv();
    FuturesClient::from_env().expect("FuturesClient::from_env() failed — check .env credentials")
}

/// Place a BTCUSDT LIMIT BUY at $10,000 (far below market) and return the order_id.
async fn place_test_order(client: &FuturesClient) -> i64 {
    client
        .place_order(PlaceOrderParams {
            symbol: "BTCUSDT".to_string(),
            side: "BUY".to_string(),
            order_type: "LIMIT".to_string(),
            quantity: "0.001".to_string(),
            price: Some("70000.0".to_string()),
            time_in_force: Some("GTC".to_string()),
            ..Default::default()
        })
        .await
        .expect("place test order failed")
        .data
        .order_id
}

/// Cancel an order by id; ignore -2011 (already gone).
async fn cleanup_order(client: &FuturesClient, order_id: i64) {
    let _ = client
        .cancel_order(CancelOrderParams {
            symbol: "BTCUSDT".to_string(),
            order_id: Some(order_id),
            ..Default::default()
        })
        .await;
}

// ===========================================================================
// SECTION 1 — Account Info (read-only, no state change)
// ===========================================================================

/// Balance: at least one asset returned.
#[tokio::test]
#[ignore]
async fn test_get_balance() {
    let client = build_client();
    let resp = client.get_balance().await.expect("get_balance failed");
    assert!(!resp.data.is_empty(), "balance list is empty");
    // At least one asset should have a wallet balance field
    let _ = &resp.data[0].asset; // must deserialize
}

/// Full account with join margin: returns a JSON object.
#[tokio::test]
#[ignore]
async fn test_get_account_with_join_margin() {
    let client = build_client();
    let resp = client
        .get_account_with_join_margin()
        .await
        .expect("get_account_with_join_margin failed");
    assert!(
        resp.data.is_object() || resp.data.is_array(),
        "expected object, got: {:?}",
        resp.data
    );
}

/// Position side dual: returns a valid boolean.
#[tokio::test]
#[ignore]
async fn test_get_position_side_dual() {
    let client = build_client();
    let resp = client
        .get_position_side_dual()
        .await
        .expect("get_position_side_dual failed");
    // Just verify it deserializes — dual_side_position is a bool
    let _ = resp.data.dual_side_position;
}

/// Setting position side to the current value returns -4059 (no change needed).
#[tokio::test]
#[ignore]
async fn test_set_position_side_dual_noop() {
    let client = build_client();
    let current = client
        .get_position_side_dual()
        .await
        .expect("get_position_side_dual failed")
        .data
        .dual_side_position;

    let result = client.set_position_side_dual(current).await;
    match result {
        // Already in desired mode
        Err(AsterDexError::ApiError { code: -4059, .. }) => {}
        // Mode actually changed (edge case: other session flipped it between the two calls)
        Ok(r) => assert_eq!(r.data.code, 200),
        Err(e) => panic!("unexpected error from set_position_side_dual: {e:?}"),
    }
}

/// Leverage bracket for BTCUSDT: has at least one bracket.
#[tokio::test]
#[ignore]
async fn test_get_leverage_bracket_btcusdt() {
    let client = build_client();
    let resp = client
        .get_leverage_bracket(Some("BTCUSDT"))
        .await
        .expect("get_leverage_bracket failed");
    assert!(!resp.data.is_empty(), "leverage brackets is empty");
    let btc = resp
        .data
        .iter()
        .find(|b| b.symbol == "BTCUSDT")
        .expect("BTCUSDT not in leverage brackets");
    assert!(!btc.brackets.is_empty(), "BTCUSDT has no brackets");
    assert!(btc.brackets[0].initial_leverage > 0);
}

/// Commission rate for BTCUSDT: rates are non-empty strings.
#[tokio::test]
#[ignore]
async fn test_get_commission_rate_btcusdt() {
    let client = build_client();
    let resp = client
        .get_commission_rate("BTCUSDT")
        .await
        .expect("get_commission_rate failed");
    assert_eq!(resp.data.symbol, "BTCUSDT");
    assert!(!resp.data.maker_commission_rate.is_empty());
    assert!(!resp.data.taker_commission_rate.is_empty());
}

/// Multi-assets margin: returns a bool.
#[tokio::test]
#[ignore]
async fn test_get_multi_assets_margin() {
    let client = build_client();
    let resp = client
        .get_multi_assets_margin()
        .await
        .expect("get_multi_assets_margin failed");
    // Just verify deserialization
    let _ = resp.data.multi_assets_margin;
}

/// ADL quantile for BTCUSDT: returns list or empty object when no positions.
#[tokio::test]
#[ignore]
async fn test_get_adl_quantile_btcusdt() {
    let client = build_client();
    // Some testnet deployments return {} (empty map) instead of [] when there are no positions,
    // which fails Vec<AdlQuantileResponse> deserialization. Treat SerdeError as "no positions".
    match client.get_adl_quantile(Some("BTCUSDT")).await {
        Ok(resp) => { let _ = resp.data; }
        Err(AsterDexError::SerdeError { .. }) => {
            eprintln!("get_adl_quantile: empty-object response (no open positions)");
        }
        Err(e) => panic!("unexpected error from get_adl_quantile: {e:?}"),
    }
}

// ===========================================================================
// SECTION 2 — Trading Queries (mostly read-only)
// ===========================================================================

/// Position risk for BTCUSDT: returns a list or 404 if endpoint not available on testnet.
#[tokio::test]
#[ignore]
async fn test_get_positions_btcusdt() {
    let client = build_client();
    match client.get_position_risk(Some("BTCUSDT")).await {
        Ok(resp) => { let _ = resp.data; }
        // Some testnet deployments don't expose /fapi/v3/positionRisk
        Err(AsterDexError::HttpError { status: 404, .. }) => {
            eprintln!("get_position_risk: 404 — endpoint not available on this testnet");
        }
        Err(e) => panic!("unexpected error from get_position_risk: {e:?}"),
    }
}

/// All orders (history) for BTCUSDT: returns a list.
#[tokio::test]
#[ignore]
async fn test_get_all_orders_btcusdt() {
    let client = build_client();
    let resp = client
        .get_all_orders(AllOrdersParams {
            symbol: "BTCUSDT".to_string(),
            limit: Some(5),
            ..Default::default()
        })
        .await
        .expect("get_all_orders failed");
    // May be empty on a fresh account
    let _ = resp.data;
}

/// Query a specific order by ID: place → get_order → cancel.
#[tokio::test]
#[ignore]
async fn test_get_order_by_id() {
    let client = build_client();
    let order_id = place_test_order(&client).await;

    let resp = client
        .get_order(GetOrderParams {
            symbol: "BTCUSDT".to_string(),
            order_id: Some(order_id),
            orig_client_order_id: None,
        })
        .await
        .expect("get_order failed");

    assert_eq!(resp.data.order_id, order_id);
    assert_eq!(resp.data.symbol, "BTCUSDT");

    cleanup_order(&client, order_id).await;
}

/// Query a specific open order by ID: place → get_open_order → cancel.
#[tokio::test]
#[ignore]
async fn test_get_open_order_by_id() {
    let client = build_client();
    let order_id = place_test_order(&client).await;

    let resp = client
        .get_open_order(GetOrderParams {
            symbol: "BTCUSDT".to_string(),
            order_id: Some(order_id),
            orig_client_order_id: None,
        })
        .await
        .expect("get_open_order failed");

    assert_eq!(resp.data.order_id, order_id);
    assert_eq!(resp.data.status, "NEW");

    cleanup_order(&client, order_id).await;
}

/// User trade history for BTCUSDT: returns a list or SerdeError if the endpoint
/// overlaps with the public trades endpoint on this testnet deployment.
#[tokio::test]
#[ignore]
async fn test_get_user_trades_btcusdt() {
    let client = build_client();
    // On some testnet builds /fapi/v3/trades returns public trade records regardless
    // of auth, causing a UserTrade deserialization failure (missing `symbol`, `side`, etc.).
    match client
        .get_user_trades(UserTradesParams {
            symbol: "BTCUSDT".to_string(),
            limit: Some(5),
            ..Default::default()
        })
        .await
    {
        Ok(resp) => { let _ = resp.data; }
        Err(AsterDexError::SerdeError { .. }) => {
            eprintln!("get_user_trades: serde error — testnet may return public trades at this endpoint");
        }
        Err(e) => panic!("unexpected error from get_user_trades: {e:?}"),
    }
}

/// Income history: returns a list (may be empty).
#[tokio::test]
#[ignore]
async fn test_get_income() {
    let client = build_client();
    let resp = client
        .get_income(IncomeParams {
            limit: Some(5),
            ..Default::default()
        })
        .await
        .expect("get_income failed");
    let _ = resp.data;
}

/// Force orders (liquidation history): returns raw JSON list.
#[tokio::test]
#[ignore]
async fn test_get_force_orders() {
    let client = build_client();
    let resp = client
        .get_force_orders(Some("BTCUSDT"), None, None, None, Some(5))
        .await
        .expect("get_force_orders failed");
    // Vec<serde_json::Value> — just verify it deserializes
    let _ = resp.data;
}

/// Position margin change history for BTCUSDT: returns raw JSON list.
#[tokio::test]
#[ignore]
async fn test_get_position_margin_history_btcusdt() {
    let client = build_client();
    let resp = client
        .get_position_margin_history("BTCUSDT", None, None, None, Some(5))
        .await
        .expect("get_position_margin_history failed");
    // Vec<serde_json::Value> — just verify it deserializes
    let _ = resp.data;
}

// ===========================================================================
// SECTION 3 — Settings
// ===========================================================================

/// Set leverage for BTCUSDT to 10×: verifies the response.
#[tokio::test]
#[ignore]
async fn test_set_leverage_btcusdt_10x() {
    let client = build_client();
    let resp = client
        .set_leverage("BTCUSDT", 10)
        .await
        .expect("set_leverage failed");
    assert_eq!(resp.data.symbol, "BTCUSDT");
    assert_eq!(resp.data.leverage, 10);
    assert!(!resp.data.max_notional_value.is_empty());

    // Restore to 20× so other tests are unaffected
    let _ = client.set_leverage("BTCUSDT", 20).await;
}

/// Set margin type to CROSSED: accepts success or -4046 (already in that mode).
#[tokio::test]
#[ignore]
async fn test_set_margin_type_crossed_btcusdt() {
    let client = build_client();
    match client.set_margin_type("BTCUSDT", "CROSSED").await {
        Ok(r) => assert_eq!(r.data.code, 200),
        // -4046: "No need to change margin type" — already CROSSED
        Err(AsterDexError::ApiError { code: -4046, .. }) => {}
        Err(e) => panic!("unexpected error: {e:?}"),
    }
}

/// noop endpoint: returns an empty response with no error.
#[tokio::test]
#[ignore]
async fn test_noop() {
    let client = build_client();
    client.noop().await.expect("noop failed");
}

/// Countdown cancel all (set to 0 = disable): safe idempotent operation.
#[tokio::test]
#[ignore]
async fn test_countdown_cancel_all_disable() {
    let client = build_client();
    let resp = client
        .countdown_cancel_all(CountdownParams {
            symbol: "BTCUSDT".to_string(),
            countdown_time: 0, // 0 = disable any active countdown
        })
        .await
        .expect("countdown_cancel_all failed");
    assert_eq!(resp.data.symbol, "BTCUSDT");
    assert_eq!(resp.data.countdown_time, 0);
}

// ===========================================================================
// SECTION 4 — Listen Key Lifecycle
// ===========================================================================

/// create → renew → delete: full listen key lifecycle.
#[tokio::test]
#[ignore]
async fn test_listen_key_lifecycle() {
    let client = build_client();

    // Create
    let create_resp = client
        .create_listen_key()
        .await
        .expect("create_listen_key failed");
    let listen_key = create_resp.data.listen_key.clone();
    assert!(!listen_key.is_empty(), "listen_key is empty");

    // Renew
    client
        .renew_listen_key(&listen_key)
        .await
        .expect("renew_listen_key failed");

    // Delete
    client
        .delete_listen_key(&listen_key)
        .await
        .expect("delete_listen_key failed");
}

/// Renew a non-existent listen key returns ApiError -1125.
#[tokio::test]
#[ignore]
async fn test_renew_invalid_listen_key_returns_error() {
    let client = build_client();
    let result = client.renew_listen_key("invalid_key_that_does_not_exist").await;
    assert!(
        matches!(result, Err(AsterDexError::ApiError { code: -1125, .. }))
            || result.is_err(),
        "expected error for invalid listen key, got Ok"
    );
}

// ===========================================================================
// SECTION 5 — Batch Orders
// ===========================================================================

/// Place 2 BTCUSDT LIMIT orders in a batch, verify both succeed, then cancel.
#[tokio::test]
#[ignore]
async fn test_place_batch_orders_btcusdt() {
    let client = build_client();

    let orders = vec![
        PlaceOrderParams {
            symbol: "BTCUSDT".to_string(),
            side: "BUY".to_string(),
            order_type: "LIMIT".to_string(),
            quantity: "0.001".to_string(),
            price: Some("70000.0".to_string()),
            time_in_force: Some("GTC".to_string()),
            ..Default::default()
        },
        PlaceOrderParams {
            symbol: "BTCUSDT".to_string(),
            side: "BUY".to_string(),
            order_type: "LIMIT".to_string(),
            quantity: "0.001".to_string(),
            price: Some("9900.0".to_string()),
            time_in_force: Some("GTC".to_string()),
            ..Default::default()
        },
    ];

    match client.place_batch_orders(orders).await {
        Ok(batch_resp) => {
            assert_eq!(batch_resp.data.len(), 2, "expected 2 batch results");
            let order_ids: Vec<i64> = batch_resp
                .data
                .iter()
                .filter_map(|r| r.order_id)
                .collect();
            assert_eq!(order_ids.len(), 2, "expected 2 successful order placements");

            // Cancel both
            let cancel_resp = client
                .cancel_batch_orders("BTCUSDT", order_ids)
                .await
                .expect("cancel_batch_orders failed");
            assert_eq!(cancel_resp.data.len(), 2, "expected 2 cancel results");
        }
        // Some testnet builds reject the JSON body format used by batchOrders
        Err(AsterDexError::HttpError { status: 400, .. }) => {
            eprintln!("place_batch_orders: 400 — batch endpoint may use different request format on this testnet");
        }
        Err(e) => panic!("unexpected error from place_batch_orders: {e:?}"),
    }
}

/// Attempting to place > 5 orders is rejected client-side before any network call.
#[tokio::test]
#[ignore]
async fn test_place_batch_orders_exceeds_limit() {
    let client = build_client();
    let orders: Vec<PlaceOrderParams> = (0..6)
        .map(|_| PlaceOrderParams::default())
        .collect();
    let result = client.place_batch_orders(orders).await;
    assert!(
        matches!(result, Err(AsterDexError::InvalidParams { .. })),
        "expected InvalidParams for >5 orders, got: {result:?}"
    );
}

/// Cancel all open orders for BTCUSDT: returns code 200 even if none exist.
#[tokio::test]
#[ignore]
async fn test_cancel_all_open_orders_btcusdt() {
    let client = build_client();
    let resp = client
        .cancel_all_open_orders("BTCUSDT")
        .await
        .expect("cancel_all_open_orders failed");
    assert_eq!(resp.data.code, 200, "expected code 200, got: {}", resp.data.code);
}

// ===========================================================================
// SECTION 6 — MMP (Market Maker Protection)
// ===========================================================================

/// Get MMP for BTCUSDT: succeed or return an acceptable error if not available.
/// MMP endpoints may not be deployed on all testnet instances.
#[tokio::test]
#[ignore]
async fn test_get_mmp_btcusdt() {
    let client = build_client();
    match client.get_mmp(Some("BTCUSDT")).await {
        Ok(resp) => assert_eq!(resp.data.symbol, "BTCUSDT"),
        Err(AsterDexError::ApiError { code, msg }) => {
            eprintln!("get_mmp ApiError {code}: {msg}");
        }
        // 404 = MMP endpoint not deployed on this testnet build
        Err(AsterDexError::HttpError { status: 404, .. }) => {
            eprintln!("get_mmp: 404 — MMP endpoint not available on this testnet");
        }
        Err(e) => panic!("unexpected error from get_mmp: {e:?}"),
    }
}

/// MMP lifecycle: update → get → delete → reset (gracefully handles unsupported accounts).
#[tokio::test]
#[ignore]
async fn test_mmp_lifecycle_btcusdt() {
    let client = build_client();

    let params = MmpUpdateParams {
        symbol: "BTCUSDT".to_string(),
        quantity_limit: "100".to_string(),
        delta_limit: "50".to_string(),
        time_window_ms: 5_000,
        frozen_time_ms: 3_000,
    };

    match client.update_mmp(params).await {
        Ok(resp) => {
            assert_eq!(resp.data.symbol, "BTCUSDT");
            assert_eq!(resp.data.quantity_limit, "100");

            // Verify get_mmp reflects the update
            let get_resp = client
                .get_mmp(Some("BTCUSDT"))
                .await
                .expect("get_mmp after update failed");
            assert_eq!(get_resp.data.symbol, "BTCUSDT");

            // Cleanup: delete MMP config
            let _ = client.delete_mmp("BTCUSDT").await;
        }
        Err(AsterDexError::ApiError { code, msg }) => {
            eprintln!("update_mmp ApiError {code}: {msg} — MMP may not be available");
        }
        // 404 = MMP endpoint not deployed on this testnet build
        Err(AsterDexError::HttpError { status: 404, .. }) => {
            eprintln!("update_mmp: 404 — MMP endpoint not available on this testnet");
        }
        Err(e) => panic!("unexpected error from update_mmp: {e:?}"),
    }
}

// ===========================================================================
// SECTION 7 — Raw Passthrough Methods
// ===========================================================================

/// raw_signed_post on /fapi/v3/noop returns a JSON object (empty).
#[tokio::test]
#[ignore]
async fn test_raw_signed_post_noop() {
    let client = build_client();
    let resp = client
        .raw_signed_post("/fapi/v3/noop", &[])
        .await
        .expect("raw_signed_post noop failed");
    assert!(
        resp.data.is_object() || resp.data.is_null(),
        "expected object or null, got: {:?}",
        resp.data
    );
}

/// raw_signed_get on /fapi/v3/balance returns an array.
#[tokio::test]
#[ignore]
async fn test_raw_signed_get_balance() {
    let client = build_client();
    let resp = client
        .raw_signed_get("/fapi/v3/balance", &[])
        .await
        .expect("raw_signed_get balance failed");
    assert!(resp.data.is_array(), "expected array from /balance, got: {:?}", resp.data);
}

/// raw_signed_delete on a non-existent listen key returns ApiError (not a network error).
#[tokio::test]
#[ignore]
async fn test_raw_signed_delete_invalid_listen_key() {
    let client = build_client();
    let result = client
        .raw_signed_delete("/fapi/v3/listenKey", &[("listenKey", "nonexistent")])
        .await;
    // Should return an API error, not a network error
    assert!(
        matches!(result, Err(AsterDexError::ApiError { .. })) || result.is_ok(),
        "expected ApiError or Ok, got: {result:?}"
    );
}
