#![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,
)]
//! PPO 45-Action Network Tests (TDD Red Phase)
//!
//! Tests for PPO network architecture expansion from 3 -> 45 actions.
//!
//! Test coverage:
//! 1. Policy network outputs 45 logits (not 3)
//! 2. Value network still outputs 1 value (unchanged)
//! 3. Softmax over 45 logits sums to 1.0
//! 4. Full forward pass with 45 actions works correctly
//!
//! Expected behavior (TDD Red -> Green):
//! - RED: Tests fail because num_actions=3 (current implementation)
//! - GREEN: Tests pass after num_actions=45 changes

use anyhow::Result;
// candle eliminated -- test uses native GPU APIs (cudarc + cuda_nn)

#[test]
fn test_policy_network_45_output() -> Result<()> {
    use ml::ppo::ppo::{PPOConfig, PPO};

    // Create PPO with 45 actions
    let config = PPOConfig {
        num_actions: 45, // 5x3x3 factored action space
        state_dim: 54,   // Wave 3 features (54->54)
        ..Default::default()
    };

    let ppo = PPO::new(config)?;

    // Create dummy state (flat f32 slice, batch_size=1)
    let state = vec![0.0_f32; 54];

    // Forward pass through policy network (returns Vec<f32> of logits)
    let logits = ppo.actor.forward_host(&state, 1)?;

    // ASSERT: Policy network outputs 45 logits (not 3)
    assert_eq!(
        logits.len(),
        45,
        "Policy network should output 45 logits, got: {}",
        logits.len()
    );

    Ok(())
}

#[test]
fn test_value_network_single_output() -> Result<()> {
    use ml::ppo::ppo::{PPOConfig, PPO};

    // Create PPO with 45 actions
    let config = PPOConfig {
        num_actions: 45, // 5x3x3 factored action space
        state_dim: 54,   // Wave 3 features (54->54)
        ..Default::default()
    };

    let ppo = PPO::new(config)?;

    // Create dummy state (flat f32 slice, batch_size=1)
    let state = vec![0.0_f32; 54];

    // Forward pass through value network (returns Vec<f32>)
    let value = ppo.critic.forward_host(&state, 1)?;

    // ASSERT: Value network still outputs 1 value (batch_size=1, scalar output)
    assert_eq!(
        value.len(),
        1,
        "Value network should output single value per batch item, got: {}",
        value.len()
    );

    Ok(())
}

#[test]
fn test_policy_network_softmax() -> Result<()> {
    use ml::ppo::ppo::{PPOConfig, PPO};

    // Create PPO with 45 actions
    let config = PPOConfig {
        num_actions: 45,
        state_dim: 54,
        ..Default::default()
    };

    let ppo = PPO::new(config)?;

    // Create dummy state (flat f32 slice, batch_size=1)
    let state = vec![0.0_f32; 54];

    // Get action probabilities (softmax of logits) -- returns Vec<f32>
    let probs = match &ppo.actor {
        ml::ppo::ppo::ActorNetwork::MLP(net) => net.action_probabilities(&state, 1)?,
        ml::ppo::ppo::ActorNetwork::LSTM(_) => {
            panic!("Expected MLP actor, got LSTM");
        }
    };

    // ASSERT 1: Probabilities have 45 elements
    assert_eq!(
        probs.len(),
        45,
        "Action probabilities should have 45 dimensions, got: {}",
        probs.len()
    );

    // ASSERT 2: Probabilities sum to 1.0 (softmax property)
    let sum: f32 = probs.iter().sum();
    assert!(
        (sum - 1.0).abs() < 1e-5,
        "Softmax probabilities should sum to 1.0, got: {}",
        sum
    );

    // ASSERT 3: Each probability is in [0, 1]
    for (i, &prob) in probs.iter().enumerate() {
        assert!(
            prob >= 0.0 && prob <= 1.0,
            "Probability {} is out of range [0, 1]: {}",
            i,
            prob
        );
    }

    Ok(())
}

#[test]
fn test_network_forward_pass() -> Result<()> {
    use ml::ppo::ppo::{PPOConfig, PPO};

    // Create PPO with 45 actions
    let config = PPOConfig {
        num_actions: 45,
        state_dim: 54,
        policy_hidden_dims: vec![128, 64],
        value_hidden_dims: vec![256, 128, 64],
        ..Default::default()
    };

    let ppo = PPO::new(config)?;

    // Create batch of states (batch_size=8, flat f32 slice)
    let batch_size = 8;
    let state = vec![0.0_f32; batch_size * 54];

    // ASSERT 1: Policy forward pass with batch (returns batch_size * num_actions logits)
    let logits = ppo.actor.forward_host(&state, batch_size)?;
    assert_eq!(
        logits.len(),
        batch_size * 45,
        "Policy logits should have {} elements (batch_size * 45), got: {}",
        batch_size * 45,
        logits.len()
    );

    // ASSERT 2: Value forward pass with batch (returns batch_size values)
    let values = ppo.critic.forward_host(&state, batch_size)?;
    assert_eq!(
        values.len(),
        batch_size,
        "Value network should output {} values, got: {}",
        batch_size,
        values.len()
    );

    // ASSERT 3: Action probabilities with batch
    let probs = match &ppo.actor {
        ml::ppo::ppo::ActorNetwork::MLP(net) => net.action_probabilities(&state, batch_size)?,
        ml::ppo::ppo::ActorNetwork::LSTM(_) => {
            panic!("Expected MLP actor, got LSTM");
        }
    };
    assert_eq!(
        probs.len(),
        batch_size * 45,
        "Action probabilities should have {} elements, got: {}",
        batch_size * 45,
        probs.len()
    );

    // ASSERT 4: Each batch item's probabilities sum to 1.0
    for i in 0..batch_size {
        let row_start = i * 45;
        let row_end = row_start + 45;
        let row = &probs[row_start..row_end];
        let sum: f32 = row.iter().sum();
        assert!(
            (sum - 1.0).abs() < 1e-5,
            "Batch item {} probabilities should sum to 1.0, got: {}",
            i,
            sum
        );
    }

    Ok(())
}

#[test]
fn test_backward_compatibility_3_actions() -> Result<()> {
    use ml::ppo::ppo::{PPOConfig, PPO};

    // Test that 3-action configuration still works (backward compatibility)
    let config = PPOConfig {
        num_actions: 3,
        state_dim: 54,
        ..Default::default()
    };

    let ppo = PPO::new(config)?;

    // Create dummy state (flat f32 slice, batch_size=1)
    let state = vec![0.0_f32; 54];

    // Policy network should output 3 logits
    let logits = ppo.actor.forward_host(&state, 1)?;
    assert_eq!(
        logits.len(),
        3,
        "Policy network with num_actions=3 should output 3 logits"
    );

    // Value network should still output 1 value
    let value = ppo.critic.forward_host(&state, 1)?;
    assert_eq!(
        value.len(),
        1,
        "Value network should output single value"
    );

    Ok(())
}

#[test]
fn test_ppo_config_default_45_actions() -> Result<()> {
    use ml::ppo::ppo::PPOConfig;

    // ASSERT: Default config uses 45 actions (not 3)
    let config = PPOConfig::default();
    assert_eq!(
        config.num_actions, 45,
        "Default PPOConfig should have num_actions=45, got: {}",
        config.num_actions
    );

    Ok(())
}

#[test]
fn test_ppo_trainer_default_45_actions() -> Result<()> {
    use ml::trainers::ppo::PpoHyperparameters;

    // ASSERT: Conservative params use 45 actions
    let params = PpoHyperparameters::conservative();
    let config: ml::ppo::ppo::PPOConfig = params.into();

    assert_eq!(
        config.num_actions, 45,
        "PpoHyperparameters should default to 45 actions, got: {}",
        config.num_actions
    );

    Ok(())
}

#[test]
fn test_hyperopt_adapter_default_45_actions() -> Result<()> {
    use ml::hyperopt::adapters::ppo::PPOParams;

    // ASSERT: PPOParams default uses 45 actions when converted to config
    let params = PPOParams::default();

    // Create config manually (since PPOParams -> PPOConfig conversion is in PPOTrainer)
    // We verify the architecture matches 45-action space
    use ml::ppo::ppo::PPOConfig;
    use ml::ppo::gae::GAEConfig;

    let config = PPOConfig {
        state_dim: 54,
        num_actions: 45, // This is what hyperopt adapter should use
        policy_hidden_dims: vec![128, 64],
        value_hidden_dims: vec![512, 384, 256, 128, 64],
        policy_learning_rate: params.policy_learning_rate,
        value_learning_rate: params.value_learning_rate,
        clip_epsilon: params.clip_epsilon as f32,
        value_loss_coeff: params.value_loss_coeff as f32,
        entropy_coeff: params.entropy_coeff as f32,
        gae_config: GAEConfig::default(),
        batch_size: 2048,
        mini_batch_size: 512,
        num_epochs: 20,
        max_grad_norm: 0.5,
        early_stopping_enabled: true,
        early_stopping_patience: 5,
        early_stopping_min_delta: 1e-4,
        early_stopping_min_epochs: 5,
        max_position_absolute: 2.0,
        transaction_cost_bps: 0.10,
        cash_reserve_pct: 20.0,
        circuit_breaker_threshold: 5,
        use_lstm: false,
        lstm_hidden_dim: 128,
        lstm_num_layers: 1,
        lstm_sequence_length: 32,
        accumulation_steps: 1,
        clip_epsilon_high: None,
        use_symlog: true,
        use_adaptive_entropy: true,
        use_percentile_scaling: true,
    };

    assert_eq!(
        config.num_actions, 45,
        "Hyperopt adapter config should use 45 actions"
    );

    Ok(())
}
