use bt_analytics::{
    compute_daily_returns, compute_performance_metrics, compute_trade_statistics, resample_to_daily,
};

const NANOS_PER_DAY: i64 = 86_400_000_000_000;

#[test]
fn metrics_compute_total_return_and_drawdown() {
    // 4 bars, all in the same UTC day → fallback to per-bar returns.
    // bar_seconds=86400, trading_days=365.25 → bars_per_year = 365.25
    let equity = vec![100.0, 120.0, 90.0, 130.0];
    let timestamps_ns = vec![0_i64, 1_000_000_000, 2_000_000_000, 3_000_000_000];
    let metrics =
        compute_performance_metrics(&equity, &timestamps_ns, 86400.0, 365.25).expect("compute");

    assert!((metrics.total_return - 0.3).abs() < 1e-12);
    assert!((metrics.max_drawdown + 0.25).abs() < 1e-12);
}

#[test]
fn metrics_multiday_uses_daily_returns() {
    // Equity across 4 UTC days — should use daily resampling path.
    let equity = vec![
        // Day 0: two bars
        100.0, 105.0, // Day 1: two bars
        110.0, 108.0, // Day 2: two bars
        112.0, 115.0, // Day 3: two bars
        118.0, 120.0,
    ];
    let timestamps_ns: Vec<i64> = (0..8)
        .map(|i| {
            let day = i / 2;
            let intra = i % 2;
            day * NANOS_PER_DAY + intra * 1_000_000_000
        })
        .collect();

    let metrics =
        compute_performance_metrics(&equity, &timestamps_ns, 1.0, 365.25).expect("compute");

    // total_return = 120/100 - 1 = 0.2
    assert!((metrics.total_return - 0.2).abs() < 1e-12);
    // max_drawdown: peak tracking: 100, 105, 110, 108 (dd=108/110-1=-2/110), 112, 115, 118, 120
    assert!(metrics.max_drawdown < 0.0);
    assert!((metrics.max_drawdown - (-2.0 / 110.0)).abs() < 1e-12);

    // Sharpe/vol should be based on daily returns (annualized with 365.25).
    assert!(metrics.volatility > 0.0);
    assert!(metrics.sharpe != 0.0);
}

#[test]
fn metrics_single_day_fallback_matches_per_bar() {
    // All timestamps in same UTC day → fallback to per-bar returns.
    let equity = vec![100.0, 101.0, 102.0, 103.0];
    let timestamps_ns = vec![0_i64, 1_000_000_000, 2_000_000_000, 3_000_000_000];

    // bar_seconds=86400 so bars_per_year=365.25 (reasonable CAGR).
    let metrics =
        compute_performance_metrics(&equity, &timestamps_ns, 86400.0, 365.25).expect("compute");

    let bars_per_year = 365.25;
    assert!((metrics.total_return - 0.03).abs() < 1e-12);
    // CAGR uses bars_per_year
    let expected_cagr = (103.0 / 100.0_f64).powf(bars_per_year / 3.0) - 1.0;
    assert!((metrics.cagr - expected_cagr).abs() < 1e-10);
}

#[test]
fn resample_to_daily_groups_by_utc_day() {
    let equity = vec![100.0, 105.0, 110.0, 108.0, 115.0];
    let timestamps_ns = vec![
        0,                 // day 0
        NANOS_PER_DAY / 2, // day 0 (midday)
        NANOS_PER_DAY,     // day 1
        NANOS_PER_DAY + 1, // day 1
        NANOS_PER_DAY * 2, // day 2
    ];

    let daily = resample_to_daily(&equity, &timestamps_ns);
    // Day 0: last = 105, Day 1: last = 108, Day 2: last = 115
    assert_eq!(daily, vec![105.0, 108.0, 115.0]);
}

#[test]
fn compute_daily_returns_multiday() {
    let equity = vec![100.0, 105.0, 110.0, 108.0];
    let timestamps_ns = vec![
        0,                 // day 0
        NANOS_PER_DAY / 2, // day 0
        NANOS_PER_DAY,     // day 1
        NANOS_PER_DAY * 2, // day 2
    ];

    let returns = compute_daily_returns(&equity, &timestamps_ns);
    // daily equity: [105, 110, 108]
    // returns: [110/105 - 1, 108/110 - 1]
    assert_eq!(returns.len(), 2);
    assert!((returns[0] - (110.0 / 105.0 - 1.0)).abs() < 1e-12);
    assert!((returns[1] - (108.0 / 110.0 - 1.0)).abs() < 1e-12);
}

#[test]
fn compute_daily_returns_single_day_returns_empty() {
    let equity = vec![100.0, 105.0, 110.0];
    let timestamps_ns = vec![0, 1_000_000_000, 2_000_000_000]; // all day 0

    let returns = compute_daily_returns(&equity, &timestamps_ns);
    assert!(returns.is_empty());
}

#[test]
fn metrics_rejects_empty_equity() {
    let result = compute_performance_metrics(&[], &[], 1.0, 365.25);
    assert!(result.is_err());
}

#[test]
fn metrics_rejects_length_mismatch() {
    let result = compute_performance_metrics(&[100.0, 110.0], &[0], 1.0, 365.25);
    assert!(result.is_err());
}

// ---------------------------------------------------------------------------
// Extended metrics tests
// ---------------------------------------------------------------------------

#[test]
fn metrics_extended_fields_populated() {
    // Multi-day equity with some volatility, including a losing day.
    let equity = vec![
        100.0, 105.0, // day 0 → daily=105
        110.0, 108.0, // day 1 → daily=108 (up from 105)
        100.0, 98.0, // day 2 → daily=98 (down from 108)
        103.0, 110.0, // day 3 → daily=110 (up from 98)
    ];
    let timestamps_ns: Vec<i64> = (0..8)
        .map(|i| {
            let day = i / 2;
            let intra = i % 2;
            day * NANOS_PER_DAY + intra * 1_000_000_000
        })
        .collect();

    let metrics =
        compute_performance_metrics(&equity, &timestamps_ns, 1.0, 365.25).expect("compute");

    // Extended fields should be non-default.
    assert!(metrics.best_day > 0.0, "best_day should be positive");
    assert!(metrics.worst_day < 0.0, "worst_day should be negative");
    assert!(metrics.pct_positive_days > 0.0, "should have positive days");
    assert!(
        metrics.pct_positive_days < 1.0,
        "should have some negative days"
    );
    assert!(metrics.ulcer_index >= 0.0, "ulcer index >= 0");
    assert!(
        metrics.omega_ratio > 0.0,
        "omega should be positive for profitable strat"
    );

    // Drawdown duration should be non-zero (there's a drawdown on day 1).
    assert!(
        metrics.max_drawdown_duration_days > 0.0,
        "drawdown duration should be > 0"
    );

    // trade_stats should be None when not set.
    assert!(metrics.trade_stats.is_none());
}

#[test]
fn metrics_skewness_and_kurtosis() {
    // Symmetric equity curve → skewness near 0.
    let equity = vec![
        100.0, 110.0, // day 0: +10%
        100.0, 110.0, // day 1: +10%  (daily: 100→110 = +10%)
        100.0, 110.0, // day 2: same
    ];
    let timestamps_ns: Vec<i64> = (0..6)
        .map(|i| {
            let day = i / 2;
            let intra = i % 2;
            day * NANOS_PER_DAY + intra * 1_000_000_000
        })
        .collect();

    let metrics =
        compute_performance_metrics(&equity, &timestamps_ns, 1.0, 365.25).expect("compute");

    // With constant daily returns, skewness should be ~0 and excess kurtosis should be ~-1.5.
    // (Two identical returns = not enough variation for meaningful higher moments.)
    assert!(
        metrics.skewness.abs() < 1.0,
        "skewness should be near zero for symmetric returns"
    );
}

// ---------------------------------------------------------------------------
// Trade statistics tests
// ---------------------------------------------------------------------------

#[test]
fn trade_stats_empty_fills() {
    let stats = compute_trade_statistics(&[], None);
    assert_eq!(stats.total_trades, 0);
    assert_eq!(stats.round_trips, 0);
    assert_eq!(stats.win_rate, 0.0);
}

#[test]
fn trade_stats_single_round_trip_long() {
    // Buy 10 @ $100 at t=0, sell 10 @ $110 at t=3600s.
    let fills = vec![
        (1_u32, 0_i64, 1_u8, 10.0, 100.0, 1.0),                 // buy
        (1_u32, 3_600_000_000_000_i64, 2_u8, 10.0, 110.0, 1.0), // sell
    ];
    let stats = compute_trade_statistics(&fills, None);

    assert_eq!(stats.total_trades, 2);
    assert_eq!(stats.round_trips, 1);
    assert!(
        (stats.win_rate - 1.0).abs() < 1e-12,
        "should be 100% win rate"
    );

    // PnL = (110-100)*10 - 1 - 1 = 98
    assert!(stats.avg_win > 0.0, "should have positive avg win");

    // Holding period = 3600 seconds.
    assert!(
        (stats.avg_holding_seconds - 3600.0).abs() < 1.0,
        "holding period should be ~3600s, got {}",
        stats.avg_holding_seconds
    );
    assert_eq!(stats.total_fees, 2.0);
}

#[test]
fn trade_stats_winning_and_losing_round_trips() {
    // RT1: buy 10@100, sell 10@110 → win
    // RT2: buy 10@110, sell 10@105 → loss
    let fills = vec![
        (1, 0_i64, 1, 10.0, 100.0, 0.0),
        (1, NANOS_PER_DAY, 2, 10.0, 110.0, 0.0),
        (1, NANOS_PER_DAY * 2, 1, 10.0, 110.0, 0.0),
        (1, NANOS_PER_DAY * 3, 2, 10.0, 105.0, 0.0),
    ];
    let stats = compute_trade_statistics(&fills, None);

    assert_eq!(stats.round_trips, 2);
    assert!((stats.win_rate - 0.5).abs() < 1e-12, "50% win rate");
    assert!(stats.avg_win > 0.0);
    assert!(stats.avg_loss < 0.0);
    assert!(stats.profit_factor > 0.0);
    assert_eq!(stats.max_consecutive_wins, 1);
    assert_eq!(stats.max_consecutive_losses, 1);
}

#[test]
fn trade_stats_multi_symbol() {
    // Two symbols, each with one round trip.
    let fills = vec![
        (1, 0_i64, 1, 5.0, 100.0, 0.5),             // sym1 buy
        (2, 1_000_000_000_i64, 1, 3.0, 200.0, 0.5), // sym2 buy
        (1, NANOS_PER_DAY, 2, 5.0, 120.0, 0.5),     // sym1 sell (win)
        (2, NANOS_PER_DAY * 2, 2, 3.0, 180.0, 0.5), // sym2 sell (loss)
    ];
    let stats = compute_trade_statistics(&fills, None);

    assert_eq!(stats.total_trades, 4);
    assert_eq!(stats.round_trips, 2);
    assert!((stats.win_rate - 0.5).abs() < 1e-12);
    assert_eq!(stats.total_fees, 2.0);
}

#[test]
fn trade_stats_consecutive_wins() {
    // 3 winning round trips in a row.
    let fills = vec![
        (1, 0_i64, 1, 1.0, 100.0, 0.0),
        (1, 1_000_000_000, 2, 1.0, 110.0, 0.0),
        (1, 2_000_000_000, 1, 1.0, 100.0, 0.0),
        (1, 3_000_000_000, 2, 1.0, 115.0, 0.0),
        (1, 4_000_000_000, 1, 1.0, 100.0, 0.0),
        (1, 5_000_000_000, 2, 1.0, 120.0, 0.0),
    ];
    let stats = compute_trade_statistics(&fills, None);

    assert_eq!(stats.round_trips, 3);
    assert!((stats.win_rate - 1.0).abs() < 1e-12);
    assert_eq!(stats.max_consecutive_wins, 3);
    assert_eq!(stats.max_consecutive_losses, 0);
}
