#![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,
)]
//! TDD Tests for Gradient Accumulation (WAVE 26 P2.2)
//!
//! Gradient accumulation allows larger effective batch sizes by accumulating
//! gradients over multiple forward/backward passes before applying optimizer step.
//!
//! Benefits:
//! - Larger effective batch size without GPU memory limits
//! - More stable gradient estimates (reduced variance)
//! - Better convergence in low-data regimes
//!
//! Example: batch_size=64, accumulation_steps=4 -> effective_batch_size=256

use ml::trainers::dqn::DQNHyperparameters;
use ml::trainers::dqn::DQNTrainer;

/// Helper to create minimal hyperparameters for testing
fn create_test_hyperparams() -> DQNHyperparameters {
    let mut params = DQNHyperparameters::conservative();
    params.epochs = 2; // Minimal epochs for fast testing
    params.batch_size = 32;
    params.buffer_size = 1000;
    params.min_replay_size = 100;
    params.learning_rate = 0.001;
    params.gradient_accumulation_steps = 1; // Default: no accumulation
    params
}

/// TEST 1: Verify default configuration (no accumulation)
#[test]
fn test_default_accumulation_steps_is_one() {
    let params = create_test_hyperparams();
    assert_eq!(
        params.gradient_accumulation_steps, 1,
        "Default accumulation_steps should be 1 (no accumulation)"
    );
}

/// TEST 2: Verify accumulation_steps field exists in config
#[test]
fn test_accumulation_steps_field_in_config() {
    let mut params = create_test_hyperparams();
    params.gradient_accumulation_steps = 4;
    assert_eq!(
        params.gradient_accumulation_steps, 4,
        "Should be able to set gradient_accumulation_steps"
    );
}

/// TEST 3: Verify trainer accepts gradient_accumulation_steps config
#[test]
fn test_trainer_accepts_accumulation_config() {
    let mut params = create_test_hyperparams();
    params.gradient_accumulation_steps = 2;

    let trainer = DQNTrainer::new(params);
    assert!(
        trainer.is_ok(),
        "Trainer should accept gradient_accumulation_steps config"
    );
}

/// TEST 4: Verify gradient accumulation field is stored in trainer
#[test]
fn test_trainer_stores_accumulation_steps() {
    let mut params = create_test_hyperparams();
    params.gradient_accumulation_steps = 4;

    let trainer = DQNTrainer::new(params).expect("Failed to create trainer");

    // Trainer should store the accumulation_steps value
    // This will be accessible via trainer.hyperparams().gradient_accumulation_steps
    assert_eq!(
        trainer.hyperparams().gradient_accumulation_steps, 4,
        "Trainer should store accumulation_steps from config"
    );
}

/// TEST 5: Validate accumulation_steps > 0
#[test]
fn test_reject_zero_accumulation_steps() {
    let mut params = create_test_hyperparams();
    params.gradient_accumulation_steps = 0; // Invalid

    let trainer = DQNTrainer::new(params);
    assert!(
        trainer.is_err(),
        "Should reject gradient_accumulation_steps = 0"
    );

    let err_msg = trainer.unwrap_err().to_string();
    assert!(
        err_msg.contains("gradient_accumulation_steps") || err_msg.contains("greater than 0"),
        "Error message should mention gradient_accumulation_steps validation"
    );
}

/// TEST 6: Verify effective batch size calculation
#[test]
fn test_effective_batch_size_calculation() {
    let mut params = create_test_hyperparams();
    params.batch_size = 64;
    params.gradient_accumulation_steps = 4;

    let trainer = DQNTrainer::new(params).expect("Failed to create trainer");

    // Effective batch size = batch_size * accumulation_steps
    let expected_effective_batch = 64 * 4; // 256

    // This validates that the trainer understands the concept
    assert_eq!(
        trainer.hyperparams().batch_size * trainer.hyperparams().gradient_accumulation_steps,
        expected_effective_batch,
        "Effective batch size should be batch_size * accumulation_steps"
    );
}

/// TEST 7: Verify gradient accumulation doesn't exceed replay buffer size
#[test]
fn test_accumulation_bounded_by_replay_buffer() {
    let mut params = create_test_hyperparams();
    params.batch_size = 64;
    params.gradient_accumulation_steps = 4; // Effective batch = 256
    params.buffer_size = 1000; // Buffer has enough samples

    let trainer = DQNTrainer::new(params);
    assert!(
        trainer.is_ok(),
        "Should accept accumulation when buffer is large enough"
    );

    // But if effective batch exceeds buffer...
    let mut params2 = create_test_hyperparams();
    params2.batch_size = 64;
    params2.gradient_accumulation_steps = 100; // Effective batch = 6400 > buffer_size=1000
    params2.buffer_size = 1000;

    let trainer2 = DQNTrainer::new(params2);
    // Trainer creation should succeed (validation happens at runtime)
    assert!(
        trainer2.is_ok(),
        "Trainer creation should succeed (runtime will validate)"
    );
}

/// TEST 8: Verify gradient accumulation preserves loss scaling
/// Loss should be scaled by 1/accumulation_steps to maintain gradient magnitude
#[tokio::test]
async fn test_loss_scaling_during_accumulation() {
    let mut params = create_test_hyperparams();
    params.gradient_accumulation_steps = 4;
    params.batch_size = 32;
    params.min_replay_size = 50; // Low threshold for faster testing

    let trainer = DQNTrainer::new(params).expect("Failed to create trainer");

    // When training with accumulation, each mini-batch loss should be divided by accumulation_steps
    // This test verifies the concept exists (actual implementation will test functional behavior)

    // The scaled loss prevents gradient explosion when accumulating
    let accumulation_steps = trainer.hyperparams().gradient_accumulation_steps;
    let scale_factor = 1.0 / accumulation_steps as f64;

    assert!(
        scale_factor > 0.0 && scale_factor <= 1.0,
        "Loss scale factor should be in range (0, 1] for accumulation_steps >= 1"
    );
}

/// TEST 9: Verify optimizer step called once per accumulation cycle
/// With accumulation_steps=4, optimizer.step() should be called every 4 mini-batches
#[test]
fn test_optimizer_step_frequency() {
    let mut params = create_test_hyperparams();
    params.gradient_accumulation_steps = 4;

    let trainer = DQNTrainer::new(params).expect("Failed to create trainer");

    // Verify the trainer has the accumulation_steps configured
    assert_eq!(
        trainer.hyperparams().gradient_accumulation_steps, 4,
        "Trainer should have accumulation_steps=4"
    );

    // The implementation will ensure optimizer.step() is called after every 4 mini-batches
    // This test validates the configuration exists
}

/// TEST 10: Verify gradient accumulation resets after optimizer step
#[tokio::test]
async fn test_gradients_reset_after_optimizer_step() {
    let mut params = create_test_hyperparams();
    params.gradient_accumulation_steps = 2;
    params.batch_size = 32;

    let trainer = DQNTrainer::new(params).expect("Failed to create trainer");

    // After accumulating gradients and calling optimizer.step(),
    // gradients should be zeroed via optimizer.zero_grad()
    // This test verifies the configuration (implementation will test functional behavior)

    assert_eq!(
        trainer.hyperparams().gradient_accumulation_steps, 2,
        "Should use accumulation_steps=2"
    );
}

/// TEST 11: Verify accumulation works with various batch sizes
#[test]
fn test_accumulation_with_different_batch_sizes() {
    // Test small batch + accumulation
    let mut params1 = create_test_hyperparams();
    params1.batch_size = 16;
    params1.gradient_accumulation_steps = 8; // Effective: 128
    assert!(DQNTrainer::new(params1).is_ok());

    // Test medium batch + accumulation
    let mut params2 = create_test_hyperparams();
    params2.batch_size = 64;
    params2.gradient_accumulation_steps = 4; // Effective: 256
    assert!(DQNTrainer::new(params2).is_ok());

    // Test large batch + no accumulation
    let mut params3 = create_test_hyperparams();
    params3.batch_size = 128;
    params3.gradient_accumulation_steps = 1; // Effective: 128
    assert!(DQNTrainer::new(params3).is_ok());
}

/// TEST 12: Verify accumulation respects GPU memory limits
/// Even with accumulation, each mini-batch must fit in GPU memory
#[test]
fn test_accumulation_respects_gpu_memory_limit() {
    let mut params = create_test_hyperparams();
    params.batch_size = 250; // Exceeds GPU limit (230)
    params.gradient_accumulation_steps = 4;

    let trainer = DQNTrainer::new(params);
    assert!(
        trainer.is_err(),
        "Should reject batch_size > 230 even with accumulation"
    );

    let err_msg = trainer.unwrap_err().to_string();
    assert!(
        err_msg.contains("230") || err_msg.contains("GPU memory"),
        "Error should mention GPU memory limit"
    );
}

/// TEST 13: Verify accumulation_steps=1 behaves like normal training
#[tokio::test]
async fn test_no_accumulation_is_backward_compatible() {
    let mut params = create_test_hyperparams();
    params.gradient_accumulation_steps = 1; // No accumulation
    params.batch_size = 32;

    let trainer = DQNTrainer::new(params).expect("Failed to create trainer");

    // With accumulation_steps=1, behavior should be identical to normal training
    // (optimizer.step() called after every batch, no gradient accumulation)
    assert_eq!(
        trainer.hyperparams().gradient_accumulation_steps, 1,
        "accumulation_steps=1 should behave like normal training"
    );
}

/// TEST 14: Verify large accumulation steps (stress test)
#[test]
fn test_large_accumulation_steps() {
    let mut params = create_test_hyperparams();
    params.batch_size = 32;
    params.gradient_accumulation_steps = 16; // Effective batch: 512
    params.buffer_size = 10000; // Large buffer to support effective batch

    let trainer = DQNTrainer::new(params);
    assert!(
        trainer.is_ok(),
        "Should accept large accumulation_steps with sufficient buffer"
    );
}

/// TEST 15: Verify accumulation works with all DQN features
#[test]
fn test_accumulation_with_rainbow_dqn_features() {
    let mut params = create_test_hyperparams();
    params.gradient_accumulation_steps = 4;

    let trainer = DQNTrainer::new(params);
    assert!(
        trainer.is_ok(),
        "Gradient accumulation should work with all Rainbow DQN features"
    );
}
