#![allow(
    clippy::assertions_on_constants,
    clippy::assertions_on_result_states,
    clippy::clone_on_copy,
    clippy::decimal_literal_representation,
    clippy::doc_markdown,
    clippy::empty_line_after_doc_comments,
    clippy::field_reassign_with_default,
    clippy::get_unwrap,
    clippy::identity_op,
    clippy::inconsistent_digit_grouping,
    clippy::indexing_slicing,
    clippy::integer_division,
    clippy::len_zero,
    clippy::let_underscore_must_use,
    clippy::manual_div_ceil,
    clippy::manual_let_else,
    clippy::manual_range_contains,
    clippy::modulo_arithmetic,
    clippy::needless_range_loop,
    clippy::non_ascii_literal,
    clippy::redundant_clone,
    clippy::shadow_reuse,
    clippy::shadow_same,
    clippy::shadow_unrelated,
    clippy::single_match_else,
    clippy::str_to_string,
    clippy::string_slice,
    clippy::tests_outside_test_module,
    clippy::too_many_lines,
    clippy::unnecessary_wraps,
    clippy::unseparated_literal_suffix,
    clippy::use_debug,
    clippy::useless_vec,
    clippy::wildcard_enum_match_arm,
    clippy::else_if_without_else,
    clippy::expect_used,
    clippy::missing_const_for_fn,
    clippy::similar_names,
    clippy::type_complexity,
    clippy::collapsible_else_if,
    clippy::doc_lazy_continuation,
    clippy::items_after_test_module,
    clippy::map_clone,
    clippy::multiple_unsafe_ops_per_block,
    clippy::unwrap_or_default,
    clippy::assign_op_pattern,
    clippy::needless_borrow,
    clippy::println_empty_string,
    clippy::unnecessary_cast,
    clippy::used_underscore_binding,
    clippy::create_dir,
    clippy::implicit_saturating_sub,
    clippy::exit,
    clippy::expect_fun_call,
    clippy::too_many_arguments,
    clippy::unnecessary_map_or,
    clippy::unwrap_used,
    dead_code,
    unused_imports,
    unused_variables,
    clippy::cloned_ref_to_slice_refs,
    clippy::neg_multiply,
    clippy::while_let_loop,
    clippy::bool_assert_comparison,
    clippy::excessive_precision,
    clippy::trivially_copy_pass_by_ref,
    clippy::op_ref,
    clippy::redundant_closure,
    clippy::unnecessary_lazy_evaluations,
    clippy::if_then_some_else_none,
    clippy::unnecessary_to_owned,
    clippy::single_component_path_imports,
)]
//! Bug #3: Net P&L Regression Tests
//!
//! Tests that evaluation engine calculates NET P&L (after transaction costs),
//! not GROSS P&L. Ensures all metrics (Sharpe, win rate, returns) are accurate.

use ml::evaluation::engine::{Action, EvaluationEngine, PositionDirection};
use ml::evaluation::metrics::{OHLCVBarF32, PerformanceMetrics};

/// Test that Trade struct includes transaction cost fields
#[test]
fn test_trade_struct_has_cost_fields() {
    // Create test bars
    let bars = create_test_bars();

    // Create engine and execute a trade
    let mut engine = EvaluationEngine::new(10000.0);

    // Open long position at bar 0 (price 100.0)
    engine.process_bar(0, &bars[0], Action::Buy);
    assert!(engine.current_position.is_some());

    // Close position at bar 1 (price 100.5)
    engine.process_bar(1, &bars[1], Action::Sell);
    assert_eq!(engine.trades.len(), 1);

    let trade = &engine.trades[0];

    // Verify Trade struct has the required fields
    assert_eq!(trade.entry_price, 100.0);
    assert_eq!(trade.exit_price, 100.5);

    // Calculate expected values
    let gross_pnl = 0.5; // exit - entry

    // Assume market orders (0.15% each)
    let entry_cost = 100.0 * 0.0015; // 0.15
    let exit_cost = 100.5 * 0.0015;  // 0.15075
    let total_cost = entry_cost + exit_cost; // 0.30075

    let expected_net_pnl = gross_pnl - total_cost; // 0.5 - 0.30075 = 0.19925

    // Verify trade.pnl is NET (after costs), not GROSS
    // Allow small floating point error
    assert!(
        (trade.pnl - expected_net_pnl).abs() < 0.001,
        "Trade.pnl should be NET P&L ({}), got {}. Gross would be {}",
        expected_net_pnl,
        trade.pnl,
        gross_pnl
    );

    // Verify trade is still profitable after costs
    assert!(trade.pnl > 0.0, "Trade should be profitable after costs");
}

/// Test that small profit becomes loss after transaction costs
#[test]
fn test_losing_trade_after_costs() {
    let bars = vec![
        OHLCVBarF32 {
            timestamp: 1000,
            open: 100.0,
            high: 100.0,
            low: 100.0,
            close: 100.0,
            volume: 1000.0,
        },
        OHLCVBarF32 {
            timestamp: 2000,
            open: 100.2,
            high: 100.2,
            low: 100.2,
            close: 100.2, // Only 0.2% profit
            volume: 1000.0,
        },
    ];

    let mut engine = EvaluationEngine::new(10000.0);

    // Open long at 100.0
    engine.process_bar(0, &bars[0], Action::Buy);

    // Close at 100.2 (0.2% gross profit)
    engine.process_bar(1, &bars[1], Action::Sell);

    assert_eq!(engine.trades.len(), 1);
    let trade = &engine.trades[0];

    // Gross profit: 0.2
    let gross_pnl = 0.2;

    // Transaction costs: 0.15% entry + 0.15% exit = 0.30%
    let entry_cost = 100.0 * 0.0015; // 0.15
    let exit_cost = 100.2 * 0.0015;  // 0.1503
    let total_cost = entry_cost + exit_cost; // 0.3003

    let expected_net_pnl = gross_pnl - total_cost; // 0.2 - 0.3003 = -0.1003 (LOSS!)

    // Verify trade is a LOSS after costs
    assert!(
        (trade.pnl - expected_net_pnl).abs() < 0.001,
        "Trade should be a loss after costs ({}), got {}",
        expected_net_pnl,
        trade.pnl
    );
    assert!(trade.pnl < 0.0, "Trade should be a loss after costs (gross profit too small)");
}

/// Test that performance metrics use net P&L
#[test]
fn test_metrics_use_net_pnl() {
    let bars = create_test_bars();
    let mut engine = EvaluationEngine::new(10000.0);

    // Execute a single trade (gross profit 0.5)
    engine.process_bar(0, &bars[0], Action::Buy);
    engine.process_bar(1, &bars[1], Action::Sell);

    let metrics = PerformanceMetrics::from_trades(&engine.trades, 10000.0, &bars);

    // Calculate expected net P&L
    let gross_pnl = 0.5;
    let total_cost = 100.0 * 0.0015 + 100.5 * 0.0015; // 0.30075
    let expected_net_pnl = gross_pnl - total_cost; // 0.19925

    // Verify metrics use net P&L
    let expected_return = (expected_net_pnl / 10000.0) * 100.0; // 0.0019925%

    assert!(
        (metrics.total_return_pct - expected_return).abs() < 0.0001,
        "Total return should be based on NET P&L ({}%), got {}%",
        expected_return,
        metrics.total_return_pct
    );

    let expected_final_equity = 10000.0 + expected_net_pnl; // 10000.19925
    assert!(
        (metrics.final_equity - expected_final_equity).abs() < 0.001,
        "Final equity should be based on NET P&L ({}), got {}",
        expected_final_equity,
        metrics.final_equity
    );
}

/// Test that win rate calculation uses net P&L
#[test]
fn test_win_rate_uses_net_pnl() {
    let bars = vec![
        // Trade 1: Large profit (0.8) - still profitable after costs
        OHLCVBarF32 { timestamp: 1000, open: 100.0, high: 100.0, low: 100.0, close: 100.0, volume: 1000.0 },
        OHLCVBarF32 { timestamp: 2000, open: 100.8, high: 100.8, low: 100.8, close: 100.8, volume: 1000.0 },
        // Hold to avoid opening new position
        OHLCVBarF32 { timestamp: 3000, open: 100.8, high: 100.8, low: 100.8, close: 100.8, volume: 1000.0 },
        // Trade 2: Small profit (0.2) - becomes loss after costs
        OHLCVBarF32 { timestamp: 4000, open: 100.8, high: 100.8, low: 100.8, close: 100.8, volume: 1000.0 },
        OHLCVBarF32 { timestamp: 5000, open: 101.0, high: 101.0, low: 101.0, close: 101.0, volume: 1000.0 },
    ];

    let mut engine = EvaluationEngine::new(10000.0);

    // Trade 1: Buy at 100.0, Sell at 100.8 (gross profit 0.8)
    engine.process_bar(0, &bars[0], Action::Buy);
    engine.process_bar(1, &bars[1], Action::Sell); // This closes long AND opens short

    // Hold to close the short position first
    engine.process_bar(2, &bars[2], Action::Buy); // This closes short AND opens long

    // Trade 2: Already have long from previous Buy, now hold then sell
    engine.process_bar(3, &bars[3], Action::Hold); // Hold the long position
    engine.process_bar(4, &bars[4], Action::Sell); // Close long at 101.0 AND open short

    // We should have 3 trades total:
    // 1. Long 100.0->100.8 (win)
    // 2. Short 100.8->100.8 (break-even, but costs make it a loss)
    // 3. Long 100.8->101.0 (small profit, but costs make it a loss)

    assert_eq!(engine.trades.len(), 3);

    // Trade 1: Long 100.0->100.8, net = 0.8 - (100.0*0.0015 + 100.8*0.0015) = 0.4988 (WIN)
    assert!(engine.trades[0].pnl > 0.0, "Trade 1 should be a win, got {}", engine.trades[0].pnl);

    // Trade 2: Short 100.8->100.8, gross = 0.0, costs = 0.3012, net = -0.3012 (LOSS)
    assert!(engine.trades[1].pnl < 0.0, "Trade 2 should be a loss after costs, got {}", engine.trades[1].pnl);

    // Trade 3: Long 100.8->101.0, gross = 0.2, costs = 0.3027, net = -0.1027 (LOSS)
    assert!(engine.trades[2].pnl < 0.0, "Trade 3 should be a loss after costs, got {}", engine.trades[2].pnl);

    let metrics = PerformanceMetrics::from_trades(&engine.trades, 10000.0, &bars);

    // Win rate should be 33.3% (1 win, 2 losses), NOT 66.7% or 100%
    assert!(
        (metrics.win_rate - 33.333).abs() < 1.0,
        "Win rate should be ~33% (1 net win, 2 net losses), got {}%",
        metrics.win_rate
    );
}

/// Test short position with transaction costs
#[test]
fn test_short_position_transaction_costs() {
    let bars = vec![
        OHLCVBarF32 {
            timestamp: 1000,
            open: 100.0,
            high: 100.0,
            low: 100.0,
            close: 100.0,
            volume: 1000.0,
        },
        OHLCVBarF32 {
            timestamp: 2000,
            open: 99.5,
            high: 99.5,
            low: 99.5,
            close: 99.5, // Price dropped 0.5
            volume: 1000.0,
        },
    ];

    let mut engine = EvaluationEngine::new(10000.0);

    // Open short at 100.0
    engine.process_bar(0, &bars[0], Action::Sell);
    assert!(engine.current_position.is_some());

    if let Some(pos) = &engine.current_position {
        assert_eq!(pos.direction, PositionDirection::Short);
    }

    // Close short at 99.5 (gross profit: 100.0 - 99.5 = 0.5)
    engine.process_bar(1, &bars[1], Action::Buy);

    assert_eq!(engine.trades.len(), 1);
    let trade = &engine.trades[0];

    // Gross profit: 0.5
    let gross_pnl = 0.5;

    // Transaction costs
    let entry_cost = 100.0 * 0.0015; // 0.15
    let exit_cost = 99.5 * 0.0015;   // 0.14925
    let total_cost = entry_cost + exit_cost; // 0.29925

    let expected_net_pnl = gross_pnl - total_cost; // 0.5 - 0.29925 = 0.20075

    assert!(
        (trade.pnl - expected_net_pnl).abs() < 0.001,
        "Short trade net P&L should be {} (gross {} - costs {}), got {}",
        expected_net_pnl,
        gross_pnl,
        total_cost,
        trade.pnl
    );
}

/// Test Kelly fraction with transaction costs
#[test]
fn test_kelly_fraction_with_costs() {
    let bars = create_test_bars();

    // Kelly fraction = 0.5 (half position)
    let mut engine = EvaluationEngine::new_with_kelly(10000.0, 0.5);

    engine.process_bar(0, &bars[0], Action::Buy);
    engine.process_bar(1, &bars[1], Action::Sell);

    assert_eq!(engine.trades.len(), 1);
    let trade = &engine.trades[0];

    // Base gross P&L (full position): 0.5
    // Kelly-scaled gross P&L: 0.5 * 0.5 = 0.25
    let kelly_scaled_gross = 0.5 * 0.5;

    // Transaction costs (FULL position value, not scaled)
    // Note: Costs are based on entry/exit prices, not scaled by Kelly
    let entry_cost = 100.0 * 0.5 * 0.0015; // 0.075 (half position)
    let exit_cost = 100.5 * 0.5 * 0.0015;  // 0.075375 (half position)
    let total_cost = entry_cost + exit_cost; // 0.150375

    let expected_net_pnl = kelly_scaled_gross - total_cost; // 0.25 - 0.150375 = 0.099625

    assert!(
        (trade.pnl - expected_net_pnl).abs() < 0.001,
        "Kelly-scaled trade should have net P&L {} (gross {} - costs {}), got {}",
        expected_net_pnl,
        kelly_scaled_gross,
        total_cost,
        trade.pnl
    );
}

/// Test that zero profit after costs is counted as loss
#[test]
fn test_zero_profit_after_costs() {
    // Set up a trade that exactly breaks even after costs
    // Gross profit = transaction costs
    let bars = vec![
        OHLCVBarF32 {
            timestamp: 1000,
            open: 100.0,
            high: 100.0,
            low: 100.0,
            close: 100.0,
            volume: 1000.0,
        },
        OHLCVBarF32 {
            timestamp: 2000,
            open: 100.3,
            high: 100.3,
            low: 100.3,
            close: 100.3, // Gross profit ~0.30 ≈ costs
            volume: 1000.0,
        },
    ];

    let mut engine = EvaluationEngine::new(10000.0);

    engine.process_bar(0, &bars[0], Action::Buy);
    engine.process_bar(1, &bars[1], Action::Sell);

    let trade = &engine.trades[0];

    // This trade should break even or have tiny profit/loss
    assert!(
        trade.pnl.abs() < 0.05,
        "Trade should be near break-even after costs, got {}",
        trade.pnl
    );
}

// Helper function to create standard test bars
fn create_test_bars() -> Vec<OHLCVBarF32> {
    vec![
        OHLCVBarF32 {
            timestamp: 1000,
            open: 100.0,
            high: 100.0,
            low: 100.0,
            close: 100.0,
            volume: 1000.0,
        },
        OHLCVBarF32 {
            timestamp: 2000,
            open: 100.5,
            high: 100.5,
            low: 100.5,
            close: 100.5,
            volume: 1000.0,
        },
    ]
}
