#![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,
)]
//! Portfolio Features Integration Tests (TDD Approach)
//!
//! These tests verify that portfolio features are correctly integrated into
//! the DQN training pipeline, from PortfolioTracker -> TradingState -> RewardFunction.
//!
//! Test Strategy:
//! - Test 1-2: Verify portfolio features are populated in TradingState
//! - Test 3-4: Verify P&L rewards are calculated correctly
//! - Test 5-6: Verify portfolio tracking across different actions
//! - Test 7-8: Verify edge cases (zero position, negative P&L, large positions)
//! - Test 9-10: Integration tests with batch processing

use ml::dqn::action_space::{ExposureLevel, FactoredAction, OrderType, Urgency};
use ml::dqn::agent::TradingState;
use ml::dqn::portfolio_tracker::PortfolioTracker;
use ml::dqn::reward::{RewardConfig, RewardFunction};
use tracing::info;

// Helper functions for consistent 3-action semantics in tests
fn buy_action() -> FactoredAction {
    FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal)
}

fn sell_action() -> FactoredAction {
    FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal)
}

fn hold_action() -> FactoredAction {
    FactoredAction::new(ExposureLevel::Flat, OrderType::Market, Urgency::Normal)
}

/// Helper function for approximate equality checks with transaction costs
/// Tolerance of 0.2% (20 basis points) to account for:
/// - Transaction costs: 0.05-0.15% per trade
/// - Slippage and fees in HFT environments
/// - Floating point precision errors
fn assert_approx_eq(actual: f32, expected: f32, context: &str) {
    let tolerance = expected.abs() * 0.002; // 0.2% tolerance
    let diff = (actual - expected).abs();
    assert!(
        diff < tolerance,
        "{}: Expected {:.2}, got {:.2} (diff: {:.2}, tolerance: {:.2})",
        context,
        expected,
        actual,
        diff,
        tolerance
    );
}

// ============================================================================
// Test 1: Portfolio Features Populated in TradingState
// ============================================================================

/// Test that portfolio features are correctly populated in TradingState
/// from PortfolioTracker.get_portfolio_features()
#[test]
fn test_portfolio_features_populated() -> anyhow::Result<()> {
    // Setup: Create portfolio tracker with known state
    let tracker = PortfolioTracker::new(10_000.0, 0.0001, 0.0);
    let current_price = 100.0;

    // Execute: Get portfolio features
    let features = tracker.get_raw_portfolio_features(current_price);

    // Verify: Features array has expected structure [value, position, spread]
    assert_eq!(
        features.len(),
        3,
        "Portfolio features should have 3 elements"
    );
    assert_eq!(
        features[0], 10_000.0,
        "Portfolio value should equal cash when no position"
    );
    assert_eq!(features[1], 0.0, "Position size should be 0 initially");
    assert_eq!(features[2], 0.0001, "Spread should match initialization");

    // Test with active position (long)
    let mut tracker_long = PortfolioTracker::new(10_000.0, 0.0001, 0.0);
    tracker_long.execute_action(buy_action(), 100.0, 10.0);
    let features_long = tracker_long.get_raw_portfolio_features(110.0);

    assert_approx_eq(
        features_long[0],
        10_100.0,
        "Portfolio value = cash + position_value = 9000 + (10*110) = 10100"
    );
    assert_eq!(
        features_long[1], 10.0,
        "Position size should be 10.0 (long)"
    );

    // Test with active position (short)
    let mut tracker_short = PortfolioTracker::new(10_000.0, 0.0001, 0.0);
    tracker_short.execute_action(sell_action(), 100.0, 10.0);
    let features_short = tracker_short.get_raw_portfolio_features(90.0);

    assert_approx_eq(
        features_short[0],
        10_100.0,
        "Portfolio value = cash + position_value = 11000 + (-10*90) = 10100"
    );
    assert_eq!(
        features_short[1], -10.0,
        "Position size should be -10.0 (short)"
    );

    Ok(())
}

// ============================================================================
// Test 2: Portfolio Features Dimension in TradingState
// ============================================================================

/// Test that TradingState correctly includes portfolio features in its dimension
/// calculation. The state should have 128 dimensions, not 125 (after adding
/// 3 portfolio features to the existing 125 features).
#[test]
fn test_portfolio_features_dimension() -> anyhow::Result<()> {
    // Setup: Create TradingState with portfolio features
    let state = TradingState::from_normalized(
        vec![0.0; 16], // 16 price features
        vec![0.0; 16], // 16 technical indicators
        vec![0.0; 16], // 16 market features
        vec![0.0; 3],  // 3 portfolio features (value, position, spread, vec![])
        vec![],        // 0 regime features (legacy test)
    );

    // Verify: Dimension should be 16 + 16 + 16 + 3 = 51
    assert_eq!(
        state.dimension(),
        51,
        "State dimension should include all feature groups"
    );

    // Verify: to_vector() produces correct length
    let vec = state.to_vector();
    assert_eq!(
        vec.len(),
        51,
        "State vector should have 51 elements (16+16+16+3)"
    );

    // Verify: Portfolio features are at the end of the vector
    assert_eq!(
        vec[48], 0.0,
        "Portfolio feature 0 (value) should be at index 48"
    );
    assert_eq!(
        vec[49], 0.0,
        "Portfolio feature 1 (position) should be at index 49"
    );
    assert_eq!(
        vec[50], 0.0,
        "Portfolio feature 2 (spread) should be at index 50"
    );

    Ok(())
}

// ============================================================================
// Test 3: P&L Reward Non-Zero for Profitable Trades
// ============================================================================

/// Test that P&L rewards are calculated correctly for profitable trades.
/// This verifies Bug #2 fix (portfolio features populated).
#[test]
fn test_pnl_reward_nonzero() -> anyhow::Result<()> {
    // Setup: Create reward function with default config
    let config = RewardConfig::default();
    let mut reward_fn = RewardFunction::new(config)?;

    // Create current state (no position)
    let current_state = TradingState::from_normalized(
        vec![0.0; 16],
        vec![0.0; 16],
        vec![
            0.001, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
        ], // spread at index 0
        vec![1.0, 0.0, 0.0001], // portfolio: normalized value=1.0 (10000, vec![]), position=0, spread=0.0001
        vec![], // 0 regime features (legacy test)
    );

    // Create next state (profitable BUY position)
    // Portfolio value increased from 10000 to 10100 (1% gain)
    let next_state = TradingState::from_normalized(
        vec![0.01; 16], // log return = 0.01
        vec![0.0; 16],
        vec![
            0.001, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
        ],
        vec![1.01, 0.1, 0.0001], // portfolio: value=1.01 (10100), position=0.1 (10/100 normalized), spread=0.0001
        vec![], // 0 regime features (legacy test)
    );

    // Execute: Calculate reward for BUY action
    // Provide diverse recent actions to avoid diversity penalty (entropy threshold = 0.5)
    let recent_actions = vec![buy_action(), hold_action(), sell_action()];
    let reward =
        reward_fn.calculate_reward(buy_action(), &current_state, &next_state, &recent_actions)?;

    // Verify: Reward should be positive for profitable trade
    // Note: Reward is clamped to [-1.0, 1.0] range
    let reward_f64: f64 = reward.try_into().unwrap();
    assert!(
        reward_f64 > 0.0,
        "Reward should be positive for profitable BUY trade, got {}",
        reward_f64
    );

    // Test losing trade
    let next_state_loss = TradingState::from_normalized(
        vec![-0.01; 16], // log return = -0.01
        vec![0.0; 16],
        vec![
            0.001, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
        ],
        vec![0.99, 0.1, 0.0001], // portfolio: value=0.99 (9900), position=10, spread=0.0001
        vec![], // 0 regime features (legacy test)
    );

    let reward_loss = reward_fn.calculate_reward(
        buy_action(),
        &current_state,
        &next_state_loss,
        &recent_actions,
    )?;

    // Verify: Reward should be negative for losing trade
    let reward_loss_f64: f64 = reward_loss.try_into().unwrap();
    assert!(
        reward_loss_f64 < 0.0,
        "Reward should be negative for losing BUY trade, got {}",
        reward_loss_f64
    );

    Ok(())
}

// ============================================================================
// Test 4: P&L Calculation Accuracy
// ============================================================================

/// Test that P&L rewards accurately reflect the magnitude of profit/loss.
/// This verifies the normalization by initial_capital (Bug #4 fix).
#[test]
fn test_pnl_calculation_accuracy() -> anyhow::Result<()> {
    let config = RewardConfig::default();
    let mut reward_fn = RewardFunction::new(config)?;

    // Test case 1: Small profit (1%)
    let current_state = TradingState::from_normalized(
        vec![0.0; 16],
        vec![0.0; 16],
        vec![
            0.001, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
        ],
        vec![1.0, 0.0, 0.0001],
        vec![], // 0 regime features (legacy test)
    );
    let next_state_1pct = TradingState::from_normalized(
        vec![0.01; 16],
        vec![0.0; 16],
        vec![
            0.001, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
        ],
        vec![1.01, 0.1, 0.0001], // 1% gain
        vec![], // 0 regime features (legacy test)
    );

    let recent_actions_diverse = vec![buy_action(), hold_action(), sell_action()];
    let reward_1pct = reward_fn.calculate_reward(
        buy_action(),
        &current_state,
        &next_state_1pct,
        &recent_actions_diverse,
    )?;

    // Test case 2: Large profit (5%)
    let next_state_5pct = TradingState::from_normalized(
        vec![0.05; 16],
        vec![0.0; 16],
        vec![
            0.001, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
        ],
        vec![1.05, 0.1, 0.0001], // 5% gain
        vec![], // 0 regime features (legacy test)
    );

    let reward_5pct = reward_fn.calculate_reward(
        buy_action(),
        &current_state,
        &next_state_5pct,
        &recent_actions_diverse,
    )?;

    // Verify: Larger profit should yield larger reward
    let r1: f64 = reward_1pct.try_into().unwrap();
    let r5: f64 = reward_5pct.try_into().unwrap();
    assert!(
        r5 > r1,
        "5% profit reward ({}) should be greater than 1% profit reward ({})",
        r5,
        r1
    );

    Ok(())
}

// ============================================================================
// Test 5: Portfolio Tracking Across BUY Actions
// ============================================================================

/// Test that portfolio state updates correctly after BUY actions
#[test]
fn test_portfolio_tracking_buy_action() -> anyhow::Result<()> {
    let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 0.0);

    // Initial state
    let features_init = tracker.get_raw_portfolio_features(100.0);
    assert_eq!(features_init[0], 10_000.0, "Initial portfolio value");
    assert_eq!(features_init[1], 0.0, "Initial position");

    // Execute BUY action
    tracker.execute_action(buy_action(), 100.0, 10.0);
    let features_after_buy = tracker.get_raw_portfolio_features(100.0);

    // Verify: Position opened, cash reduced
    assert_eq!(
        features_after_buy[1], 10.0,
        "Position size should be 10.0 after BUY"
    );
    assert_approx_eq(
        features_after_buy[0],
        10_000.0,
        "Portfolio value = cash + position_value = 9000 + (10*100) = 10000"
    );

    // Price increases to 110
    let features_profit = tracker.get_raw_portfolio_features(110.0);
    assert_approx_eq(
        features_profit[0],
        10_100.0,
        "Portfolio value = cash + position_value = 9000 + (10*110) = 10100"
    );

    Ok(())
}

// ============================================================================
// Test 6: Portfolio Tracking Across SELL Actions
// ============================================================================

/// Test that portfolio state updates correctly after SELL actions
#[test]
fn test_portfolio_tracking_sell_action() -> anyhow::Result<()> {
    let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 0.0);

    // Execute SELL action (open short)
    tracker.execute_action(sell_action(), 100.0, 10.0);
    let features_after_sell = tracker.get_raw_portfolio_features(100.0);

    // Verify: Short position opened, cash increased
    assert_eq!(
        features_after_sell[1], -10.0,
        "Position size should be -10.0 after SELL"
    );
    assert_approx_eq(
        features_after_sell[0],
        10_000.0,
        "Portfolio value = cash + position_value = 11000 + (-10*100) = 10000"
    );

    // Price decreases to 90 (profitable for short)
    let features_profit = tracker.get_raw_portfolio_features(90.0);
    assert_approx_eq(
        features_profit[0],
        10_100.0,
        "Portfolio value = cash + position_value = 11000 + (-10*90) = 10100"
    );

    Ok(())
}

// ============================================================================
// Test 7: Portfolio Tracking Across HOLD Actions
// ============================================================================

/// Test that portfolio state remains unchanged after HOLD actions
#[test]
fn test_portfolio_tracking_hold_action() -> anyhow::Result<()> {
    let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 0.0);

    // Execute BUY to create a position
    tracker.execute_action(buy_action(), 100.0, 10.0);
    let _features_after_buy = tracker.get_raw_portfolio_features(100.0);

    // Execute HOLD action (Flat exposure = close position)
    // Note: In FactoredAction system, HOLD means "target Flat exposure" = close all positions
    tracker.execute_action(hold_action(), 110.0, 10.0);
    let features_after_hold = tracker.get_raw_portfolio_features(100.0);

    // Verify: Position closed (Flat exposure)
    assert_eq!(
        features_after_hold[1], 0.0,
        "Position should be Flat (0) after HOLD action"
    );

    Ok(())
}

// ============================================================================
// Test 8: Edge Case - Zero Position
// ============================================================================

/// Test portfolio features when position size is zero
#[test]
fn test_edge_case_zero_position() -> anyhow::Result<()> {
    let tracker = PortfolioTracker::new(10_000.0, 0.0001, 0.0);
    let features = tracker.get_raw_portfolio_features(100.0);

    assert_eq!(features[1], 0.0, "Position should be zero initially");
    assert_eq!(
        features[0], 10_000.0,
        "Portfolio value should equal cash when no position"
    );

    Ok(())
}

// ============================================================================
// Test 9: Edge Case - Negative P&L
// ============================================================================

/// Test that negative P&L is correctly reflected in portfolio value
#[test]
fn test_edge_case_negative_pnl() -> anyhow::Result<()> {
    let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 0.0);

    // Open long position at 100
    tracker.execute_action(buy_action(), 100.0, 10.0);

    // Price drops to 90 (10 point loss per unit)
    let features_loss = tracker.get_raw_portfolio_features(90.0);

    // Portfolio value = cash + position_value = 9000 + (10*90) = 9900
    // Unrealized P&L = 9900 - 10000 = -100 (loss)
    assert_approx_eq(
        features_loss[0],
        9_900.0,
        "Portfolio value = 9000 + (10*90) = 9900"
    );

    Ok(())
}

// ============================================================================
// Test 10: Edge Case - Large Positions
// ============================================================================

/// Test portfolio features with large position sizes
#[test]
fn test_edge_case_large_positions() -> anyhow::Result<()> {
    let mut tracker = PortfolioTracker::new(100_000.0, 0.0001, 0.0);

    // Open large long position
    tracker.execute_action(buy_action(), 100.0, 100.0);
    let features = tracker.get_raw_portfolio_features(101.0);

    // Portfolio value = cash + position_value = 90000 + (100*101) = 100100
    assert_eq!(features[1], 100.0, "Position size should be 100.0 units");
    assert_approx_eq(
        features[0],
        100_100.0,
        "Portfolio value = 90000 + (100*101) = 100100"
    );

    Ok(())
}

// ============================================================================
// Test 11: Reward Function Receives Portfolio Data
// ============================================================================

/// Test that reward function correctly receives and uses portfolio features
/// from TradingState
#[test]
fn test_reward_function_receives_portfolio() -> anyhow::Result<()> {
    let config = RewardConfig::default();
    let mut reward_fn = RewardFunction::new(config)?;

    // Create states with different portfolio values
    let state_low = TradingState::from_normalized(
        vec![0.0; 16],
        vec![0.0; 16],
        vec![
            0.001, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
        ],
        vec![0.9, 0.1, 0.0001], // Portfolio value = 0.9 (9000), position normalized (10/100)
        vec![], // 0 regime features (legacy test)
    );

    let state_high = TradingState::from_normalized(
        vec![0.0; 16],
        vec![0.0; 16],
        vec![
            0.001, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
        ],
        vec![1.1, 0.1, 0.0001], // Portfolio value = 1.1 (11000)
        vec![], // 0 regime features (legacy test)
    );

    // Calculate reward for portfolio value increase
    let recent_actions_diverse = vec![buy_action(), hold_action(), sell_action()];
    let reward = reward_fn.calculate_reward(
        buy_action(),
        &state_low,
        &state_high,
        &recent_actions_diverse,
    )?;

    // Verify: Reward should be positive (portfolio value increased)
    let reward_f64: f64 = reward.try_into().unwrap();
    assert!(
        reward_f64 > 0.0,
        "Reward should be positive when portfolio value increases, got {}",
        reward_f64
    );

    Ok(())
}

// ============================================================================
// Test 12: Integration - Portfolio Tracking Through Full Trade Cycle
// ============================================================================

/// Test portfolio tracking through a complete trade cycle:
/// Flat -> Long -> Flat -> Short -> Flat
#[test]
fn test_integration_full_trade_cycle() -> anyhow::Result<()> {
    let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 0.0);

    // 1. Initial state (flat)
    let features_flat1 = tracker.get_raw_portfolio_features(100.0);
    assert_eq!(features_flat1[1], 0.0, "Should start flat");
    assert_eq!(features_flat1[0], 10_000.0, "Initial capital");

    // 2. Open long position
    tracker.execute_action(buy_action(), 100.0, 10.0);
    let features_long = tracker.get_raw_portfolio_features(110.0);
    assert_eq!(features_long[1], 10.0, "Should be long 10 units");
    assert_approx_eq(
        features_long[0],
        10_100.0,
        "Portfolio value = 9000 + (10*110) = 10100"
    );

    // 3. Close long position (HOLD = Flat exposure)
    tracker.execute_action(hold_action(), 110.0, 10.0);
    let features_flat2 = tracker.get_raw_portfolio_features(110.0);
    assert_eq!(features_flat2[1], 0.0, "Should be flat after close");
    assert_approx_eq(
        features_flat2[0],
        10_100.0,
        "Cash should reflect realized profit"
    );

    // 4. Open short position
    tracker.execute_action(sell_action(), 110.0, 10.0);
    let features_short = tracker.get_raw_portfolio_features(100.0);
    assert_eq!(features_short[1], -10.0, "Should be short 10 units");
    // Cash after long close: 10100, then open short: +1100, so cash = 11200
    // Portfolio = 11200 + (-10 * 100) = 10200
    assert_approx_eq(
        features_short[0],
        10_200.0,
        "Portfolio value = 11200 + (-10*100) = 10200"
    );

    // 5. Close short position (HOLD = Flat exposure)
    tracker.execute_action(hold_action(), 100.0, 10.0);
    let features_flat3 = tracker.get_raw_portfolio_features(100.0);
    assert_eq!(features_flat3[1], 0.0, "Should be flat after close");
    assert_approx_eq(
        features_flat3[0],
        10_200.0,
        "Cash should reflect all realized profits"
    );

    Ok(())
}

// ============================================================================
// Test 13: Integration - Batch Reward Calculation
// ============================================================================

/// Test batch reward calculation with portfolio features
#[test]
fn test_integration_batch_rewards() -> anyhow::Result<()> {
    use ml::dqn::reward::calculate_batch_rewards;

    let config = RewardConfig::default();
    let mut reward_fn = RewardFunction::new(config)?;

    // Create batch of state transitions
    let actions = vec![buy_action(), hold_action(), sell_action()];

    let current_states = vec![
        TradingState::from_normalized(
            vec![0.0; 16],
            vec![0.0; 16],
            vec![
                0.001, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
            ],
            vec![1.0, 0.0, 0.0001],
            vec![], // 0 regime features (legacy test)
        ),
        TradingState::from_normalized(
            vec![0.0; 16],
            vec![0.0; 16],
            vec![
                0.001, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
            ],
            vec![1.0, 0.1, 0.0001],
            vec![], // 0 regime features (legacy test)
        ),
        TradingState::from_normalized(
            vec![0.0; 16],
            vec![0.0; 16],
            vec![
                0.001, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
            ],
            vec![1.0, 0.1, 0.0001],
            vec![], // 0 regime features (legacy test)
        ),
    ];

    let next_states = vec![
        TradingState::from_normalized(
            vec![0.01; 16],
            vec![0.0; 16],
            vec![
                0.001, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
            ],
            vec![1.01, 0.1, 0.0001],
            vec![], // 0 regime features (legacy test)
        ),
        TradingState::from_normalized(
            vec![0.005; 16],
            vec![0.0; 16],
            vec![
                0.001, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
            ],
            vec![1.005, 0.1, 0.0001],
            vec![], // 0 regime features (legacy test)
        ),
        TradingState::from_normalized(
            vec![-0.01; 16],
            vec![0.0; 16],
            vec![
                0.001, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
            ],
            vec![0.99, 0.0, 0.0001],
            vec![], // 0 regime features (legacy test)
        ),
    ];

    let recent_actions = vec![buy_action(), hold_action(), sell_action()];

    // Calculate batch rewards
    let rewards = calculate_batch_rewards(
        &mut reward_fn,
        &actions,
        &current_states,
        &next_states,
        &recent_actions,
    )?;

    // Verify: Should have 3 rewards
    assert_eq!(rewards.len(), 3, "Should have 3 rewards for batch of 3");

    // Verify: Rewards should be non-zero (portfolio features used)
    for (i, reward) in rewards.iter().enumerate() {
        let r: f64 = (*reward).try_into().unwrap();
        info!(index = i, reward = r, "Reward calculated");
        // Note: HOLD action might have small rewards due to hold_reward config
        // We just verify they're calculated (not NaN)
        assert!(!r.is_nan(), "Reward {} should not be NaN", i);
    }

    Ok(())
}

// ============================================================================
// Test 14: Edge Case - Portfolio Value Near Zero
// ============================================================================

/// Test edge case where portfolio value approaches zero (large losses)
#[test]
fn test_edge_case_portfolio_near_zero() -> anyhow::Result<()> {
    let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 0.0);

    // Open large position
    tracker.execute_action(buy_action(), 100.0, 100.0);

    // Catastrophic price drop (90% loss)
    let features_crash = tracker.get_raw_portfolio_features(10.0);

    // Expected: cash=0, unrealized loss=-9000, total=-9000
    // (This represents a margin call scenario in real trading)
    assert!(
        features_crash[0] < 10_000.0,
        "Portfolio value should drop significantly"
    );

    Ok(())
}

// ============================================================================
// Test 15: Transaction Cost Tolerance Regression Test (Wave 7.1)
// ============================================================================

/// Regression test to verify transaction cost tolerance is working correctly
/// This test documents the expected behavior where portfolio values have
/// small discrepancies due to realistic transaction costs (0.05-0.15%)
#[test]
fn test_transaction_cost_tolerance() -> anyhow::Result<()> {
    // Setup: Create tracker with 0.01% spread (10 basis points)
    let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 0.0);

    // Test 1: Buy 10 units at $100
    tracker.execute_action(buy_action(), 100.0, 10.0);
    let features_buy = tracker.get_raw_portfolio_features(100.0);

    // Expected: Portfolio value = $10,000 (cash + position value)
    // Actual: Slightly less due to transaction costs
    // Tolerance: 0.2% (20 basis points) to account for fees
    assert_approx_eq(
        features_buy[0],
        10_000.0,
        "Portfolio value after buy (with transaction costs)"
    );

    // Test 2: Verify transaction costs are within expected range
    let actual_value = features_buy[0];
    let expected_value = 10_000.0;
    let cost_percentage = ((expected_value - actual_value) / expected_value * 100.0).abs();

    assert!(
        cost_percentage <= 0.2,
        "Transaction costs should be within 0.2% (20 bps), got {:.4}%",
        cost_percentage
    );

    // Test 3: Verify transaction costs are not zero (realistic costs applied)
    assert!(
        (actual_value - expected_value).abs() > 0.01,
        "Transaction costs should be applied (value difference > $0.01)"
    );

    // Test 4: Multiple trades compound transaction costs
    tracker.execute_action(hold_action(), 110.0, 10.0); // Close long at profit
    tracker.execute_action(sell_action(), 110.0, 10.0); // Open short
    tracker.execute_action(hold_action(), 100.0, 10.0); // Close short at profit

    let features_final = tracker.get_raw_portfolio_features(100.0);

    // Expected: ~$10,200 (profit from both trades)
    // Actual: Slightly less due to accumulated transaction costs from 4 trades
    assert_approx_eq(
        features_final[0],
        10_200.0,
        "Final portfolio value after multiple trades (with costs)"
    );

    Ok(())
}

// ============================================================================
// Test 16: Reward Calculation Consistency
// ============================================================================

/// Test that reward calculations are consistent and deterministic
#[test]
fn test_reward_calculation_consistency() -> anyhow::Result<()> {
    let config = RewardConfig::default();

    // Calculate same reward twice
    let mut reward_fn1 = RewardFunction::new(config.clone())?;
    let mut reward_fn2 = RewardFunction::new(config)?;

    let current_state = TradingState::from_normalized(
        vec![0.0; 16],
        vec![0.0; 16],
        vec![
            0.001, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
        ],
        vec![1.0, 0.0, 0.0001],
        vec![], // 0 regime features (legacy test)
    );

    let next_state = TradingState::from_normalized(
        vec![0.01; 16],
        vec![0.0; 16],
        vec![
            0.001, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
        ],
        vec![1.01, 0.1, 0.0001],
        vec![], // 0 regime features (legacy test)
    );

    let recent_actions = vec![buy_action(), hold_action(), sell_action()];

    let reward1 =
        reward_fn1.calculate_reward(buy_action(), &current_state, &next_state, &recent_actions)?;

    let reward2 =
        reward_fn2.calculate_reward(buy_action(), &current_state, &next_state, &recent_actions)?;

    // Verify: Same inputs should produce same reward
    assert_eq!(
        reward1, reward2,
        "Reward calculation should be deterministic"
    );

    Ok(())
}
