cruxi 0.2.0

Minimal, transport-agnostic hexagonal architecture framework
Documentation
//! Additional tests for Context and Extensions
//!
//! Tests for edge cases including:
//! - Concurrent access patterns
//! - Nested context creation
//! - Expired context handling

use cruxi::{Context, ContextBuilder};
use std::f64::consts::PI;
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};

// ============================================================================
// EXTENSIONS EDGE CASES
// ============================================================================

#[test]
fn extensions_replace_value_of_same_type() {
    let mut ctx = Context::new();
    ctx.extensions_mut().insert(42u32);
    assert_eq!(ctx.get::<u32>(), Some(&42));

    ctx.extensions_mut().insert(100u32);
    assert_eq!(ctx.get::<u32>(), Some(&100));
}

#[test]
fn extensions_multiple_types() {
    let ctx = Context::new()
        .with_value(42u32)
        .with_value("hello".to_string())
        .with_value(PI)
        .with_value(vec![1i32, 2, 3]);

    assert_eq!(ctx.get::<u32>(), Some(&42));
    assert_eq!(ctx.get::<String>(), Some(&"hello".to_string()));
    assert_eq!(ctx.get::<f64>(), Some(&PI));
    assert_eq!(ctx.get::<Vec<i32>>(), Some(&vec![1, 2, 3]));
}

#[test]
fn extensions_remove_and_check() {
    let mut ctx = Context::new();
    ctx.extensions_mut().insert(42u32);
    assert!(ctx.extensions().contains::<u32>());

    let removed = ctx.extensions_mut().remove::<u32>();
    assert!(removed.is_some());
    assert!(!ctx.extensions().contains::<u32>());
    assert_eq!(ctx.get::<u32>(), None);
}

#[test]
fn extensions_remove_nonexistent() {
    let mut ctx = Context::new();
    let removed = ctx.extensions_mut().remove::<u32>();
    assert!(removed.is_none());
}

// ============================================================================
// CONTEXT CLONING AND SHARING
// ============================================================================

#[test]
fn context_clone_preserves_values() {
    let ctx1 = Context::new()
        .with_value(42u32)
        .with_value("original".to_string());

    let ctx2 = ctx1.clone();

    assert_eq!(ctx2.get::<u32>(), Some(&42));
    assert_eq!(ctx2.get::<String>(), Some(&"original".to_string()));
}

#[test]
fn context_clone_independent_modifications() {
    let ctx1 = Context::new().with_value(42u32);
    let mut ctx2 = ctx1.clone();

    ctx2.extensions_mut().insert(100u32);

    // ctx1 should still have original value
    assert_eq!(ctx1.get::<u32>(), Some(&42));
    // ctx2 has new value
    assert_eq!(ctx2.get::<u32>(), Some(&100));
}

#[test]
fn context_shared_across_threads() {
    let ctx = Arc::new(
        Context::new()
            .with_value(42u32)
            .with_value("shared".to_string()),
    );

    let ctx_clone = Arc::clone(&ctx);
    let handle = thread::spawn(move || {
        assert_eq!(ctx_clone.get::<u32>(), Some(&42));
        assert_eq!(ctx_clone.get::<String>(), Some(&"shared".to_string()));
    });

    assert_eq!(ctx.get::<u32>(), Some(&42));
    handle.join().expect("Thread should complete");
}

// ============================================================================
// DEADLINE AND TIMEOUT EDGE CASES
// ============================================================================

#[test]
fn context_deadline_exactly_now() {
    let now = Instant::now();
    let ctx = Context::with_deadline(now);

    // Should be done immediately or very shortly
    std::thread::sleep(Duration::from_millis(1));
    assert!(ctx.is_done());
}

#[test]
fn context_very_short_timeout() {
    let ctx = Context::with_timeout(Duration::from_nanos(1));
    std::thread::sleep(Duration::from_millis(1));

    assert!(ctx.is_done());
    assert!(ctx.time_remaining().is_none());
}

#[test]
fn context_very_long_timeout() {
    let ctx = Context::with_timeout(Duration::from_secs(3600)); // 1 hour

    assert!(!ctx.is_done());
    let remaining = ctx.time_remaining();
    assert!(remaining.is_some());
    assert!(remaining.unwrap() > Duration::from_secs(3500));
}

#[test]
fn context_time_remaining_decreases() {
    let ctx = Context::with_timeout(Duration::from_millis(500));

    let remaining1 = ctx.time_remaining().unwrap();
    std::thread::sleep(Duration::from_millis(100));
    let remaining2 = ctx.time_remaining().unwrap();

    assert!(remaining2 < remaining1);
}

#[test]
fn context_no_deadline_never_expires() {
    let ctx = Context::new();

    assert!(ctx.deadline().is_none());
    assert!(ctx.time_remaining().is_none());
    assert!(!ctx.is_done());
}

// ============================================================================
// CANCELLATION TESTS
// ============================================================================

#[test]
fn context_cancel_makes_done() {
    let mut ctx = Context::new();
    assert!(!ctx.is_done());

    ctx.cancel();
    assert!(ctx.is_done());
}

#[test]
fn context_cancel_with_deadline_still_done() {
    let mut ctx = Context::with_timeout(Duration::from_secs(60));
    assert!(!ctx.is_done());

    ctx.cancel();
    assert!(ctx.is_done());
}

#[test]
fn context_cancel_twice() {
    let mut ctx = Context::new();
    ctx.cancel();
    ctx.cancel(); // Should not panic

    assert!(ctx.is_done());
}

// ============================================================================
// NESTED CONTEXT / SHORTER DEADLINE TESTS
// ============================================================================

#[test]
fn context_shorter_deadline_works() {
    let far = Instant::now() + Duration::from_secs(60);
    let near = Instant::now() + Duration::from_secs(10);

    let ctx = Context::with_deadline(far);
    let child = ctx.with_shorter_deadline(near);

    assert_eq!(child.deadline(), Some(near));
}

#[test]
fn context_longer_deadline_keeps_original() {
    let near = Instant::now() + Duration::from_secs(10);
    let far = Instant::now() + Duration::from_secs(60);

    let ctx = Context::with_deadline(near);
    let child = ctx.with_shorter_deadline(far);

    assert_eq!(child.deadline(), Some(near));
}

#[test]
fn context_shorter_timeout_on_no_deadline() {
    let ctx = Context::new();
    let child = ctx.with_shorter_timeout(Duration::from_secs(30));

    assert!(child.deadline().is_some());
    assert!(!child.is_done());
}

#[test]
fn context_shorter_deadline_preserves_values() {
    let ctx = Context::new()
        .with_value(42u32)
        .with_value("preserved".to_string());

    let child = ctx.with_shorter_timeout(Duration::from_secs(30));

    assert_eq!(child.get::<u32>(), Some(&42));
    assert_eq!(child.get::<String>(), Some(&"preserved".to_string()));
}

// ============================================================================
// TRANSPORT TESTS
// ============================================================================

#[derive(Debug)]
struct HttpRequest {
    method: String,
    path: String,
}

#[derive(Debug)]
#[allow(dead_code)]
struct GrpcRequest {
    service: String,
    method: String,
}

#[test]
fn context_transport_wrong_type() {
    use cruxi::TransportAs;

    let ctx = Context::new().with_transport(HttpRequest {
        method: "GET".to_string(),
        path: "/api/users".to_string(),
    });

    // Should return None for wrong type
    let grpc = ctx.transport_as::<GrpcRequest>();
    assert!(grpc.is_none());
}

#[test]
fn context_transport_no_transport_set() {
    use cruxi::TransportAs;

    let ctx = Context::new();
    assert!(!ctx.has_transport());

    let http = ctx.transport_as::<HttpRequest>();
    assert!(http.is_none());
}

#[test]
fn context_transport_replace() {
    use cruxi::TransportAs;

    let ctx = Context::new()
        .with_transport(HttpRequest {
            method: "GET".to_string(),
            path: "/v1".to_string(),
        })
        .with_transport(HttpRequest {
            method: "POST".to_string(),
            path: "/v2".to_string(),
        });

    let http = ctx.transport_as::<HttpRequest>();
    assert!(http.is_some());
    assert_eq!(http.unwrap().method, "POST");
    assert_eq!(http.unwrap().path, "/v2");
}

// ============================================================================
// CONTEXT BUILDER TESTS
// ============================================================================

#[test]
fn context_builder_empty() {
    let ctx = ContextBuilder::new().build();

    assert!(ctx.deadline().is_none());
    assert!(!ctx.has_transport());
    assert!(!ctx.is_done());
}

#[test]
fn context_builder_full_configuration() {
    let ctx = ContextBuilder::new()
        .with_timeout(Duration::from_secs(30))
        .with_value(42u32)
        .with_value("request-id-123".to_string())
        .with_transport(HttpRequest {
            method: "GET".to_string(),
            path: "/api".to_string(),
        })
        .build();

    assert!(ctx.deadline().is_some());
    assert_eq!(ctx.get::<u32>(), Some(&42));
    assert_eq!(ctx.get::<String>(), Some(&"request-id-123".to_string()));
    assert!(ctx.has_transport());
}

#[test]
fn context_builder_deadline_precedence() {
    let deadline = Instant::now() + Duration::from_secs(10);
    let ctx = ContextBuilder::new()
        .with_timeout(Duration::from_secs(60)) // First set 60s
        .with_deadline(deadline) // Then override with 10s
        .build();

    assert_eq!(ctx.deadline(), Some(deadline));
}

// ============================================================================
// DEBUG FORMATTING
// ============================================================================

#[test]
fn context_debug_format() {
    let ctx = Context::new().with_value(42u32);
    let debug_str = format!("{:?}", ctx);

    assert!(debug_str.contains("Context"));
    assert!(debug_str.contains("deadline"));
    assert!(debug_str.contains("extensions"));
}

#[test]
fn extensions_debug_shows_count() {
    let ctx = Context::new()
        .with_value(1u32)
        .with_value("test".to_string());

    let debug_str = format!("{:?}", ctx.extensions());
    assert!(debug_str.contains("count"));
    assert!(debug_str.contains("2"));
}