batpak 0.8.0

Event sourcing with causal graphs and caller-defined gates. Sync API, no async runtime.
Documentation
use super::*;
use std::sync::atomic::{AtomicI64, Ordering};
use std::time::Duration;

#[test]
fn validated_runtime_clock_wraps_direct_field_assignment() {
    let raw = Arc::new(AtomicI64::new(2_000));
    let raw_clock = {
        let raw = Arc::clone(&raw);
        Arc::new(move || raw.load(Ordering::SeqCst)) as Arc<dyn Fn() -> i64 + Send + Sync>
    };

    let mut config = StoreConfig::new("target/test-clock-wrap");
    config.clock = Some(clock_from_fn(raw_clock));

    let runtime = config.validated().expect("config validates");
    assert_eq!(runtime.now_us(), 2_000);

    raw.store(1_500, Ordering::SeqCst);
    assert_eq!(
        runtime.now_us(),
        2_000,
        "validated runtime clock must clamp direct-field regressions"
    );
}

#[test]
fn cache_now_us_clamps_negative_custom_clock_values() {
    let raw_clock = Arc::new(|| -42_i64) as Arc<dyn Fn() -> i64 + Send + Sync>;
    let mut config = StoreConfig::new("target/test-cache-clock-clamp");
    config.clock = Some(clock_from_fn(raw_clock));

    let runtime = config.validated().expect("config validates");
    assert_eq!(
        runtime.cache_now_us(),
        0,
        "projection/cache metadata clock must not persist negative timestamps"
    );
}

#[test]
fn cache_now_us_preserves_zero_custom_clock_value() {
    let raw_clock = Arc::new(|| 0_i64) as Arc<dyn Fn() -> i64 + Send + Sync>;
    let mut config = StoreConfig::new("target/test-cache-clock-zero");
    config.clock = Some(clock_from_fn(raw_clock));

    let runtime = config.validated().expect("config validates");
    assert_eq!(
        runtime.cache_now_us(),
        0,
        "PROPERTY: zero is a valid cache timestamp boundary, not a negative-clock violation"
    );
}

#[test]
fn index_topology_tiles64_simd_builder_sets_only_simd_overlay() {
    let topology = IndexTopology::default().with_tiles64_simd(true);

    assert!(
        topology.tiles64_simd_enabled(),
        "PROPERTY: with_tiles64_simd(true) must enable the SIMD overlay"
    );
    assert!(
        !IndexTopology::default().tiles64_simd_enabled(),
        "PROPERTY: default topology keeps the experimental SIMD overlay disabled"
    );
    assert!(
        topology.soa_enabled() == IndexTopology::default().soa_enabled()
            && topology.entity_groups_enabled() == IndexTopology::default().entity_groups_enabled()
            && topology.tiles64_enabled() == IndexTopology::default().tiles64_enabled(),
        "PROPERTY: with_tiles64_simd must not silently reset the rest of the topology"
    );
}

#[test]
fn validated_accepts_documented_inclusive_upper_bounds() {
    let mut config = StoreConfig::new("target/test-config-upper-bounds");
    config.writer.pressure_retry_threshold_pct = 100;
    config.batch.max_size = 4096;

    config
        .validated()
        .expect("documented inclusive upper bounds should validate");
}

#[test]
fn validated_rejects_values_above_documented_upper_bounds() {
    let mut pressure = StoreConfig::new("target/test-config-pressure-too-high");
    pressure.writer.pressure_retry_threshold_pct = 101;
    assert!(
        matches!(
            pressure.validated(),
            Err(crate::store::StoreError::Configuration(_))
        ),
        "PROPERTY: pressure retry threshold above 100 must be rejected"
    );

    let mut batch = StoreConfig::new("target/test-config-batch-too-large");
    batch.batch.max_size = 4097;
    assert!(
        matches!(
            batch.validated(),
            Err(crate::store::StoreError::Configuration(_))
        ),
        "PROPERTY: batch.max_size above 4096 must be rejected"
    );

    let mut single_append = StoreConfig::new("target/test-config-single-append-too-large");
    single_append.single_append_max_bytes = 64 * 1024 * 1024 + 1;
    assert!(
        matches!(
            single_append.validated(),
            Err(crate::store::StoreError::Configuration(_))
        ),
        "PROPERTY: single_append_max_bytes above 64MB must be rejected"
    );

    let mut batch_bytes = StoreConfig::new("target/test-config-batch-bytes-too-large");
    batch_bytes.batch.max_bytes = 16 * 1024 * 1024 + 1;
    assert!(
        matches!(
            batch_bytes.validated(),
            Err(crate::store::StoreError::Configuration(_))
        ),
        "PROPERTY: batch.max_bytes above 16MB must be rejected"
    );
}

#[test]
fn validated_rejects_zero_payload_size_boundaries() {
    let mut single_append = StoreConfig::new("target/test-config-single-append-zero");
    single_append.single_append_max_bytes = 0;
    assert!(
        matches!(
            single_append.validated(),
            Err(crate::store::StoreError::Configuration(_))
        ),
        "PROPERTY: single_append_max_bytes of zero must be rejected"
    );

    let mut batch_bytes = StoreConfig::new("target/test-config-batch-bytes-zero");
    batch_bytes.batch.max_bytes = 0;
    assert!(
        matches!(
            batch_bytes.validated(),
            Err(crate::store::StoreError::Configuration(_))
        ),
        "PROPERTY: batch.max_bytes of zero must be rejected"
    );
}

#[test]
fn validated_config_debug_names_runtime_policy_fields() {
    let runtime = StoreConfig::new("target/test-validated-debug")
        .validated()
        .expect("config validates");
    let rendered = format!("{runtime:?}");

    assert!(
        rendered.contains("ValidatedStoreConfig")
            && rendered.contains("pressure_retry_threshold")
            && rendered.contains("group_commit_drain_budget")
            && rendered.contains("signing_registry"),
        "PROPERTY: ValidatedStoreConfig Debug must name the runtime policy fields, got: {rendered}"
    );
}

#[test]
fn process_boot_ns_is_nonzero_and_stable_in_process() {
    let clock = SystemClock::new();
    let first = clock.process_boot_ns();
    let second = clock.process_boot_ns();

    assert_ne!(
        first, 0,
        "PROPERTY: process_boot_ns must expose the captured wall-clock anchor, not zero/default"
    );
    assert_eq!(
        first, second,
        "PROPERTY: process_boot_ns must stay stable for the process lifetime"
    );
}

#[test]
fn now_mono_ns_advances_beyond_nonzero_sentinel() {
    let clock = SystemClock::new();
    std::thread::sleep(Duration::from_millis(1));
    let elapsed = clock.now_mono_ns();

    assert!(
        elapsed > 1,
        "PROPERTY: now_mono_ns must report elapsed nanoseconds from the process anchor, not a fixed sentinel; got {elapsed}"
    );
}

#[test]
fn duration_micros_preserves_zero_and_one_microsecond_boundaries() {
    assert_eq!(
        duration_micros(Duration::ZERO),
        0,
        "PROPERTY: zero duration must remain zero, not a default/nonzero sentinel"
    );
    assert_eq!(
        duration_micros(Duration::from_micros(1)),
        1,
        "PROPERTY: one microsecond must round-trip exactly"
    );
}