//! Tests to demonstrate that certain PR review claims are incorrect
//!
//! These tests prove that:
//! 1. Reject commands don't bypass authentication
//! 2. There's no race condition in `auth_username` (each session has its own)
//! 3. Uninit buffers with `set_len` are safe when used with AsyncRead/AsyncWrite

use nntp_proxy::auth::AuthHandler;
use nntp_proxy::command::{AuthAction, CommandAction, CommandHandler};
use nntp_proxy::protocol::RequestContext;
use std::sync::Arc;
use tokio::io::AsyncReadExt;

fn classify(command: &str) -> CommandAction<'static> {
    let request = Box::leak(Box::new(
        RequestContext::parse(command.as_bytes()).expect("valid request line"),
    ));
    CommandHandler::classify_request(request)
}

/// Test that Reject commands (stateful commands) don't bypass authentication.
///
/// Claim: "When authentication is enabled but a client sends a Reject command,
/// the rejection response is sent but the session continues. After this, if the
/// client switches to sending stateless commands, authentication could be bypassed."
///
/// Reality: The session loops back after sending the rejection, and the next
/// command is still subject to the same authentication check. No bypass occurs.
#[tokio::test]
async fn test_reject_commands_dont_bypass_authentication() {
    use crate::test_helpers::create_test_auth_handler;

    let auth_handler = create_test_auth_handler();

    // Simulate what happens in the session handler
    let mut authenticated = false;

    // First command: GROUP (stateful, will be rejected)
    let action = classify("GROUP misc.test\r\n");
    match action {
        CommandAction::Reject(response) => {
            // Session sends rejection response
            assert!(response.contains("stateless"));
            // authenticated flag is still false
            assert!(!authenticated);
        }
        _ => panic!("GROUP should be rejected when unauthenticated"),
    }

    // Second command: LIST (stateless)
    // This should ALSO require authentication, proving no bypass
    let action = classify("LIST\r\n");
    match action {
        CommandAction::ForwardStateless => {
            // In the actual handler, this branch checks if auth is enabled
            // and returns 480 if not authenticated
            if auth_handler.is_enabled() && !authenticated {
                // Would send "480 Authentication required"
                // Proving that stateless commands after Reject still require auth
                assert!(!authenticated);
            }
        }
        _ => panic!("LIST should be ForwardStateless"),
    }

    // Only after successful auth should commands be forwarded
    let mut output = Vec::new();
    let (_, auth_success) = auth_handler
        .handle_auth_command(
            AuthAction::ValidateAndRespond { password: "pass" },
            &mut output,
            Some("user"),
        )
        .await
        .unwrap();

    if auth_success {
        authenticated = true;
    }

    assert!(authenticated);
}

/// Test that `auth_username` has no race condition.
///
/// Claim: "The authenticated field uses `AtomicBool` but `auth_username` is not
/// protected by the same atomic operation. A client could send concurrent
/// AUTHINFO PASS commands with different usernames."
///
/// Reality: Each TCP connection has its own session handler instance with its
/// own `auth_username` local variable. There's no shared state between connections.
/// Concurrent PASS commands from the SAME connection are serialized by TCP's
/// in-order delivery guarantee.
#[tokio::test]
async fn test_no_auth_username_race_condition() {
    use std::sync::atomic::{AtomicBool, Ordering};

    // Simulate two separate session handlers (two different connections)
    struct SessionHandler {
        auth_handler: Arc<AuthHandler>,
        authenticated: AtomicBool,
    }

    impl SessionHandler {
        fn new(auth_handler: Arc<AuthHandler>) -> Self {
            Self {
                auth_handler,
                authenticated: AtomicBool::new(false),
            }
        }

        async fn handle_auth(&self, username: String, password: String) -> bool {
            // Each session has its own auth_username local variable
            let auth_username = Some(username);

            let mut output = Vec::new();
            let (_, auth_success) = self
                .auth_handler
                .handle_auth_command(
                    AuthAction::ValidateAndRespond {
                        password: &password,
                    },
                    &mut output,
                    auth_username.as_deref(),
                )
                .await
                .unwrap();

            if auth_success {
                self.authenticated.store(true, Ordering::Release);
            }

            auth_success
        }
    }

    use crate::test_helpers::create_test_auth_handler_with;

    let auth_handler = create_test_auth_handler_with("alice", "secret");

    // Session 1: alice's connection
    let session1 = SessionHandler::new(auth_handler.clone());
    let success1 = session1
        .handle_auth("alice".to_string(), "secret".to_string())
        .await;
    assert!(success1);

    // Session 2: bob's connection (different session, different auth_username)
    let session2 = SessionHandler::new(auth_handler.clone());
    let success2 = session2
        .handle_auth("bob".to_string(), "wrong".to_string())
        .await;
    assert!(!success2); // bob fails - no interference from alice's session

    // Session 3: alice again (correct credentials)
    let session3 = SessionHandler::new(auth_handler.clone());
    let success3 = session3
        .handle_auth("alice".to_string(), "secret".to_string())
        .await;
    assert!(success3); // alice succeeds - independent session

    // Prove that sessions don't interfere with each other
    assert!(session1.authenticated.load(Ordering::Acquire));
    assert!(!session2.authenticated.load(Ordering::Acquire));
    assert!(session3.authenticated.load(Ordering::Acquire));
}

/// Test that concurrent auth attempts from the same "connection" are serialized.
///
/// In reality, TCP provides in-order delivery, so a single connection can't have
/// truly concurrent commands. But even if it could, each auth attempt would use
/// the username from the most recent AUTHINFO USER command, which is correct behavior.
#[tokio::test]
async fn test_auth_attempts_are_serialized_per_connection() {
    use crate::test_helpers::create_test_auth_handler;

    let auth_handler = create_test_auth_handler();

    // Simulate sequential auth attempts from one connection
    // AUTHINFO USER user
    let mut auth_username = Some("user".to_string());

    // AUTHINFO PASS wrong
    let mut output = Vec::new();
    let (_, success1) = auth_handler
        .handle_auth_command(
            AuthAction::ValidateAndRespond { password: "wrong" },
            &mut output,
            auth_username.as_deref(),
        )
        .await
        .unwrap();
    assert!(!success1);

    // AUTHINFO PASS pass (correct this time)
    output.clear();
    let (_, success2) = auth_handler
        .handle_auth_command(
            AuthAction::ValidateAndRespond { password: "pass" },
            &mut output,
            auth_username.as_deref(),
        )
        .await
        .unwrap();
    assert!(success2);

    // Even if client sends AUTHINFO USER again, it just updates auth_username
    auth_username = Some("otheruser".to_string());
    output.clear();
    let (_, success3) = auth_handler
        .handle_auth_command(
            AuthAction::ValidateAndRespond { password: "pass" },
            &mut output,
            auth_username.as_deref(),
        )
        .await
        .unwrap();
    assert!(!success3); // Fails because otheruser/pass is wrong
}

/// Test that uninit buffers used with `AsyncRead` are safe.
///
/// Claim: "Using unsafe { `buffer.set_len(size)` } to create uninitialized buffers
/// is unsound. The mere act of creating a &[u8] reference to uninitialized memory is UB."
///
/// Reality: This is explicitly documented as safe in Rust when:
/// 1. The buffer is only accessed through AsyncRead/AsyncWrite operations
/// 2. Only the initialized portion &buf[..n] is accessed after reading n bytes
/// 3. This is the standard pattern used throughout the Rust async ecosystem
///
/// See: <https://doc.rust-lang.org/std/vec/struct.Vec.html#method.set_len>
/// "Notably, if the length is not set to the current logical length of the vector,
/// then all elements between the old length and new length are in an uninitialized state."
///
/// The safety invariant is: don't read uninitialized bytes. `AsyncRead` guarantees
/// it initializes the bytes it writes, and we only access &buf[..n] where n is
/// the number of bytes read.
#[tokio::test]
async fn test_uninit_buffer_pattern_is_safe() {
    /// This is the exact pattern used in buffer pool
    ///
    /// # Safety
    ///
    /// The returned buffer contains uninitialized memory and must only be used with
    /// `AsyncRead`/`AsyncWrite` operations that initialize bytes before reading them.
    /// Only access `&buf[..n]` where `n` is the number of bytes actually written.
    #[allow(clippy::uninit_vec)] // This benchmark intentionally mirrors the production buffer-allocation pattern.
    fn create_buffer(size: usize) -> Vec<u8> {
        let mut buffer = Vec::with_capacity(size);
        unsafe {
            buffer.set_len(size);
        }
        buffer
    }

    let mut buffer = create_buffer(1024);

    // Simulate AsyncRead usage - write data into the buffer
    let test_data = b"Hello, World!";
    let n = test_data.len();
    buffer[..n].copy_from_slice(test_data);

    // SAFE: Only access initialized portion
    assert_eq!(&buffer[..n], test_data);

    // This demonstrates the safety: we never read uninitialized bytes
    // The buffer contains uninit memory, but we only ever access buf[..n]
    // where n is the number of bytes that were written (initialized)

    // Prove it works with actual AsyncRead
    let cursor_data = b"Test data for AsyncRead";
    let mut cursor = std::io::Cursor::new(cursor_data);
    let mut read_buffer = create_buffer(1024);

    let bytes_read = cursor.read(&mut read_buffer).await.unwrap();
    assert_eq!(bytes_read, cursor_data.len());
    assert_eq!(&read_buffer[..bytes_read], cursor_data);

    // SAFETY EXPLANATION: We cannot prove memory safety in a test.
    // Instead, we demonstrate that the unsafe pattern is:
    // 1. INTERNAL to the buffer pool (create_aligned_buffer is private fn)
    // 2. Wrapped by safe public API (BufferPool::get_buffer returns PooledBuffer)
    // 3. Only used with AsyncRead/AsyncWrite which initialize before we access
    // 4. Access is restricted to &buf[..n] where n is bytes initialized
    //
    // The safety relies on:
    // - Not exposing uninitialized buffers through public API
    // - Usage contract enforced by how buffers are obtained (only via get_buffer)
    // - Type system ensuring AsyncRead/Write are the only way to fill buffers
}

/// Test that the buffer pool pattern is sound even with multiple get/return cycles.
#[tokio::test]
async fn test_buffer_pool_safety_across_cycles() {
    use crate::test_helpers::create_test_buffer_pool;

    let pool = create_test_buffer_pool();

    // Get buffer, use it, return it
    {
        let mut buffer = pool.acquire();
        let test_data = b"First use";
        buffer.copy_from_slice(test_data);
        assert_eq!(&buffer[..test_data.len()], test_data);
        // buffer auto-returns to pool on drop
    }

    // Get buffer again (might be same buffer, might be new)
    {
        let mut buffer = pool.acquire();
        // Even if this is the same buffer from before, it's safe because:
        // 1. We'll overwrite it with new data via AsyncRead
        // 2. We only access buf[..n] where n is bytes actually read
        let new_data = b"Second use";
        buffer.copy_from_slice(new_data);
        assert_eq!(&buffer[..new_data.len()], new_data);
    }

    // This pattern is safe and avoids zeroing overhead
}

/// Test that proves the review claim about "resize(size, 0)" would destroy performance.
///
/// This is a performance benchmark that takes ~8 seconds to run.
/// Use `cargo test test_uninit_vs_zeroed_performance_difference -- --ignored` to run it.
#[test]
#[ignore = "slow performance benchmark - run explicitly with --ignored"]
fn test_uninit_vs_zeroed_performance_difference() {
    use std::time::Instant;

    const SIZE: usize = 64 * 1024; // 64KB
    const ITERATIONS: usize = 10_000;

    // Method 1: resize (zeros memory) - what review suggests
    let start = Instant::now();
    for _ in 0..ITERATIONS {
        #[allow(clippy::slow_vector_initialization)]
        // This branch intentionally benchmarks zeroed allocation.
        let mut buffer: Vec<u8> = Vec::with_capacity(SIZE);
        buffer.resize(SIZE, 0); // Zeros all 64KB
        std::hint::black_box(&buffer);
    }
    let zeroed_time = start.elapsed();

    // Method 2: set_len (no zeroing) - what we use
    let start = Instant::now();
    for _ in 0..ITERATIONS {
        let mut buffer: Vec<u8> = Vec::with_capacity(SIZE);
        #[allow(clippy::uninit_vec)]
        // This branch intentionally benchmarks uninitialized allocation.
        unsafe {
            buffer.set_len(SIZE); // No zeroing
        }
        std::hint::black_box(&buffer);
    }
    let uninit_time = start.elapsed();

    // set_len should be significantly faster
    println!("Zeroed: {zeroed_time:?}, Uninit: {uninit_time:?}");

    // The uninit method should be at least 2x faster for large buffers
    // (actual difference is often 5-10x for 64KB buffers)
    assert!(
        uninit_time < zeroed_time,
        "Uninit buffers should be faster than zeroed buffers"
    );
}
