#![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,
)]
//! Recurrent PPO Integration Tests
//!
//! This module provides comprehensive integration tests for LSTM-enhanced PPO:
//! 1. Single episode training with LSTM
//! 2. Hidden state continuity across timesteps
//! 3. Episode boundary handling (hidden state reset)
//! 4. Recurrent vs feedforward comparison
//! 5. Checkpointing with hidden states
//!
//! These tests verify end-to-end functionality of the recurrent PPO system.

#![allow(unused_crate_dependencies)]

// candle eliminated — test uses native APIs
use ml::dqn::{FactoredAction, ExposureLevel, OrderType, Urgency};
use tracing::info;
use ml::ppo::{
    ppo::{PPOConfig, PPO},
    trajectories::{Trajectory, TrajectoryBatch, TrajectoryStep},
    gae::GAEConfig,
};
use std::path::PathBuf;

// ============================================================================
// Test 1: Single Episode Training with LSTM
// ============================================================================

#[test]
fn test_recurrent_ppo_single_episode() {
    // Test that LSTM-enhanced PPO can train on a single episode
    let config = PPOConfig {
        state_dim: 32,
        num_actions: 45,
        policy_hidden_dims: vec![64],
        value_hidden_dims: vec![64],
        use_lstm: true,
        lstm_hidden_dim: 128,
        lstm_num_layers: 1,
        batch_size: 16,
        mini_batch_size: 8,
        num_epochs: 3,
        policy_learning_rate: 1e-4,
        value_learning_rate: 1e-3,
        clip_epsilon: 0.2,
        value_loss_coeff: 1.0,
        entropy_coeff: 0.01,
        gae_config: GAEConfig {
            gamma: 0.99,
            lambda: 0.95,
            normalize_advantages: true,
        },
        max_grad_norm: 0.5,
        early_stopping_enabled: false,
        early_stopping_patience: 10,
        early_stopping_min_delta: 1e-4,
        early_stopping_min_epochs: 20,
        max_position_absolute: 2.0,
        transaction_cost_bps: 0.10,
        cash_reserve_pct: 20.0,
        circuit_breaker_threshold: 5,
        lstm_sequence_length: 32,
        accumulation_steps: 1,
        clip_epsilon_high: None,
        use_symlog: true,
        use_adaptive_entropy: true,
        use_percentile_scaling: true,
    };

    let mut ppo = PPO::new(config.clone())
        .expect("Failed to create recurrent PPO");

    // Verify hidden state manager is initialized
    assert!(ppo.hidden_state_manager.is_some(), "Hidden state manager should be initialized when use_lstm=true");

    // Create a simple trajectory (10 timesteps)
    let mut trajectory = Trajectory::new();
    for t in 0..10 {
        let state = vec![t as f32; 32]; // Simple incrementing state
        let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal);
        let log_prob = -1.0;
        let value = 5.0 + t as f32;
        let reward = 1.0;
        let done = t == 9;

        trajectory.add_step(TrajectoryStep::new(state, action, log_prob, value, reward, done));
    }

    // Create batch with simple advantages and returns
    let advantages = vec![1.0; 10];
    let returns = vec![10.0; 10];
    let mut batch = TrajectoryBatch::from_trajectories(vec![trajectory], advantages, returns);

    // Normalize advantages
    batch.normalize_advantages().expect("Failed to normalize advantages");

    // Train for 1 epoch
    let (policy_loss, value_loss) = ppo.update(&mut batch)
        .expect("Training should complete without errors");

    // Verify losses are finite (not NaN or Inf)
    assert!(policy_loss.is_finite(), "Policy loss should be finite, got {}", policy_loss);
    assert!(value_loss.is_finite(), "Value loss should be finite, got {}", value_loss);

    info!(policy_loss, value_loss, "test_recurrent_ppo_single_episode passed");
}

// ============================================================================
// Test 2: Hidden State Continuity Across Timesteps
// ============================================================================

#[test]
fn test_recurrent_ppo_hidden_state_continuity() {
    // Test that hidden states persist and evolve across timesteps within an episode
    let config = PPOConfig {
        state_dim: 16,
        num_actions: 45,
        policy_hidden_dims: vec![32],
        value_hidden_dims: vec![32],
        use_lstm: true,
        lstm_hidden_dim: 64,
        lstm_num_layers: 1,
        batch_size: 8,
        mini_batch_size: 4,
        num_epochs: 2,
        policy_learning_rate: 1e-4,
        value_learning_rate: 1e-3,
        lstm_sequence_length: 32,
        ..PPOConfig::default()
    };

    let ppo = PPO::new(config.clone())
        .expect("Failed to create recurrent PPO");

    // Get hidden state manager
    let manager = ppo.hidden_state_manager.as_ref()
        .expect("Hidden state manager should exist");

    // Extract initial hidden states (should be zeros)
    let (h0_policy, _c0_policy) = manager.get_policy_state();
    let (h0_value, _c0_value) = manager.get_value_state();

    let h0_sum_policy: f32 = h0_policy.iter().sum();
    let h0_sum_value: f32 = h0_value.iter().sum();

    // Verify initial states are zeros
    assert_eq!(h0_sum_policy, 0.0, "Initial policy hidden state should be zeros");
    assert_eq!(h0_sum_value, 0.0, "Initial value hidden state should be zeros");

    info!("test_recurrent_ppo_hidden_state_continuity passed: States initialized correctly");
}

// ============================================================================
// Test 3: Episode Boundary Handling (Hidden State Reset)
// ============================================================================

#[test]
fn test_recurrent_ppo_episode_boundaries() {
    // Test that hidden states reset between episodes
    let config = PPOConfig {
        state_dim: 16,
        num_actions: 45,
        policy_hidden_dims: vec![32],
        value_hidden_dims: vec![32],
        use_lstm: true,
        lstm_hidden_dim: 64,
        lstm_num_layers: 1,
        batch_size: 8,
        mini_batch_size: 4,
        num_epochs: 2,
        lstm_sequence_length: 32,
        ..PPOConfig::default()
    };

    let mut ppo = PPO::new(config.clone())
        .expect("Failed to create recurrent PPO");

    // Create two episodes
    let mut episode1 = Trajectory::new();
    for t in 0..5 {
        episode1.add_step(TrajectoryStep::new(
            vec![1.0; 16],
            FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal),
            -1.0,
            5.0,
            1.0,
            t == 4, // Done at last step
        ));
    }

    let mut episode2 = Trajectory::new();
    for t in 0..5 {
        episode2.add_step(TrajectoryStep::new(
            vec![2.0; 16],
            FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal),
            -1.0,
            5.0,
            1.0,
            t == 4, // Done at last step
        ));
    }

    // Create batch from both episodes
    let advantages = vec![1.0; 10]; // 5 steps per episode × 2 episodes
    let returns = vec![10.0; 10];
    let mut batch = TrajectoryBatch::from_trajectories(
        vec![episode1, episode2],
        advantages,
        returns,
    );

    batch.normalize_advantages().expect("Failed to normalize advantages");

    // Train on batch (should handle episode boundaries)
    let result = ppo.update(&mut batch);
    assert!(result.is_ok(), "Training should complete without errors across episode boundaries");

    // Verify hidden state manager can reset states
    if let Some(ref mut manager) = ppo.hidden_state_manager {
        // Get actual batch size from hidden state manager
        let actual_batch_size = manager.batch_size();

        // Create done mask: all environments done (match actual batch size)
        let done_mask = vec![true; actual_batch_size];

        // Reset states
        manager.reset_on_done(&done_mask).expect("Reset should succeed");

        // Verify states are zeros after reset
        let (h_policy, _c_policy) = manager.get_policy_state();
        let h_sum: f32 = h_policy.iter().sum();
        assert_eq!(h_sum, 0.0, "Hidden states should be zeros after reset");
    }

    info!("test_recurrent_ppo_episode_boundaries passed: Episode boundaries handled correctly");
}

// ============================================================================
// Test 4: Recurrent vs Feedforward Comparison
// ============================================================================

#[test]
fn test_recurrent_vs_feedforward_ppo() {
    // Compare LSTM vs non-LSTM PPO training behavior
    let base_config = PPOConfig {
        state_dim: 16,
        num_actions: 45,
        policy_hidden_dims: vec![32],
        value_hidden_dims: vec![32],
        batch_size: 16,
        mini_batch_size: 8,
        num_epochs: 5,
        policy_learning_rate: 1e-4,
        value_learning_rate: 1e-3,
        lstm_sequence_length: 32,
        ..PPOConfig::default()
    };

    // Create feedforward PPO
    let mut feedforward_config = base_config.clone();
    feedforward_config.use_lstm = false;
    let mut ppo_feedforward = PPO::new(feedforward_config)
        .expect("Failed to create feedforward PPO");

    // Create recurrent PPO
    let mut recurrent_config = base_config.clone();
    recurrent_config.use_lstm = true;
    recurrent_config.lstm_hidden_dim = 128;
    recurrent_config.lstm_num_layers = 1;
    let mut ppo_recurrent = PPO::new(recurrent_config)
        .expect("Failed to create recurrent PPO");

    // Verify configurations
    assert!(ppo_feedforward.hidden_state_manager.is_none(), "Feedforward PPO should not have hidden state manager");
    assert!(ppo_recurrent.hidden_state_manager.is_some(), "Recurrent PPO should have hidden state manager");

    // Create identical trajectory for both
    let mut trajectory = Trajectory::new();
    for t in 0..10 {
        trajectory.add_step(TrajectoryStep::new(
            vec![t as f32; 16],
            FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal),
            -1.0,
            5.0,
            1.0,
            t == 9,
        ));
    }

    let advantages = vec![1.0; 10];
    let returns = vec![10.0; 10];

    // Train feedforward
    let mut batch_ff = TrajectoryBatch::from_trajectories(vec![trajectory.clone()], advantages.clone(), returns.clone());
    batch_ff.normalize_advantages().expect("Failed to normalize");
    let (ff_policy_loss, ff_value_loss) = ppo_feedforward.update(&mut batch_ff)
        .expect("Feedforward training failed");

    // Train recurrent
    let mut batch_rec = TrajectoryBatch::from_trajectories(vec![trajectory], advantages, returns);
    batch_rec.normalize_advantages().expect("Failed to normalize");
    let (rec_policy_loss, rec_value_loss) = ppo_recurrent.update(&mut batch_rec)
        .expect("Recurrent training failed");

    // Both should produce finite losses
    assert!(ff_policy_loss.is_finite(), "Feedforward policy loss should be finite");
    assert!(ff_value_loss.is_finite(), "Feedforward value loss should be finite");
    assert!(rec_policy_loss.is_finite(), "Recurrent policy loss should be finite");
    assert!(rec_value_loss.is_finite(), "Recurrent value loss should be finite");

    info!(ff_policy_loss, ff_value_loss, rec_policy_loss, rec_value_loss, "test_recurrent_vs_feedforward_ppo passed");
}

// ============================================================================
// Test 5: Checkpointing with Hidden States
// ============================================================================

#[test]
#[ignore = "LSTM checkpoint loading not yet implemented (requires from_varbuilder methods in lstm_networks.rs)"]
fn test_recurrent_ppo_checkpointing() {
    // Test that checkpoints can be saved and loaded with LSTM configuration
    // TODO: Implement from_varbuilder methods in LSTMPolicyNetwork and LSTMValueNetwork
    // to enable loading LSTM checkpoints from safetensors files
    let config = PPOConfig {
        state_dim: 16,
        num_actions: 45,
        policy_hidden_dims: vec![32],
        value_hidden_dims: vec![32],
        use_lstm: true,
        lstm_hidden_dim: 64,
        lstm_num_layers: 1,
        batch_size: 16,
        mini_batch_size: 8,
        num_epochs: 3,
        lstm_sequence_length: 32,
        ..PPOConfig::default()
    };

    let mut ppo = PPO::new(config.clone())
        .expect("Failed to create recurrent PPO");

    // Train for a few steps
    let mut trajectory = Trajectory::new();
    for t in 0..10 {
        trajectory.add_step(TrajectoryStep::new(
            vec![t as f32; 16],
            FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal),
            -1.0,
            5.0,
            1.0,
            t == 9,
        ));
    }

    let advantages = vec![1.0; 10];
    let returns = vec![10.0; 10];
    let mut batch = TrajectoryBatch::from_trajectories(vec![trajectory], advantages, returns);
    batch.normalize_advantages().expect("Failed to normalize");

    let (_loss_before, _) = ppo.update(&mut batch)
        .expect("Training failed");

    // Save checkpoint
    let checkpoint_dir = PathBuf::from("/tmp/ppo_recurrent_checkpoint_test");
    std::fs::create_dir_all(&checkpoint_dir).expect("Failed to create checkpoint dir");

    let checkpoint_path = checkpoint_dir.join("checkpoint");

    let result = ppo.save_checkpoint(&checkpoint_path);
    assert!(result.is_ok(), "Checkpoint save should succeed for recurrent PPO");

    // Verify checkpoint config file exists (save_checkpoint writes a .json sidecar)
    let config_path = checkpoint_path.with_extension("json");
    assert!(config_path.exists(), "Config file should exist");

    // Load checkpoint into new PPO instance
    let ppo_loaded = PPO::load_checkpoint(&checkpoint_path);
    assert!(ppo_loaded.is_ok(), "Checkpoint load should succeed for recurrent PPO");

    // Note: load_checkpoint does not restore training_steps from metadata.
    // This is expected behavior - training_steps start from 0 after loading.
    // The important part is that the model weights are restored correctly.

    // Clean up
    std::fs::remove_dir_all(&checkpoint_dir).ok();

    info!("test_recurrent_ppo_checkpointing passed: Checkpoints save/load correctly");
}
