use std::{env, sync::Arc, time::Instant};

use dotenv::dotenv;

use crate::shared::{
    models::{
        margin::Margin,
        price::{Percentage, PercentageCapped},
        quantity::Quantity,
    },
    rest::lnm::rate_limit::RateLimiter,
};

use super::super::super::config::RestClientConfig;
use super::*;

fn init_repository_without_creds_from_env(
    rate_limiter: Option<RateLimiter>,
) -> LnmFuturesRepository {
    dotenv().ok();

    let domain =
        env::var("LNM_API_DOMAIN").expect("LNM_API_DOMAIN environment variable must be set");

    let base = LnmRestBase::new(RestClientConfig::default().timeout(), domain, rate_limiter)
        .expect("Can create `LnmApiBase`");

    LnmFuturesRepository::new(base)
}

fn init_repository_with_creds_from_env() -> LnmFuturesRepository {
    dotenv().ok();

    let domain =
        env::var("LNM_API_DOMAIN").expect("LNM_API_DOMAIN environment variable must be set");
    let key = env::var("LNM_API_V2_KEY").expect("LNM_API_V2_KEY environment variable must be set");
    let secret =
        env::var("LNM_API_V2_SECRET").expect("LNM_API_V2_SECRET environment variable must be set");
    let passphrase = env::var("LNM_API_V2_PASSPHRASE")
        .expect("LNM_API_V2_PASSPHRASE environment variable must be set");

    let base = LnmRestBase::with_credentials(
        RestClientConfig::default().timeout(),
        domain,
        key,
        passphrase,
        SignatureGeneratorV2::new(secret),
        None,
    )
    .expect("Can create `LnmApiBase`");

    LnmFuturesRepository::new(base)
}

async fn test_ticker(repo: &LnmFuturesRepository) -> Ticker {
    let ticker = repo.ticker().await.expect("must get ticker");

    assert!(!ticker.exchanges_weights().is_empty());

    ticker
}

async fn test_create_short_trade_quantity_limit(
    repo: &LnmFuturesRepository,
    ticker: &Ticker,
) -> Trade {
    let side = TradeSide::Sell;
    let quantity = Quantity::try_from(1).unwrap();
    let leverage = Leverage::try_from(1).unwrap();
    let discount_percentage = PercentageCapped::try_from(30).unwrap();
    let out_of_mkt_price = ticker
        .ask_price()
        .apply_discount(discount_percentage)
        .unwrap();
    let execution = out_of_mkt_price.into();
    let stoploss = None;
    let takeprofit = None;

    let created_trade = repo
        .create_new_trade(
            side,
            quantity.into(),
            leverage,
            execution,
            stoploss,
            takeprofit,
        )
        .await
        .expect("must create trade");

    assert_eq!(created_trade.trade_type(), execution.to_type());
    assert_eq!(created_trade.side(), side);
    assert_eq!(created_trade.quantity(), quantity);
    assert_eq!(created_trade.leverage(), leverage);
    assert_eq!(created_trade.price(), out_of_mkt_price);
    assert_eq!(created_trade.stoploss(), stoploss);
    assert_eq!(created_trade.takeprofit(), takeprofit);

    assert!(created_trade.open());
    assert!(!created_trade.running());
    assert!(!created_trade.closed());
    assert!(!created_trade.canceled());

    assert_eq!(created_trade.opening_fee(), 0);
    assert_eq!(created_trade.closing_fee(), 0);

    assert!(created_trade.market_filled_ts().is_none());
    assert!(created_trade.closed_ts().is_none());
    assert!(created_trade.exit_price().is_none());

    created_trade
}

async fn test_create_long_trade_quantity_market(
    repo: &LnmFuturesRepository,
    ticker: &Ticker,
) -> Trade {
    let side = TradeSide::Buy;
    let quantity = Quantity::try_from(1).unwrap();
    let leverage = Leverage::try_from(2).unwrap();
    let est_price = ticker.ask_price();
    let range_percentage = PercentageCapped::try_from(10).unwrap();
    let stoploss = Some(est_price.apply_discount(range_percentage).unwrap());
    let takeprofit = Some(est_price.apply_gain(range_percentage.into()).unwrap());
    let execution = TradeExecution::Market;

    let created_trade = repo
        .create_new_trade(
            side,
            quantity.into(),
            leverage,
            execution,
            stoploss,
            takeprofit,
        )
        .await
        .expect("must create trade");

    assert_eq!(created_trade.trade_type(), execution.to_type());
    assert_eq!(created_trade.side(), side);
    assert_eq!(created_trade.quantity(), quantity);
    assert_eq!(created_trade.leverage(), leverage);
    assert_eq!(created_trade.stoploss(), stoploss);
    assert_eq!(created_trade.takeprofit(), takeprofit);

    assert!(!created_trade.open());
    assert!(created_trade.running());
    assert!(!created_trade.closed());
    assert!(!created_trade.canceled());

    assert!(created_trade.opening_fee() > 0);
    assert_eq!(created_trade.closing_fee(), 0);

    assert!(created_trade.market_filled_ts().is_some());
    assert!(created_trade.closed_ts().is_none());
    assert!(created_trade.exit_price().is_none());

    created_trade
}

async fn test_create_long_trade_margin_limit(
    repo: &LnmFuturesRepository,
    ticker: &Ticker,
) -> Trade {
    let side = TradeSide::Buy;
    let leverage = Leverage::try_from(1).unwrap();
    let discount = PercentageCapped::try_from(30).unwrap();
    let out_of_mkt_price = ticker.ask_price().apply_discount(discount).unwrap();
    let implied_qtd = Quantity::try_from(1).unwrap();
    let margin = Margin::calculate(implied_qtd, out_of_mkt_price, leverage);
    let discount = PercentageCapped::try_from(5).unwrap();
    let stoploss = Some(out_of_mkt_price.apply_discount(discount).unwrap());
    let takeprofit = None;
    let execution = out_of_mkt_price.into();

    let created_trade = repo
        .create_new_trade(
            side,
            margin.into(),
            leverage,
            execution,
            stoploss,
            takeprofit,
        )
        .await
        .expect("must create trade");

    assert_eq!(created_trade.trade_type(), execution.to_type());
    assert_eq!(created_trade.side(), side);
    assert_eq!(created_trade.margin(), margin);
    assert_eq!(created_trade.leverage(), leverage);
    assert_eq!(created_trade.price(), out_of_mkt_price);
    assert_eq!(created_trade.stoploss(), stoploss);
    assert_eq!(created_trade.takeprofit(), takeprofit);

    assert!(created_trade.open());
    assert!(!created_trade.running());
    assert!(!created_trade.closed());
    assert!(!created_trade.canceled());

    assert_eq!(created_trade.opening_fee(), 0);
    assert_eq!(created_trade.closing_fee(), 0);

    assert!(created_trade.market_filled_ts().is_none());
    assert!(created_trade.closed_ts().is_none());
    assert!(created_trade.exit_price().is_none());

    created_trade
}

async fn test_create_short_trade_margin_market(
    repo: &LnmFuturesRepository,
    ticker: &Ticker,
) -> Trade {
    let discount = PercentageCapped::try_from(5).unwrap();
    let est_min_price = ticker.ask_price().apply_discount(discount).unwrap();

    let side = TradeSide::Sell;
    let leverage = Leverage::try_from(1).unwrap();
    let implied_qtd = Quantity::try_from(1).unwrap();
    let margin = Margin::calculate(implied_qtd, est_min_price, leverage);
    let est_price = ticker.ask_price();
    let range = PercentageCapped::try_from(10).unwrap();
    let stoploss = Some(est_price.apply_gain(range.into()).unwrap());
    let takeprofit = Some(est_price.apply_discount(range).unwrap());
    let execution = TradeExecution::Market;

    let created_trade = repo
        .create_new_trade(
            side,
            margin.into(),
            leverage,
            execution,
            stoploss,
            takeprofit,
        )
        .await
        .expect("must create trade");

    assert_eq!(created_trade.trade_type(), execution.to_type());
    assert_eq!(created_trade.side(), side);
    assert_eq!(created_trade.margin(), margin);
    assert_eq!(created_trade.leverage(), leverage);
    assert_eq!(created_trade.stoploss(), stoploss);
    assert_eq!(created_trade.takeprofit(), takeprofit);

    assert!(!created_trade.open());
    assert!(created_trade.running());
    assert!(!created_trade.closed());
    assert!(!created_trade.canceled());

    assert!(created_trade.opening_fee() > 0);
    assert_eq!(created_trade.closing_fee(), 0);

    assert!(created_trade.market_filled_ts().is_some());
    assert!(created_trade.closed_ts().is_none());
    assert!(created_trade.exit_price().is_none());

    created_trade
}

async fn test_get_trade(repo: &LnmFuturesRepository, exp_trade: &Trade) {
    let trade = repo
        .get_trade(exp_trade.id())
        .await
        .expect("must get trade");

    assert_eq!(trade.id(), exp_trade.id());
    assert_eq!(trade.uid(), exp_trade.uid());
    assert_eq!(trade.trade_type(), exp_trade.trade_type());
    assert_eq!(trade.side(), exp_trade.side());
    assert_eq!(trade.opening_fee(), exp_trade.opening_fee());
    assert_eq!(trade.closing_fee(), exp_trade.closing_fee());
    assert_eq!(trade.maintenance_margin(), exp_trade.maintenance_margin());
    assert_eq!(trade.quantity(), exp_trade.quantity());
    assert_eq!(trade.margin(), exp_trade.margin());
    assert_eq!(trade.leverage(), exp_trade.leverage());
    assert_eq!(trade.price(), exp_trade.price());
    assert_eq!(trade.liquidation(), exp_trade.liquidation());
    assert_eq!(trade.stoploss(), exp_trade.stoploss());
    assert_eq!(trade.takeprofit(), exp_trade.takeprofit());
    assert_eq!(trade.exit_price(), exp_trade.exit_price());
    assert_eq!(trade.creation_ts(), exp_trade.creation_ts());
    assert_eq!(trade.market_filled_ts(), exp_trade.market_filled_ts());
    assert_eq!(trade.closed_ts(), exp_trade.closed_ts());
    assert_eq!(trade.entry_price(), exp_trade.entry_price());
    assert_eq!(trade.entry_margin(), exp_trade.entry_margin());
    assert_eq!(trade.open(), exp_trade.open());
    assert_eq!(trade.running(), exp_trade.running());
    assert_eq!(trade.canceled(), exp_trade.canceled());
    assert_eq!(trade.closed(), exp_trade.closed());

    if trade.open() {
        // If the trade was never executed
        assert_eq!(trade.pl(), exp_trade.pl());
        assert_eq!(trade.sum_carry_fees(), exp_trade.sum_carry_fees());
    }
}

async fn test_get_trades_open(repo: &LnmFuturesRepository, exp_open_trades: Vec<&Trade>) {
    let open_trades = repo
        .get_trades(TradeStatus::Open, None, None, None)
        .await
        .expect("must get trades");

    assert_eq!(open_trades.len(), exp_open_trades.len());

    for open_trade in &open_trades {
        let ok = exp_open_trades
            .iter()
            .any(|exp| exp.id() == open_trade.id());
        assert!(ok, "open trade {} was not returned", open_trade.id());
    }
}

async fn test_get_trades_running(repo: &LnmFuturesRepository, exp_running_trades: Vec<&Trade>) {
    let running_trades = repo
        .get_trades(TradeStatus::Running, None, None, None)
        .await
        .expect("must get trades");

    assert_eq!(running_trades.len(), exp_running_trades.len());

    for running_trade in &running_trades {
        let ok = exp_running_trades
            .iter()
            .any(|exp| exp.id() == running_trade.id());
        assert!(ok, "running trade {} was not returned", running_trade.id());
    }
}

async fn test_get_trades_closed(repo: &LnmFuturesRepository, exp_closed_trades: Vec<&Trade>) {
    let closed_trades = repo
        .get_trades(
            TradeStatus::Closed,
            None,
            None,
            Some(exp_closed_trades.len()),
        )
        .await
        .expect("must get trades");

    assert_eq!(closed_trades.len(), exp_closed_trades.len());

    for closed_trade in &closed_trades {
        let ok = exp_closed_trades
            .iter()
            .any(|exp| exp.id() == closed_trade.id());
        assert!(ok, "closed trade {} was not returned", closed_trade.id());
    }
}

async fn test_cancel_trade(repo: &LnmFuturesRepository, id: Uuid) {
    let canceled_trade = repo.cancel_trade(id).await.expect("must cancel trade");

    assert_eq!(canceled_trade.id(), id);
    assert!(!canceled_trade.open());
    assert!(!canceled_trade.running());
    assert!(!canceled_trade.closed());
    assert!(canceled_trade.canceled());
}

async fn test_close_trade(repo: &LnmFuturesRepository, id: Uuid) {
    let closed_trade = repo.close_trade(id).await.expect("must close trade");

    assert_eq!(closed_trade.id(), id);
    assert!(!closed_trade.open());
    assert!(!closed_trade.running());
    assert!(closed_trade.closed());
    assert!(!closed_trade.canceled());
}

async fn test_cancel_all_trades(repo: &LnmFuturesRepository, exp_open_trades: Vec<&Trade>) {
    let cancelled_trades = repo.cancel_all_trades().await.expect("must cancel trades");

    assert_eq!(cancelled_trades.len(), exp_open_trades.len());

    for open_trade in &exp_open_trades {
        let cancelled = cancelled_trades
            .iter()
            .any(|cancelled| cancelled.id() == open_trade.id());
        assert!(cancelled, "trade {} was not cancelled", open_trade.id());
    }
}

async fn test_close_all_trades(repo: &LnmFuturesRepository, exp_running_trades: Vec<&Trade>) {
    let closed_trades = repo.close_all_trades().await.expect("must close trades");

    assert_eq!(closed_trades.len(), exp_running_trades.len());

    for running_trade in &exp_running_trades {
        let closed = closed_trades
            .iter()
            .any(|closed| closed.id() == running_trade.id());
        assert!(closed, "trade {} was not closed", running_trade.id());
    }
}

async fn test_update_trade_stoploss(repo: &LnmFuturesRepository, id: Uuid, price: Price) {
    let gain = Percentage::try_from(5).unwrap();
    let stoploss = price.apply_gain(gain).unwrap();
    let updated_trade = repo
        .update_trade_stoploss(id, stoploss)
        .await
        .expect("must update trade");

    assert_eq!(updated_trade.id(), id);
    assert_eq!(updated_trade.stoploss(), Some(stoploss));
}

async fn test_update_trade_takeprofit(repo: &LnmFuturesRepository, id: Uuid, price: Price) {
    let gain = Percentage::try_from(5).unwrap();
    let takeprofit = price.apply_gain(gain).unwrap();
    let updated_trade = repo
        .update_trade_takeprofit(id, takeprofit)
        .await
        .expect("must update trade");

    assert_eq!(updated_trade.id(), id);
    assert_eq!(updated_trade.takeprofit(), Some(takeprofit));
}

async fn test_add_margin(repo: &LnmFuturesRepository, trade: Trade) -> Trade {
    assert!(trade.leverage().as_f64() > 1.6);

    let target_leverage = Leverage::try_from(1.5).unwrap();
    let target_margin = Margin::calculate(trade.quantity(), trade.price(), target_leverage);
    let amount = target_margin.as_u64() - trade.margin().as_u64();
    let amount = amount.try_into().unwrap();

    let updated_trade = repo
        .add_margin(trade.id(), amount)
        .await
        .expect("must add margin");

    assert_eq!(updated_trade.id(), trade.id());
    assert_eq!(updated_trade.margin(), target_margin);
    assert!(updated_trade.leverage() < trade.leverage());

    updated_trade
}

async fn test_cash_in(repo: &LnmFuturesRepository, trade: Trade) -> Trade {
    assert!(trade.leverage().as_f64() < 1.9);

    let target_leverage = Leverage::try_from(2).unwrap();
    let target_margin = Margin::calculate(trade.quantity(), trade.price(), target_leverage);
    let amount = trade.margin().as_u64() - target_margin.as_u64() + trade.pl().max(0) as u64;
    let amount = amount.try_into().unwrap();

    let updated_trade = repo
        .cash_in(trade.id(), amount)
        .await
        .expect("must cash-in");

    assert_eq!(updated_trade.id(), trade.id());
    assert!(updated_trade.leverage() > trade.leverage());

    updated_trade
}

#[tokio::test]
#[ignore]
async fn test_api() {
    let repo = init_repository_with_creds_from_env();

    macro_rules! time_test {
        ($test_name: expr, $test_block: expr) => {{
            println!("\nStarting test: {}", $test_name);
            let start = Instant::now();
            let result = $test_block;
            let elapsed = start.elapsed();
            println!("Test '{}' took: {:?}", $test_name, elapsed);
            result
        }};
    }

    // Initial clean-up

    time_test!(
        "cancel_all_trades (cleanup)",
        repo.cancel_all_trades().await.expect("must cancel trades")
    );

    time_test!(
        "close_all_trades (cleanup)",
        repo.close_all_trades().await.expect("must close trades")
    );

    // Start tests

    let ticker = time_test!("test_ticker", test_ticker(&repo).await);

    let short_limit_trade_a = time_test!(
        "test_create_short_trade_quantity_limit",
        test_create_short_trade_quantity_limit(&repo, &ticker).await
    );

    time_test!(
        "test_get_trade",
        test_get_trade(&repo, &short_limit_trade_a).await
    );

    let long_limit_trade_b = time_test!(
        "test_create_long_trade_margin_limit",
        test_create_long_trade_margin_limit(&repo, &ticker).await
    );

    time_test!(
        "test_get_trades_open",
        test_get_trades_open(&repo, vec![&short_limit_trade_a, &long_limit_trade_b]).await
    );

    time_test!(
        "test_update_trade_stoploss",
        test_update_trade_stoploss(&repo, short_limit_trade_a.id(), short_limit_trade_a.price())
            .await
    );

    time_test!(
        "test_update_trade_takeprofit",
        test_update_trade_takeprofit(&repo, long_limit_trade_b.id(), long_limit_trade_b.price())
            .await
    );

    time_test!(
        "test_cancel_trade",
        test_cancel_trade(&repo, short_limit_trade_a.id()).await
    );

    time_test!(
        "test_cancel_all_trades",
        test_cancel_all_trades(&repo, vec![&long_limit_trade_b]).await
    );

    let long_market_trade_a = time_test!(
        "test_create_long_trade_quantity_market",
        test_create_long_trade_quantity_market(&repo, &ticker).await
    );

    time_test!(
        "test_get_trade",
        test_get_trade(&repo, &long_market_trade_a).await
    );

    let long_market_trade_a = time_test!(
        "test_add_margin",
        test_add_margin(&repo, long_market_trade_a).await
    );

    let long_market_trade_a = time_test!(
        "test_cash_in",
        test_cash_in(&repo, long_market_trade_a).await
    );

    let short_market_trade_b = time_test!(
        "test_create_short_trade_margin_market",
        test_create_short_trade_margin_market(&repo, &ticker).await
    );

    time_test!(
        "test_get_trades_running",
        test_get_trades_running(&repo, vec![&long_market_trade_a, &short_market_trade_b]).await
    );

    time_test!(
        "test_close_trade",
        test_close_trade(&repo, long_market_trade_a.id()).await
    );

    time_test!(
        "test_close_all_trades",
        test_close_all_trades(&repo, vec![&short_market_trade_b]).await
    );

    time_test!(
        "test_get_trades_closed",
        test_get_trades_closed(&repo, vec![&long_market_trade_a, &short_market_trade_b]).await
    );
}

// Fires 35 concurrent `ticker` requests through a rate-limited client.
// NOTE: As of Feb 2026, LNM doesn't return a 429 for up to 120 req/min for this endpoint.
//
// The rate limiter paces authenticated requests at 30 req/min, in line with official doc limit.
#[tokio::test]
#[ignore]
async fn test_v2_rate_limiter_prevents_unauth_429() {
    let config = RestClientConfig::default();
    let rate_limiter = RateLimiter::from(&config);
    let repo = Arc::new(init_repository_without_creds_from_env(Some(rate_limiter)));

    let total_requests: usize = 35;
    let mut handles = Vec::with_capacity(total_requests);

    let start = Instant::now();

    for i in 0..total_requests {
        let repo = repo.clone();
        handles.push(tokio::spawn(async move {
            let result = repo.ticker().await;
            (i, result)
        }));
    }

    let mut successes = 0;
    let mut failures = Vec::new();

    for handle in handles {
        let (i, result) = handle.await.expect("task must not panic");
        match result {
            Ok(_) => successes += 1,
            Err(e) => failures.push((i, e)),
        }
    }

    let elapsed = start.elapsed();

    println!("\n{total_requests} concurrent requests completed in {elapsed:?}");
    println!("  successes: {successes}");
    println!("  failures:  {}", failures.len());

    for (i, err) in &failures {
        println!("  request #{i} failed: {err}");
    }

    assert!(
        failures.is_empty(),
        "rate limiter should prevent all 429 errors, but {}/{total_requests} requests failed",
        failures.len(),
    );
}
