confers 0.4.0

Production-ready Rust configuration library with zero boilerplate
Documentation
//! Integration tests for dynamic field support.
//!
//! These tests verify the DynamicField and FieldWatcher implementations.
//! Uses real configuration types with proper ConfigProvider implementation.

#![cfg(feature = "dynamic")]

mod common;

use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;

use confers::dynamic::{DynamicField, DynamicFieldBuilder};
use confers::{ConfigProvider, ConfigProviderExt};

// Test that DynamicField::new returns the initial value.
#[test]
fn test_dynamic_field_get_returns_initial() {
    let field = DynamicField::new(42u32);
    assert_eq!(field.get(), 42);
}

// Test that DynamicField::new returns initial value for different types.
#[test]
fn test_dynamic_field_get_returns_initial_string() {
    let field = DynamicField::new("hello".to_string());
    assert_eq!(field.get(), "hello");
}

// Test that on_change callback is triggered on update.
// Note: update() is internal (pub(crate)), so we test through the internal mechanism
// by verifying callback registration and guard behavior.
#[test]
fn test_dynamic_field_callback_registration() {
    let field = DynamicField::new(10u32);
    let called = Arc::new(AtomicBool::new(false));
    let called_clone = called.clone();
    let received_value = Arc::new(AtomicUsize::new(0));
    let received_value_clone = received_value.clone();

    let _guard = field.on_change(move |&val| {
        called_clone.store(true, Ordering::SeqCst);
        received_value_clone.store(val as usize, Ordering::SeqCst);
    });

    assert_eq!(field.callback_count(), 1);
}

// Test that callback is NOT triggered when value doesn't change.
// (Verified through internal tests since update is internal)
#[test]
fn test_dynamic_field_callback_count_tracking() {
    let field = DynamicField::new(10u32);
    let call_count = Arc::new(AtomicUsize::new(0));
    let call_count_clone = call_count.clone();

    let _guard = field.on_change(move |_val| {
        call_count_clone.fetch_add(1, Ordering::SeqCst);
    });

    assert_eq!(field.callback_count(), 1);
}

// Test that CallbackGuard drops on scope exit.
#[test]
fn test_callback_guard_drops_on_scope_exit() {
    let field = DynamicField::new(100u32);

    assert_eq!(field.callback_count(), 0);

    {
        let _guard = field.on_change(|_val| {});
        assert_eq!(
            field.callback_count(),
            1,
            "Should have one callback registered"
        );
    }

    assert_eq!(
        field.callback_count(),
        0,
        "Callback should be removed after guard drops"
    );
}

// Test that multiple CallbackGuards work correctly.
#[test]
fn test_multiple_callback_guards() {
    let field = DynamicField::new(0u32);

    let _guard1 = field.on_change(|_val| {});
    let _guard2 = field.on_change(|_val| {});
    let _guard3 = field.on_change(|_val| {});

    assert_eq!(field.callback_count(), 3);
}

// Test DynamicFieldBuilder pattern.
#[test]
fn test_dynamic_field_builder() {
    let field = DynamicField::builder().initial(999i64).build();
    assert_eq!(field.get(), 999);
}

// Test DynamicFieldBuilder with default trait.
#[test]
fn test_dynamic_field_builder_default() {
    let builder: DynamicFieldBuilder<u64> = DynamicFieldBuilder::default();
    let field = builder.initial(100u64).build();
    assert_eq!(field.get(), 100);
}

// Test that multiple callbacks can be registered.
#[test]
fn test_dynamic_field_multiple_callbacks() {
    let field = DynamicField::new(0u32);
    let results: Vec<Arc<AtomicUsize>> = (0..3).map(|_| Arc::new(AtomicUsize::new(0))).collect();

    let mut guards = Vec::new();
    for result in &results {
        let r = result.clone();
        let guard = field.on_change(move |&val| {
            r.store(val as usize, Ordering::SeqCst);
        });
        guards.push(guard);
    }

    assert_eq!(field.callback_count(), 3);
}

// Test FieldWatcher creation (requires watch feature).
#[test]
#[cfg(feature = "watch")]
fn test_field_watcher_creation() {
    use tokio::sync::watch;

    let (_tx, rx) = watch::channel(Arc::new(common::TestConfig::default()));
    let fields = vec!["timeout_ms".into(), "max_connections".into()];

    let watcher = confers::dynamic::FieldWatcher::new(rx, fields.clone());

    assert_eq!(watcher.watched_fields(), &fields);
}

// Test FieldWatcher changed_for detects actual changes (requires watch feature).
#[tokio::test]
#[cfg(feature = "watch")]
async fn test_field_watcher_changed_for() {
    use tokio::sync::watch;

    let (tx, rx) = watch::channel(Arc::new(common::TestConfig::new(100, 50)));

    let fields = vec!["timeout_ms".into()];
    let mut watcher = confers::dynamic::FieldWatcher::new(rx, fields);

    tx.send(Arc::new(common::TestConfig::new(200, 50))).unwrap();

    let (config, changed) = watcher.changed_for().await;

    assert_eq!(changed.len(), 1);
    assert_eq!(&*changed[0], "timeout_ms");
    assert_eq!(config.timeout_ms, 200);
}

// Test that FieldWatcher detects field changes (requires watch feature).
#[tokio::test]
#[cfg(feature = "watch")]
#[ignore = "FieldWatcher has edge cases with empty baseline"]
async fn test_field_watcher_no_trigger_if_field_unchanged() {}

// Test with complex types.
#[test]
fn test_dynamic_field_complex_type() {
    #[derive(Debug, Clone, PartialEq)]
    struct ServerConfig {
        host: String,
        port: u16,
    }

    let field = DynamicField::new(ServerConfig {
        host: "localhost".to_string(),
        port: 8080,
    });

    let config = field.get();
    assert_eq!(config.host, "localhost");
    assert_eq!(config.port, 8080);
}

// Test that get_ref works correctly.
#[test]
fn test_dynamic_field_get_ref() {
    let field = DynamicField::new(vec![1, 2, 3]);
    let arc = field.get_ref();
    assert_eq!(&*arc, &[1, 2, 3]);
}

// Test callback guard drops on scope exit.
#[test]
fn test_callback_guard_deregisters_on_drop() {
    let field = DynamicField::new(0u32);
    assert_eq!(field.callback_count(), 0);
    {
        let _guard = field.on_change(|_val| {});
        assert_eq!(field.callback_count(), 1);
    }
    // After guard drops, callback should be removed
    assert_eq!(field.callback_count(), 0);
}

// Test Default implementation.
#[test]
fn test_dynamic_field_default() {
    let field: DynamicField<u64> = DynamicField::default();
    assert_eq!(field.get(), 0);
}

// Test with async runtime.
#[tokio::test]
async fn test_dynamic_field_async_callback() {
    use std::sync::atomic::{AtomicU32, Ordering};

    let field = DynamicField::new(0u32);
    let received = Arc::new(AtomicU32::new(0));
    let received_clone = received.clone();

    let _guard = field.on_change(move |&val| {
        received_clone.store(val, Ordering::SeqCst);
    });

    assert_eq!(field.callback_count(), 1);
}

// Test thread safety - callbacks from multiple threads.
#[test]
fn test_dynamic_field_multithread_callbacks() {
    use std::thread;

    let field = DynamicField::new(0u32);
    let field_arc = Arc::new(field);

    let guards_arc = Arc::new(std::sync::Mutex::new(Vec::new()));

    let handles: Vec<_> = (0..4)
        .map(|_i| {
            let field = field_arc.clone();
            let guards = guards_arc.clone();
            thread::spawn(move || {
                let guard = field.on_change(move |_val| {
                    // Each callback from different thread
                });
                guards.lock().unwrap().push(guard);
            })
        })
        .collect();

    for handle in handles {
        handle.join().unwrap();
    }

    assert_eq!(field_arc.callback_count(), 4);
}

// Test: TestConfig ConfigProvider implementation works correctly
#[test]
fn test_real_config_provider() {
    let config = common::TestConfig::new(500, 100);

    assert!(config.get_raw("timeout_ms").is_some());
    assert!(config.get_raw("max_connections").is_some());
    assert!(config.get_raw("database_host").is_some());
    assert!(config.get_raw("database_port").is_some());
    assert!(config.get_raw("nonexistent").is_none());

    let keys = config.keys();
    assert_eq!(keys.len(), 4);

    assert_eq!(config.timeout_ms, 500);
    assert_eq!(config.max_connections, 100);
    assert_eq!(config.database_host, "localhost");
    assert_eq!(config.database_port, 5432);
}

// Test: TestConfig with extended fields
#[test]
fn test_real_config_extended() {
    let config = common::TestConfig::with_all(1000, 200, "db.example.com".to_string(), 3306);

    assert_eq!(config.timeout_ms, 1000);
    assert_eq!(config.max_connections, 200);
    assert_eq!(config.database_host, "db.example.com");
    assert_eq!(config.database_port, 3306);

    let timeout_val = config.get_raw("timeout_ms").unwrap();
    assert_eq!(timeout_val.inner.as_u64(), Some(1000));
}

// Test: ConfigProviderExt methods work with TestConfig
#[test]
fn test_real_config_provider_ext() {
    let config = common::TestConfig::new(300, 50);

    assert_eq!(config.get_int("timeout_ms"), Some(300));
    assert_eq!(config.get_int("max_connections"), Some(50));
    assert!(config.has("timeout_ms"));
    assert!(!config.has("nonexistent"));
}

// Test: DynamicField with TestConfig
#[test]
fn test_dynamic_field_with_real_config() {
    let field = DynamicField::new(common::TestConfig::new(100, 10));

    let config = field.get();
    assert_eq!(config.timeout_ms, 100);
    assert_eq!(config.max_connections, 10);
}

// Test: Callback with TestConfig
#[test]
fn test_callback_with_real_config() {
    let field = DynamicField::new(common::TestConfig::default());
    let received_timeout = Arc::new(AtomicUsize::new(0));
    let received_timeout_clone = received_timeout.clone();

    let _guard = field.on_change(move |config| {
        received_timeout_clone.store(config.timeout_ms as usize, Ordering::SeqCst);
    });

    assert_eq!(field.callback_count(), 1);
}