use crate::error::{DbError, DbResult};

/// Default for [`Config::compaction_max_files_per_pass`]: a single compaction
/// pass retires at most this many immutable files per shard. Bounds per-pass
/// work; raise it (or set 0 = unlimited) for continuous overwrite workloads.
pub(crate) const DEFAULT_COMPACTION_MAX_FILES_PER_PASS: usize = 4;

#[cfg(any(feature = "armour", feature = "postcard-codec"))]
fn default_compaction_max_files_per_pass() -> usize {
    DEFAULT_COMPACTION_MAX_FILES_PER_PASS
}

/// Bitcask flush-path I/O strategy. Tunable; in-memory effect only — the
/// on-disk format is identical regardless. Default: `Pwrite`.
///
/// `Uring` requires BOTH the non-default `io-uring` Cargo feature AND Linux; if
/// either is missing it falls back to blocking `pwrite` (logged once at open).
/// io_uring is off by default because its SQPOLL kernel poller busy-polls a CPU
/// per shard — opt in only when benchmarks on your workload justify it. See
/// `docs/research/sqpoll-noise.md`.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(
    any(feature = "armour", feature = "postcard-codec"),
    derive(serde::Deserialize)
)]
pub enum IoBackend {
    /// io_uring writer. `sqpoll_idle_ms = Some(ms)` spawns a dedicated SQPOLL
    /// kernel poller (`iou-sqp-*`) with that idle timeout in milliseconds;
    /// `None` uses io_uring without a poller (each submit makes an
    /// `io_uring_enter` syscall — no kernel thread).
    Uring { sqpoll_idle_ms: Option<u32> },
    /// Blocking `pwrite` via `FileExt::write_all_at` — no io_uring, no poller.
    #[default]
    Pwrite,
}

/// Database configuration.
///
/// # Immutable vs tunable parameters
///
/// **Immutable** (fixed at creation, changing breaks the database):
/// - `shard_count` — stored in `db.meta`, determines key→shard routing
/// - `shard_prefix_bits` — determines key→shard grouping; changing breaks
///   `atomic` guarantees and dead_bytes tracking across shards
/// - `encryption_key` — presence/absence stored in `db.meta`
///
/// **Tunable** (safe to change between restarts):
/// - `max_file_size`, `compaction_threshold`, `enable_fsync`, `write_buffer_size`
/// - `cache` — in-memory only, no on-disk state
/// - `reversed` — only affects in-memory index ordering
/// - `hints` — controls hint file generation/use during recovery
#[derive(Debug, Clone)]
#[cfg_attr(
    any(feature = "armour", feature = "postcard-codec"),
    derive(serde::Deserialize)
)]
pub struct Config {
    /// Number of shards. **Immutable** — stored in `db.meta`, changing requires
    /// recreating the database. Default: `available_parallelism()`.
    pub shard_count: usize,
    /// Maximum size of a single data file before rotation. Tunable.
    /// Default: 256 MB.
    pub max_file_size: u64,
    /// Ratio of dead_bytes / total_bytes required to trigger compaction. Tunable.
    /// Default: 0.3.
    pub compaction_threshold: f64,
    /// Maximum number of immutable files a single compaction pass retires per
    /// shard. Bounds per-pass work (scan I/O + the temporary disk used by the
    /// rewritten output) so one `compact()` call can't stall on a huge rewrite.
    /// **A low value throttles reclaim under continuous overwrite churn**: if the
    /// writer creates files faster than `cap` files are retired per pass, on-disk
    /// size tracks total bytes written rather than the live set. For such
    /// workloads either raise this, set it to `0` (unlimited — drain the whole
    /// backlog in one pass), or drive `compact()` to convergence each cycle.
    /// Tunable. Default: 4.
    #[cfg_attr(
        any(feature = "armour", feature = "postcard-codec"),
        serde(default = "default_compaction_max_files_per_pass")
    )]
    pub compaction_max_files_per_pass: usize,
    /// Whether to fsync after writes. **Currently a no-op for the Bitcask write
    /// path** — Bitcask never fsyncs per write (`DurabilityInner::sync()` for
    /// Bitcask is a no-op). Durability comes from write-buffer flush plus an
    /// explicit `flush()`/`close()`, or the optional periodic [`Flusher`].
    /// Tunable. Default: false.
    pub enable_fsync: bool,
    /// Write buffer size per shard in bytes. Tunable. Writes are buffered in
    /// memory and flushed to disk when the buffer is full or on explicit
    /// flush/close. Default: 1 MB.
    pub write_buffer_size: usize,
    /// Value cache configuration (for VarTree). Tunable — in-memory only.
    #[cfg(feature = "var-collections")]
    pub cache: CacheConfig,
    /// Number of prefix bits of the key to use for shard routing. **Immutable** —
    /// changing breaks `atomic` guarantees (keys regroup across shards) and
    /// dead_bytes tracking (old entries attributed to wrong shard).
    /// 0 = hash full key (default). Non-zero = hash first N bits for locality.
    pub shard_prefix_bits: usize,
    /// Reverse key ordering in the index. Tunable — only affects in-memory
    /// index ordering. When `true`, forward iteration yields keys in
    /// descending order (newest first for monotonic IDs). Default: true.
    /// Also governs iteration order of iterable Map collections (`iter_view`).
    pub reversed: bool,
    /// Build the in-memory companion key index that enables ordered iteration
    /// (`iter`/`range`/`paginate`/`retain` via `iter_view()`). Tunable,
    /// **in-memory only** — not stored in `db.meta`, so it can be toggled freely
    /// across reopens. Costs extra memory (a per-shard BTreeSet of keys) and
    /// O(log N) per write. Default: false.
    ///
    /// Opt in only for **occasional admin/maintenance** scans (browsing a
    /// collection, bulk-cleaning stale rows) — not for hot-path reads. A Map's
    /// strength is O(1) point lookup; if scans are part of the normal workload,
    /// use a `*Tree` collection instead. Map iteration is a batched k-way merge
    /// that releases shard locks between batches, so it is **not a point-in-time
    /// snapshot** like a `Tree` scan: concurrent writes may be only partially
    /// observed.
    #[cfg_attr(any(feature = "armour", feature = "postcard-codec"), serde(default))]
    pub iterable: bool,
    /// Generate and use hint files for faster recovery. Tunable.
    /// `None` ⇒ the collection `open()` chooses by type (Var → true,
    /// inline → false). `Some(x)` is respected as an explicit override.
    /// Default per workload preset: `None`.
    #[cfg_attr(any(feature = "armour", feature = "postcard-codec"), serde(default))]
    pub hints: Option<bool>,
    /// Use `O_DIRECT` on Linux for the hot serving path (active-file write +
    /// live read fds). Tunable — in-memory effect only; the on-disk format is
    /// identical regardless. Falls back to buffered if the filesystem rejects
    /// `O_DIRECT`. No effect off Linux. Default: false.
    pub direct_io: bool,
    /// Bitcask flush-path I/O strategy. Tunable. Default: `Pwrite`.
    /// See [`IoBackend`].
    #[cfg_attr(any(feature = "armour", feature = "postcard-codec"), serde(default))]
    pub io_backend: IoBackend,
    /// 32-byte AES-256 encryption key. **Immutable** — presence/absence is
    /// stored in `db.meta`. `None` = no encryption.
    /// Use `PageCipher::key_from_env("ARMDB_KEY")` to read from environment.
    #[cfg(feature = "encryption")]
    pub encryption_key: Option<[u8; 32]>,
}

/// Configuration for the value cache used by `VarTree`.
#[cfg(feature = "var-collections")]
#[derive(Debug, Clone)]
#[cfg_attr(
    any(feature = "armour", feature = "postcard-codec"),
    derive(serde::Deserialize)
)]
pub struct CacheConfig {
    /// Maximum cache size in bytes. 0 = disabled. Default: 0.
    pub max_size: u64,
    /// Estimated number of items (for hash table pre-allocation). Default: 100_000.
    pub estimated_items: usize,
}

impl Config {
    /// Config with small shard count for tests. Keeps fd usage low
    /// so `cargo test` doesn't hit "Too many open files" on default ulimit.
    pub fn test() -> Self {
        Config::balanced().shard_count(2).hints(true).build()
    }

    /// Test config with `hints: None` (lets `open()` resolve by type).
    #[doc(hidden)]
    pub fn test_no_hints() -> Self {
        let mut cfg = Self::test();
        cfg.hints = None;
        cfg
    }

    /// Resolve `hints` against the collection type's default: `None` becomes
    /// `Some(default)`, an explicit `Some(_)` is left untouched. Called by each
    /// leaf collection `open()` (Var → `true`, inline → `false`).
    pub(crate) fn with_resolved_hints(mut self, default: bool) -> Self {
        self.hints = Some(self.hints.unwrap_or(default));
        self
    }

    /// True when a Var collection would open without a block cache
    /// (`max_size == 0`) — every `get` then hits disk. Used to emit a one-time
    /// `warn!` from Var collection `open()`.
    #[cfg(feature = "var-collections")]
    pub(crate) fn var_cache_warn_needed(&self) -> bool {
        self.cache.max_size == 0
    }

    pub fn validate(&self) -> DbResult<()> {
        if self.shard_count == 0 || self.shard_count > 255 {
            return Err(DbError::Config("shard_count must be between 1 and 255"));
        }
        if self.max_file_size < 4096 {
            return Err(DbError::Config("max_file_size must be at least 4096"));
        }
        if self.write_buffer_size < 4096 {
            return Err(DbError::Config("write_buffer_size must be at least 4096"));
        }
        if self.max_file_size > u32::MAX as u64 {
            return Err(DbError::Config(
                "max_file_size must not exceed u32::MAX (4 GiB)",
            ));
        }
        if (self.write_buffer_size as u64) > (u32::MAX as u64) - 4096 {
            return Err(DbError::Config(
                "write_buffer_size must not exceed u32::MAX - 4096",
            ));
        }
        if (self.write_buffer_size as u64) > self.max_file_size {
            return Err(DbError::Config(
                "max_file_size must be >= write_buffer_size",
            ));
        }
        if self.shard_prefix_bits > u8::MAX as usize {
            return Err(DbError::Config("shard_prefix_bits must be <= 255"));
        }
        if !self.compaction_threshold.is_finite()
            || !(0.0..=1.0).contains(&self.compaction_threshold)
        {
            return Err(DbError::Config(
                "compaction_threshold must be a finite value in [0.0, 1.0]",
            ));
        }
        let page_aligned = {
            #[cfg(feature = "encryption")]
            {
                self.direct_io || self.encryption_key.is_some()
            }
            #[cfg(not(feature = "encryption"))]
            {
                self.direct_io
            }
        };
        if page_aligned {
            if self.write_buffer_size < 8192 {
                return Err(DbError::Config(
                    "write_buffer_size must be at least 8192 when direct_io or encryption is enabled",
                ));
            }
            if !self.write_buffer_size.is_multiple_of(4096) {
                return Err(DbError::Config(
                    "write_buffer_size must be a multiple of 4096 when direct_io or encryption is enabled",
                ));
            }
        }
        Ok(())
    }
}

#[cfg(feature = "var-collections")]
impl Default for CacheConfig {
    fn default() -> Self {
        Self {
            max_size: 0,
            estimated_items: 100_000,
        }
    }
}

/// Bitcask deployment-intent presets for [`Config`]. Pick one as the builder
/// start point: `Config::for_workload(Workload::…)` or a sugar method.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Workload {
    /// Balanced middle-ground (closest to the former `Default`).
    Balanced,
    /// Embedded / low-latency: few shards, small files & buffers.
    LowLatency,
    /// High-throughput server: many shards, large files & buffers.
    HighThroughput,
    /// Durability-first: narrow loss window (small write buffer) + `enable_fsync`.
    /// NOTE: `enable_fsync` is currently a **no-op for the Bitcask write path**
    /// (read only by FixedStore). Bitcask durability here = small
    /// `write_buffer_size` + `Flusher`/`flush()`/`close()`. The flag is set as a
    /// forward-compat placeholder (spec §F1).
    Durable,
    /// Large disk-resident values (Var collections, big blobs): large files,
    /// `O_DIRECT`, and a non-zero block-cache baseline.
    LargeValues,
}

pub(crate) fn default_shard_count() -> usize {
    #[cfg(debug_assertions)]
    {
        std::thread::available_parallelism()
            .map(|p| p.get() / 2)
            .unwrap_or(2)
            .clamp(1, 4)
    }
    #[cfg(not(debug_assertions))]
    {
        std::thread::available_parallelism()
            .map(|p| p.get() / 2)
            .unwrap_or(4)
            .clamp(1, 8)
    }
}

impl Workload {
    fn base(self) -> Config {
        let common = Config {
            shard_count: default_shard_count(),
            max_file_size: 256 * 1024 * 1024,
            compaction_threshold: 0.30,
            compaction_max_files_per_pass: DEFAULT_COMPACTION_MAX_FILES_PER_PASS,
            enable_fsync: false,
            write_buffer_size: 1024 * 1024,
            #[cfg(feature = "var-collections")]
            cache: CacheConfig {
                max_size: 0,
                estimated_items: 100_000,
            },
            shard_prefix_bits: 0,
            reversed: true,
            iterable: false,
            hints: None,
            direct_io: false,
            io_backend: IoBackend::Pwrite,
            #[cfg(feature = "encryption")]
            encryption_key: None,
        };
        match self {
            Workload::Balanced => common,
            Workload::LowLatency => Config {
                shard_count: 4,
                max_file_size: 64 * 1024 * 1024,
                write_buffer_size: 256 * 1024,
                ..common
            },
            Workload::HighThroughput => Config {
                shard_count: 64,
                max_file_size: 512 * 1024 * 1024,
                write_buffer_size: 4 * 1024 * 1024,
                compaction_threshold: 0.25,
                ..common
            },
            Workload::Durable => Config {
                shard_count: 16,
                max_file_size: 128 * 1024 * 1024,
                write_buffer_size: 64 * 1024,
                enable_fsync: true,
                ..common
            },
            Workload::LargeValues => Config {
                max_file_size: 1 << 30, // 1GB
                write_buffer_size: 4 * 1024 * 1024,
                direct_io: true,
                #[cfg(feature = "var-collections")]
                cache: CacheConfig {
                    max_size: 512 << 20, // 536MB
                    estimated_items: 131_072,
                },
                ..common
            },
        }
    }
}

#[bon::bon]
impl Config {
    /// Build a `Config` from a [`Workload`] preset; any field is overridable via
    /// the chained setters, unset fields keep the preset value.
    #[builder(builder_type = ConfigBuilder, finish_fn = build)]
    pub fn for_workload(
        #[builder(start_fn)] workload: Workload,
        shard_count: Option<usize>,
        max_file_size: Option<u64>,
        write_buffer_size: Option<usize>,
        compaction_threshold: Option<f64>,
        compaction_max_files_per_pass: Option<usize>,
        enable_fsync: Option<bool>,
        direct_io: Option<bool>,
        io_backend: Option<IoBackend>,
        reversed: Option<bool>,
        iterable: Option<bool>,
        shard_prefix_bits: Option<usize>,
        /// `None` ⇒ collection `open()` chooses by type.
        hints: Option<bool>,
        #[cfg(feature = "var-collections")] cache: Option<CacheConfig>,
        #[cfg(feature = "encryption")] encryption_key: Option<[u8; 32]>,
    ) -> Config {
        let b = workload.base();
        Config {
            shard_count: shard_count.unwrap_or(b.shard_count),
            max_file_size: max_file_size.unwrap_or(b.max_file_size),
            write_buffer_size: write_buffer_size.unwrap_or(b.write_buffer_size),
            compaction_threshold: compaction_threshold.unwrap_or(b.compaction_threshold),
            compaction_max_files_per_pass: compaction_max_files_per_pass
                .unwrap_or(b.compaction_max_files_per_pass),
            enable_fsync: enable_fsync.unwrap_or(b.enable_fsync),
            direct_io: direct_io.unwrap_or(b.direct_io),
            io_backend: io_backend.unwrap_or(b.io_backend),
            reversed: reversed.unwrap_or(b.reversed),
            iterable: iterable.unwrap_or(b.iterable),
            shard_prefix_bits: shard_prefix_bits.unwrap_or(b.shard_prefix_bits),
            hints,
            #[cfg(feature = "var-collections")]
            cache: cache.unwrap_or(b.cache),
            #[cfg(feature = "encryption")]
            encryption_key: encryption_key.or(b.encryption_key),
        }
    }
}

impl Config {
    /// Builder starting from the [`Workload::Balanced`] preset.
    pub fn balanced() -> ConfigBuilder {
        Self::for_workload(Workload::Balanced)
    }
    /// Builder starting from the [`Workload::LowLatency`] preset.
    pub fn low_latency() -> ConfigBuilder {
        Self::for_workload(Workload::LowLatency)
    }
    /// Builder starting from the [`Workload::HighThroughput`] preset.
    pub fn high_throughput() -> ConfigBuilder {
        Self::for_workload(Workload::HighThroughput)
    }
    /// Builder starting from the [`Workload::Durable`] preset.
    pub fn durable() -> ConfigBuilder {
        Self::for_workload(Workload::Durable)
    }
    /// Builder starting from the [`Workload::LargeValues`] preset.
    pub fn large_values() -> ConfigBuilder {
        Self::for_workload(Workload::LargeValues)
    }
}

#[cfg(test)]
mod io_backend_tests {
    use super::*;

    #[test]
    fn default_io_backend_is_pwrite() {
        let cfg = Config::balanced().build();
        assert_eq!(cfg.io_backend, IoBackend::Pwrite);
    }

    #[test]
    fn io_backend_default_impl_matches_config_default() {
        assert_eq!(IoBackend::default(), IoBackend::Pwrite);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn default_for_test() -> Config {
        Config {
            shard_count: 2,
            max_file_size: 256 * 1024 * 1024,
            compaction_threshold: 0.3,
            compaction_max_files_per_pass: DEFAULT_COMPACTION_MAX_FILES_PER_PASS,
            enable_fsync: false,
            write_buffer_size: 1024 * 1024,
            #[cfg(feature = "var-collections")]
            cache: CacheConfig::default(),
            shard_prefix_bits: 0,
            reversed: true,
            iterable: false,
            hints: None,
            direct_io: false,
            io_backend: IoBackend::default(),
            #[cfg(feature = "encryption")]
            encryption_key: None,
        }
    }

    #[test]
    fn test_default_config_is_valid() {
        let cfg = Config::balanced().build();
        assert!(cfg.shard_count >= 1);
        assert!(cfg.validate().is_ok());
    }

    #[test]
    fn validate_rejects_max_file_size_smaller_than_write_buffer() {
        let mut cfg = default_for_test();
        cfg.max_file_size = 4096;
        cfg.write_buffer_size = 8192;
        assert!(cfg.validate().is_err());
    }

    #[test]
    fn validate_rejects_write_buffer_at_u32_limit() {
        let mut cfg = default_for_test();
        cfg.write_buffer_size = (u32::MAX as usize) - 4095;
        cfg.max_file_size = u32::MAX as u64;
        assert!(cfg.validate().is_err());
    }

    #[cfg(feature = "encryption")]
    #[test]
    fn validate_rejects_small_write_buffer_under_encryption() {
        let mut cfg = default_for_test();
        cfg.encryption_key = Some([0u8; 32]);
        cfg.write_buffer_size = 4096;
        assert!(cfg.validate().is_err());
    }

    #[cfg(feature = "encryption")]
    #[test]
    fn validate_rejects_non_page_multiple_write_buffer_under_encryption() {
        let mut cfg = default_for_test();
        cfg.encryption_key = Some([0u8; 32]);
        cfg.write_buffer_size = 8192 + 100;
        cfg.max_file_size = 256 * 1024 * 1024;
        assert!(cfg.validate().is_err());
    }

    #[cfg(feature = "encryption")]
    #[test]
    fn validate_accepts_page_multiple_8k_under_encryption() {
        let mut cfg = default_for_test();
        cfg.encryption_key = Some([0u8; 32]);
        cfg.write_buffer_size = 8192;
        cfg.max_file_size = 256 * 1024 * 1024;
        assert!(cfg.validate().is_ok());
    }

    #[test]
    fn validate_accepts_plain_4096_write_buffer() {
        let mut cfg = default_for_test();
        cfg.write_buffer_size = 4096;
        cfg.max_file_size = 8192;
        assert!(cfg.validate().is_ok());
    }

    #[test]
    fn validate_rejects_shard_prefix_bits_over_255() {
        let mut cfg = default_for_test();
        cfg.shard_prefix_bits = 256;
        let err = cfg.validate().unwrap_err();
        assert!(
            err.to_string().contains("shard_prefix_bits"),
            "expected shard_prefix_bits error, got: {err}"
        );
    }

    #[test]
    fn validate_rejects_compaction_threshold_nan() {
        let mut cfg = default_for_test();
        cfg.compaction_threshold = f64::NAN;
        assert!(cfg.validate().is_err());
    }

    #[test]
    fn validate_rejects_compaction_threshold_negative() {
        let mut cfg = default_for_test();
        cfg.compaction_threshold = -0.1;
        assert!(cfg.validate().is_err());
    }

    #[test]
    fn validate_rejects_compaction_threshold_over_one() {
        let mut cfg = default_for_test();
        cfg.compaction_threshold = 1.1;
        assert!(cfg.validate().is_err());
    }

    #[test]
    fn validate_accepts_compaction_threshold_half() {
        let mut cfg = default_for_test();
        cfg.compaction_threshold = 0.5;
        assert!(cfg.validate().is_ok());
    }

    #[test]
    fn validate_accepts_compaction_threshold_boundaries() {
        let mut cfg = default_for_test();
        cfg.compaction_threshold = 0.0;
        assert!(cfg.validate().is_ok());
        cfg.compaction_threshold = 1.0;
        assert!(cfg.validate().is_ok());
    }

    #[test]
    fn direct_io_requires_page_aligned_write_buffer() {
        let mut c = default_for_test();
        c.direct_io = true;
        c.write_buffer_size = 5000; // not a multiple of 4096
        assert!(c.validate().is_err());

        c.write_buffer_size = 8192;
        assert!(c.validate().is_ok());

        c.write_buffer_size = 4096; // < 8192
        assert!(c.validate().is_err());
    }

    #[test]
    fn direct_io_defaults_false() {
        assert!(!Config::balanced().build().direct_io);
    }

    #[test]
    fn with_resolved_hints_fills_none_with_default() {
        let cfg = default_for_test();
        assert_eq!(cfg.clone().with_resolved_hints(true).hints, Some(true));
        assert_eq!(cfg.with_resolved_hints(false).hints, Some(false));
    }

    #[test]
    fn with_resolved_hints_respects_some() {
        let mut cfg = default_for_test();
        cfg.hints = Some(true);
        assert_eq!(cfg.with_resolved_hints(false).hints, Some(true));
    }

    #[cfg(feature = "var-collections")]
    #[test]
    fn var_cache_warn_needed_true_when_zero() {
        let mut cfg = default_for_test();
        cfg.cache.max_size = 0;
        assert!(cfg.var_cache_warn_needed());
        cfg.cache.max_size = 1 << 20;
        assert!(!cfg.var_cache_warn_needed());
    }

    #[test]
    fn workload_balanced_matches_table() {
        let c = Config::balanced().build();
        assert_eq!(c.max_file_size, 256 * 1024 * 1024);
        assert_eq!(c.write_buffer_size, 1024 * 1024);
        assert_eq!(c.compaction_threshold, 0.30);
        assert!(!c.enable_fsync);
        assert!(!c.direct_io);
        assert_eq!(c.hints, None);
        assert!(c.shard_count >= 1);
        c.validate().unwrap();
    }

    #[test]
    fn workload_high_throughput_matches_table() {
        let c = Config::high_throughput().build();
        assert_eq!(c.shard_count, 64);
        assert_eq!(c.max_file_size, 512 * 1024 * 1024);
        assert_eq!(c.write_buffer_size, 4 * 1024 * 1024);
        assert_eq!(c.compaction_threshold, 0.25);
        c.validate().unwrap();
    }

    #[test]
    fn workload_durable_keeps_fsync_placeholder() {
        let c = Config::durable().build();
        assert_eq!(c.shard_count, 16);
        assert_eq!(c.write_buffer_size, 64 * 1024);
        assert!(c.enable_fsync);
        c.validate().unwrap();
    }

    #[cfg(feature = "var-collections")]
    #[test]
    fn workload_large_values_has_cache_and_direct_io() {
        let c = Config::large_values().build();
        assert_eq!(c.max_file_size, 1 << 30);
        assert_eq!(c.write_buffer_size, 4 * 1024 * 1024);
        assert!(c.direct_io);
        assert_eq!(c.cache.max_size, 512 << 20);
        assert_eq!(c.cache.estimated_items, 131_072);
        c.validate().unwrap();
    }

    #[test]
    fn compaction_max_files_per_pass_default_and_override() {
        // Default preserves the historical per-pass cap (4).
        assert_eq!(Config::balanced().build().compaction_max_files_per_pass, 4);
        assert_eq!(Config::test().compaction_max_files_per_pass, 4);
        // Overridable, including 0 = unlimited.
        let c = Config::for_workload(Workload::Balanced)
            .compaction_max_files_per_pass(0)
            .build();
        assert_eq!(c.compaction_max_files_per_pass, 0);
    }

    #[test]
    fn for_workload_overrides_apply() {
        let c = Config::for_workload(Workload::Balanced)
            .shard_count(7)
            .compaction_threshold(0.5)
            .hints(true)
            .build();
        assert_eq!(c.shard_count, 7);
        assert_eq!(c.compaction_threshold, 0.5);
        assert_eq!(c.hints, Some(true));
        assert_eq!(c.max_file_size, 256 * 1024 * 1024);
    }

    #[cfg(feature = "encryption")]
    #[test]
    fn large_values_with_encryption_validates() {
        let c = Config::large_values().encryption_key([0u8; 32]).build();
        c.validate().unwrap();
    }

    #[test]
    fn iterable_defaults_false_and_overridable() {
        assert!(!Config::balanced().build().iterable);
        assert!(Config::balanced().iterable(true).build().iterable);
        assert!(!Config::test().iterable); // test() does not opt in
    }
}
