use arrow::array::{Array, Float64Array, StringArray, TimestampNanosecondArray, UInt8Array};
use arrow::record_batch::RecordBatch;
use bt_connectors::{BarInterval, BinanceProvider, DataProvider};
use bt_kernel::{TimeRange, Timestamp};
use mockito::{Matcher, Server};

fn sample_range() -> TimeRange {
    TimeRange::new(
        Timestamp::from_nanos(1_704_067_200_000_000_000),
        Timestamp::from_nanos(1_704_067_203_000_000_000),
    )
    .expect("valid range")
}

#[tokio::test]
async fn fetch_trades_parses_sampled_payload() {
    let mut server = Server::new_async().await;
    let payload = r#"[
      {"a": 1001, "p": "42000.10", "q": "0.25", "T": 1704067200000, "m": false},
      {"a": 1002, "p": "42010.00", "q": "0.10", "T": 1704067201000, "m": true}
    ]"#;

    let endpoint = server
        .mock("GET", "/api/v3/aggTrades")
        .match_query(Matcher::Any)
        .with_status(200)
        .with_header("content-type", "application/json")
        .with_body(payload)
        .create_async()
        .await;

    let provider = BinanceProvider::with_base_urls(server.url(), server.url());
    let batch = provider
        .fetch_trades("BTCUSDT", sample_range())
        .await
        .expect("fetch trades");
    endpoint.assert_async().await;

    assert_eq!(batch.num_rows(), 2);

    let timestamps = timestamp_column(&batch, "timestamp");
    assert_eq!(timestamps.value(0), 1_704_067_200_000_000_000);
    assert_eq!(timestamps.value(1), 1_704_067_201_000_000_000);

    let symbols = string_column(&batch, "symbol_raw");
    assert_eq!(symbols.value(0), "BTCUSDT");
    assert_eq!(symbols.value(1), "BTCUSDT");

    let prices = f64_column(&batch, "price");
    assert_eq!(prices.value(0), 42000.10);
    assert_eq!(prices.value(1), 42010.00);

    let sides = u8_column(&batch, "side");
    assert_eq!(sides.value(0), 1);
    assert_eq!(sides.value(1), 2);
}

#[tokio::test]
async fn fetch_bars_parses_sampled_payload() {
    let mut server = Server::new_async().await;
    let payload = r#"[
      [1704067200000, "42000.0", "42100.0", "41950.0", "42050.0", "12.5", 1704067200999, "525625.0", 150]
    ]"#;

    let endpoint = server
        .mock("GET", "/api/v3/klines")
        .match_query(Matcher::Any)
        .with_status(200)
        .with_header("content-type", "application/json")
        .with_body(payload)
        .create_async()
        .await;

    let provider = BinanceProvider::with_base_urls(server.url(), server.url());
    let batch = provider
        .fetch_bars("BTCUSDT", sample_range(), BarInterval::Seconds(1))
        .await
        .expect("fetch bars")
        .expect("provider returned bars");
    endpoint.assert_async().await;

    assert_eq!(batch.num_rows(), 1);

    let open = f64_column(&batch, "open");
    let close = f64_column(&batch, "close");
    let vwap = f64_column(&batch, "vwap");

    assert_eq!(open.value(0), 42000.0);
    assert_eq!(close.value(0), 42050.0);
    assert_eq!(vwap.value(0), 42050.0);
}

#[tokio::test]
async fn fetch_funding_rates_parses_sampled_payload() {
    let mut server = Server::new_async().await;
    let payload = r#"[
      {"symbol":"BTCUSDT","fundingRate":"0.00010000","fundingTime":1704067200000,"markPrice":"42123.50"},
      {"symbol":"BTCUSDT","fundingRate":"-0.00020000","fundingTime":1704096000000}
    ]"#;

    let endpoint = server
        .mock("GET", "/fapi/v1/fundingRate")
        .match_query(Matcher::Any)
        .with_status(200)
        .with_header("content-type", "application/json")
        .with_body(payload)
        .create_async()
        .await;

    let provider = BinanceProvider::with_base_urls(server.url(), server.url());
    let batch = provider
        .fetch_funding_rates("BTCUSDT", sample_range())
        .await
        .expect("fetch funding rates")
        .expect("provider returned funding rates");
    endpoint.assert_async().await;

    assert_eq!(batch.num_rows(), 2);

    let rates = f64_column(&batch, "funding_rate");
    assert_eq!(rates.value(0), 0.0001);
    assert_eq!(rates.value(1), -0.0002);

    let mark_prices = f64_column(&batch, "mark_price");
    assert_eq!(mark_prices.value(0), 42123.5);
    assert!(mark_prices.is_null(1));
}

#[tokio::test]
async fn list_symbols_parses_sampled_payload() {
    let mut server = Server::new_async().await;
    let payload = r#"{
      "symbols": [
        {"symbol":"BTCUSDT","status":"TRADING","baseAsset":"BTC","quoteAsset":"USDT"},
        {"symbol":"ETHUSDT","status":"TRADING","baseAsset":"ETH","quoteAsset":"USDT"}
      ]
    }"#;

    let endpoint = server
        .mock("GET", "/api/v3/exchangeInfo")
        .with_status(200)
        .with_header("content-type", "application/json")
        .with_body(payload)
        .create_async()
        .await;

    let provider = BinanceProvider::with_base_urls(server.url(), server.url());
    let symbols = provider.list_symbols().await.expect("list symbols");
    endpoint.assert_async().await;

    assert_eq!(symbols.len(), 2);
    assert_eq!(symbols[0].symbol, "BTCUSDT");
    assert_eq!(symbols[0].base_asset, "BTC");
    assert_eq!(symbols[0].quote_asset, "USDT");
    assert_eq!(symbols[0].status, "TRADING");
}

fn timestamp_column<'a>(batch: &'a RecordBatch, name: &str) -> &'a TimestampNanosecondArray {
    batch
        .column_by_name(name)
        .expect("column exists")
        .as_any()
        .downcast_ref::<TimestampNanosecondArray>()
        .expect("timestamp array")
}

fn string_column<'a>(batch: &'a RecordBatch, name: &str) -> &'a StringArray {
    batch
        .column_by_name(name)
        .expect("column exists")
        .as_any()
        .downcast_ref::<StringArray>()
        .expect("string array")
}

fn f64_column<'a>(batch: &'a RecordBatch, name: &str) -> &'a Float64Array {
    batch
        .column_by_name(name)
        .expect("column exists")
        .as_any()
        .downcast_ref::<Float64Array>()
        .expect("float64 array")
}

fn u8_column<'a>(batch: &'a RecordBatch, name: &str) -> &'a UInt8Array {
    batch
        .column_by_name(name)
        .expect("column exists")
        .as_any()
        .downcast_ref::<UInt8Array>()
        .expect("u8 array")
}
