// Copyright 2023 PingCAP, Inc.

use std::{
    collections::{HashMap, HashSet},
    error::Error,
};

use online_config::OnlineConfig;
use serde::{Deserialize, Serialize};
use tikv_util::config::{AbsoluteOrPercentSize, ReadableDuration, ReadableSize};

use crate::{
    compaction::CURRENT_COMPACTOR_VERSION,
    ia::{
        manager::{validate_cache_capacity_ratio, validate_cache_capacity_shrink_step},
        util::IaConfig,
    },
    table::{
        ChecksumType, blobtable::builder::BlobTableBuildOptions,
        columnar::ColumnarTableBuildOptions, fts::FtsBuildOptions, sstable::BlockCacheType,
        vector_index::VectorIndexBuildOptions,
    },
};

pub(crate) const DEFAULT_COMPACTION_REQUEST_VERSION: u32 = CURRENT_COMPACTOR_VERSION;
pub(crate) const DEFAULT_COMPACTION_TOMBS_RATIO: f64 = 0.3;
pub(crate) const DEFAULT_COMPACTION_TOMBS_COUNT: u64 = 10000;
/// File-level reclaimable-entries ratio `(tombs + old_entries) / (entries +
/// old_entries)` that triggers a file-level GC compaction.
pub(crate) const DEFAULT_FILE_GC_COMPACTION_RATIO: f64 = 0.3;
// Each level is 10x the size of the one above, so with uniformly distributed
// keys an upper-level file overlaps about 10x its size in the level below.
// That 10x is the write amplification a normal size-triggered compaction
// already pays. We cap file-GC compaction at 15x, slightly above that
// baseline, to keep reclaim work conservative and avoid high write
// amplification from thin-spread reclaimable data.
pub(crate) const DEFAULT_FILE_GC_COMPACTION_MAX_OVERLAP_RATIO: f64 = 15.0;
pub(crate) const DEFAULT_COMPACTION_OLD_DATA_RATIO: f64 = 0.3;
pub(crate) const DEFAULT_COMPACTION_OLD_DATA_SIZE: ReadableSize = ReadableSize::mb(64);
/// Minimum interval between per-node file-level GC compaction triggers. `0`
/// disables the rate limit.
pub(crate) const DEFAULT_FILE_GC_COMPACTION_MIN_INTERVAL_SECS: u64 = 5;

/// The maximum configured memtable target size. An individual write can exceed
/// this target and will cause the memtable to be switched afterward.
pub const MEM_TABLE_MAX_SIZE: u64 = 128 * 1024 * 1024;

pub const DEFAULT_BASE_SIZE: u64 = 16 << 20; // 16MB

pub const DEFAULT_SOFT_REGION_MEM_USAGE_LIMIT_MB: u64 = 256;
pub const DEFAULT_HARD_REGION_MEM_USAGE_LIMIT_MB: u64 = 512;
pub const DEFAULT_MAX_REGION_SPEED_LIMIT_MB_PER_SEC: u64 = 50;
pub const DEFAULT_MIN_REGION_SPEED_LIMIT_MB_PER_SEC: u64 = 1;
pub const LOAD_AWARE_RATIO_LOWER_BOUND: f64 = 0.0;
pub const LOAD_AWARE_RATIO_UPPER_BOUND: f64 = 1.0;
pub const DEFAULT_REMOTE_COPROCESSOR_TOPN_MAX_PUSHDOWN_ROWS: u64 = 50_000;

pub const DEFAULT_FD_CACHE_CAPCITY: usize = 200000;

// If the number of panic regions of a table exceeds this threshold, the table
// will be auto blacklisted.
pub const DEFAULT_TABLE_AUTO_BLACKLIST_THRESHOLD: u64 = 3;
// If the number of blacklisted tables of a keyspace exceeds this threshold,
// the keyspace will be auto blacklisted.
pub const DEFAULT_KEYSPACE_AUTO_BLACKLIST_THRESHOLD: u64 = 4;

#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
#[serde(default)]
#[serde(rename_all = "kebab-case")]
pub struct PerKeyspaceConfig {
    pub keyspace: u32,

    /// The factor of default split size for the keyspace, valid range from 0.1
    /// to 10.0.
    pub split_size_factor: f64,

    /// enable blob table for the keyspace.
    pub enable_blob: bool,
}

impl Default for PerKeyspaceConfig {
    fn default() -> Self {
        Self {
            keyspace: 0,
            split_size_factor: 1.0,
            enable_blob: false,
        }
    }
}

#[derive(Clone, Serialize, Deserialize, PartialEq, Debug, OnlineConfig)]
#[serde(default)]
#[serde(rename_all = "kebab-case")]
pub struct Config {
    /// The maximum delay duration for delete range.
    #[online_config(skip)]
    pub max_del_range_delay: ReadableDuration,
    #[online_config(skip)]
    pub compaction_request_version: u32,
    /// The ratio threshold of tombstone entries to trigger compaction.
    #[online_config(skip)]
    pub compaction_tombs_ratio: f64,
    /// The number threshold of tombstone entries to trigger compaction.
    #[online_config(skip)]
    pub compaction_tombs_count: u64,
    /// File-level reclaimable-entries ratio `(tombs + old_entries) / (entries +
    /// old_entries)` that triggers a file-level GC compaction. Independent of
    /// the shard-aggregate `compaction_tombs_ratio` /
    /// `compaction_old_data_ratio`; more sensitive since any single file at
    /// this ratio triggers.
    pub file_gc_compaction_ratio: f64,
    /// Skip a file-level GC candidate when its lower-level overlap exceeds this
    /// multiple of its own size: thin-spread reclaimable data helps scans
    /// little while the merge pulls in all that lower data. Set very large
    /// to disable.
    pub file_gc_compaction_max_overlap_ratio: f64,
    /// The ratio threshold of old-version data bytes to trigger compaction.
    pub compaction_old_data_ratio: f64,
    /// The absolute old-version data size threshold to trigger compaction.
    pub compaction_old_data_size: ReadableSize,
    /// Minimum interval between file-level GC compaction triggers on a node.
    /// `0` disables the rate limit.
    pub file_gc_compaction_min_interval: ReadableDuration,
    /// Allow file-level GC to run the last-level (L3) in-place rewrite. Off by
    /// default for rolling-upgrade safety: a full drop emits an empty change
    /// set that crashes peers whose `is_move_down` lacks the
    /// empty-`top_deletes` guard. Flip on once no such peer remains.
    pub file_gc_compaction_allow_l3: bool,

    /// The remote worker address to run analyze and checksum requests.
    pub remote_worker_addr: String,

    /// The remote coprocessor address to run heavy coprocessor requests.
    pub remote_coprocessor_addr: String,

    /// The minimum number of blocks for a coprocessor request to be run on
    /// remote worker.
    pub remote_coprocessor_min_blocks_size: usize,

    /// Always send to remote coprocessor if the number of ranges reach this
    /// value.
    pub remote_coprocessor_num_ranges: usize,

    /// The minimum duration for a coprocessor request to be run on remote.
    /// Currently, it is used by DAG that matches lazy remote pattern and
    /// dynamically determines whether to run on remote or local.
    pub remote_coprocessor_min_process_duration: ReadableDuration,

    /// Maximum non-partition TopN limit executed by the remote cop-worker.
    /// Larger TopN operators receive `limit - 1` paging so the worker returns
    /// resumable source rows instead. Zero disables the TiKV-side rewrite.
    pub remote_coprocessor_topn_max_pushdown_rows: u64,

    /// If enabled, TiKV keeps coprocessor requests local when the load-aware
    /// signal is low, even if they match the existing remote offload rules.
    /// The current implementation depends on CPU signals collected through
    /// resource_control.
    ///
    /// TODO: Support online config for these load-aware knobs after #4964 is
    /// merged. That follow-up needs to wire the remote-decision path to read
    /// them dynamically from live kvengine config and preserve the coupled
    /// threshold validations during updates.
    #[online_config(skip)]
    pub remote_coprocessor_load_aware: bool,

    /// TiKV prefers local execution when the current load-aware signal
    /// (currently TiKV total CPU usage ratio) is below this threshold.
    #[online_config(skip)]
    pub remote_coprocessor_load_aware_idle_threshold: f64,

    /// TiKV allows remote offload when the current load-aware signal
    /// (currently TiKV total CPU usage ratio) is above this threshold.
    #[online_config(skip)]
    pub remote_coprocessor_load_aware_busy_threshold: f64,

    /// Requests whose estimated blocks size exceeds this threshold still
    /// offload even when the local CPU is currently idle.
    #[online_config(skip)]
    pub remote_coprocessor_load_aware_force_blocks_size: usize,

    /// Requests whose range count exceeds this higher threshold still offload
    /// even when the local CPU is currently idle.
    #[online_config(skip)]
    pub remote_coprocessor_load_aware_force_num_ranges: usize,

    /// If enabled, flush large L0 file will split into multiple files.
    #[online_config(skip)]
    pub flush_split_l0: bool,

    /// Use Crossbeam-backed memtables for serving shards. Set to false to use
    /// Arena for those memtables. This is a startup-only rollout switch.
    #[online_config(skip)]
    pub enable_crossbeam_memtable: bool,

    /// If enabled, columnar table will not be loaded. Used for troubleshooting.
    #[online_config(skip)]
    pub ignore_columnar_table_load: bool,
    /// Enable building columnar table. Default is false.
    #[online_config(skip)]
    pub build_columnar: bool,
    /// Enable building fts index. Default is false.
    #[online_config(skip)]
    pub build_fts_index: bool,
    /// Enable columnar table read. Default is false.
    #[online_config(skip)]
    pub read_columnar: bool,
    /// Enable gc lock and extra cf. Default is false.
    #[online_config(skip)]
    pub gc_lock_extra_cf: bool,

    #[online_config(skip)]
    pub txn_file_worker_pool_size: Option<usize>,

    /// Concurrency per core for loading dfs files.
    #[online_config(skip)]
    pub dfs_load_concurrency_per_core: usize,
    /// Concurrency per request (e.g. prepare change set) for loading dfs files.
    #[online_config(skip)]
    pub dfs_load_concurrency_per_request: usize,

    /// the capacity of open fd cache.
    #[online_config(skip)]
    pub fd_cache_capacity: usize,

    /// the capacity of value cache.
    #[online_config(skip)]
    pub value_cache_capacity: AbsoluteOrPercentSize,
    /// When enabled, cache hit will double check by reading from the snapshot.
    #[online_config(skip)]
    pub value_cache_double_check: bool,

    /// the capacity of table meta cache.
    #[online_config(skip)]
    pub columnar_meta_cache_capacity: AbsoluteOrPercentSize,

    /// The panic regions threshold of the table to auto blacklist.
    #[online_config(skip)]
    pub table_auto_blacklist_threshold: u64,

    /// The panic tables threshold of the keyspace to auto blacklist.
    #[online_config(skip)]
    pub keyspace_auto_blacklist_threshold: u64,

    /// Memory ratio for required DFS file loading (0.0-1.0), used to
    /// calculate its memory limit from system memory. Best-effort cache reads
    /// use spare process-memory headroom, with their burst ceiling and bounded
    /// unreserved allowance derived from this value.
    #[online_config(skip)]
    pub dfs_load_memory_ratio: f64,

    /// Open-file cache TTL (seconds).
    /// Controls how long an idle opened file (FD) is kept in the
    /// kvengine file cache to avoid reopen overhead; once a file is idle
    /// longer than this (and not pinned by snapshot/recovery), background
    /// maintenance may close it and reclaim local resources. This does NOT
    /// delete data files; it only evicts/closes cached open handles.
    /// Default: 1800s (30 minutes).
    #[online_config(skip)]
    pub file_ttl: u64,

    #[online_config(skip)]
    pub checksum_type: ChecksumType,

    #[online_config(skip)]
    pub block_cache_type: BlockCacheType,

    /// Support multiple disks. files will be distributed to different dirs by
    /// hash.
    #[online_config(skip)]
    pub extra_dirs: Vec<String>,

    #[online_config(skip)]
    pub per_keyspace_configs: Vec<PerKeyspaceConfig>,

    /// Enable IA by setting `ia.mem_cap > 0 && ia.disk_cap > 0`.
    #[online_config(submodule)]
    pub ia: IaConfig,

    #[online_config(skip)]
    pub blob_table_build_options: BlobTableBuildOptions,

    #[online_config(skip)]
    pub columnar_table_build_options: ColumnarTableBuildOptions,

    #[online_config(skip)]
    pub vector_index_build_options: VectorIndexBuildOptions,

    #[online_config(skip)]
    pub fts_build_options: FtsBuildOptions,
    // Note: Fields of simple (not structure) type can not be the last. Otherwise serializing the
    // config will meet the "ValueAfterTable" error.
    // See https://docs.rs/toml/0.5.11/toml/ser/enum.Error.html#variant.ValueAfterTable.
}

impl Default for Config {
    fn default() -> Self {
        Self {
            max_del_range_delay: ReadableDuration::secs(3600),
            compaction_request_version: DEFAULT_COMPACTION_REQUEST_VERSION,
            compaction_tombs_ratio: DEFAULT_COMPACTION_TOMBS_RATIO,
            compaction_tombs_count: DEFAULT_COMPACTION_TOMBS_COUNT,
            file_gc_compaction_ratio: DEFAULT_FILE_GC_COMPACTION_RATIO,
            file_gc_compaction_max_overlap_ratio: DEFAULT_FILE_GC_COMPACTION_MAX_OVERLAP_RATIO,
            compaction_old_data_ratio: DEFAULT_COMPACTION_OLD_DATA_RATIO,
            compaction_old_data_size: DEFAULT_COMPACTION_OLD_DATA_SIZE,
            file_gc_compaction_min_interval: ReadableDuration::secs(
                DEFAULT_FILE_GC_COMPACTION_MIN_INTERVAL_SECS,
            ),
            file_gc_compaction_allow_l3: false,
            table_auto_blacklist_threshold: DEFAULT_TABLE_AUTO_BLACKLIST_THRESHOLD,
            keyspace_auto_blacklist_threshold: DEFAULT_KEYSPACE_AUTO_BLACKLIST_THRESHOLD,
            remote_worker_addr: "".to_string(),
            remote_coprocessor_addr: "".to_string(),
            remote_coprocessor_min_blocks_size: 32 * 1024 * 1024,
            remote_coprocessor_num_ranges: 2048,
            remote_coprocessor_min_process_duration: ReadableDuration::millis(30),
            remote_coprocessor_topn_max_pushdown_rows:
                DEFAULT_REMOTE_COPROCESSOR_TOPN_MAX_PUSHDOWN_ROWS,
            remote_coprocessor_load_aware: false,
            remote_coprocessor_load_aware_idle_threshold: 0.3,
            remote_coprocessor_load_aware_busy_threshold: 0.5,
            remote_coprocessor_load_aware_force_blocks_size: 128 * 1024 * 1024,
            remote_coprocessor_load_aware_force_num_ranges: 2048 * 4,
            flush_split_l0: true,
            enable_crossbeam_memtable: true,
            txn_file_worker_pool_size: None,
            // 8GB memory per core, 64 * 16MB files consumes 1GB at max.
            dfs_load_concurrency_per_core: 64,
            dfs_load_concurrency_per_request: 16,
            // Default memory ratio: 25% of system memory.
            dfs_load_memory_ratio: 0.25,
            fd_cache_capacity: DEFAULT_FD_CACHE_CAPCITY,
            value_cache_capacity: 0.into(),
            value_cache_double_check: false,
            columnar_meta_cache_capacity: AbsoluteOrPercentSize::Percent(0.2),
            checksum_type: ChecksumType::Crc32,
            block_cache_type: BlockCacheType::Quick,
            blob_table_build_options: Default::default(),
            per_keyspace_configs: vec![],
            ia: IaConfig {
                mem_cap: AbsoluteOrPercentSize::Percent(15.0),
                disk_cap: AbsoluteOrPercentSize::Percent(70.0),
                dynamic_capacity: true,
                ..Default::default()
            },
            extra_dirs: vec![],
            columnar_table_build_options: Default::default(),
            vector_index_build_options: Default::default(),
            fts_build_options: Default::default(),
            ignore_columnar_table_load: false,
            build_columnar: false,
            build_fts_index: false,
            read_columnar: false,
            gc_lock_extra_cf: true,
            file_ttl: 1800, // 1800s, i.e., 30 minutes
        }
    }
}

impl Config {
    pub fn validate(&self) -> Result<(), Box<dyn Error>> {
        if !(LOAD_AWARE_RATIO_LOWER_BOUND..LOAD_AWARE_RATIO_UPPER_BOUND)
            .contains(&self.remote_coprocessor_load_aware_idle_threshold)
        {
            return Err(
                "remote-coprocessor-load-aware-idle-threshold must be in [0.0, 1.0)".into(),
            );
        }
        if !(LOAD_AWARE_RATIO_LOWER_BOUND..=LOAD_AWARE_RATIO_UPPER_BOUND)
            .contains(&self.remote_coprocessor_load_aware_busy_threshold)
            || self.remote_coprocessor_load_aware_busy_threshold == LOAD_AWARE_RATIO_LOWER_BOUND
        {
            return Err(
                "remote-coprocessor-load-aware-busy-threshold must be in (0.0, 1.0]".into(),
            );
        }
        if self.remote_coprocessor_load_aware_idle_threshold
            >= self.remote_coprocessor_load_aware_busy_threshold
        {
            return Err(
                "remote-coprocessor-load-aware-idle-threshold must be smaller than \
                 remote-coprocessor-load-aware-busy-threshold"
                    .into(),
            );
        }
        if self.remote_coprocessor_load_aware_force_blocks_size
            < self.remote_coprocessor_min_blocks_size
        {
            return Err(
                "remote-coprocessor-load-aware-force-blocks-size must be greater than or equal to \
                 remote-coprocessor-min-blocks-size"
                    .into(),
            );
        }
        if self.remote_coprocessor_load_aware_force_num_ranges < self.remote_coprocessor_num_ranges
        {
            return Err(
                "remote-coprocessor-load-aware-force-num-ranges must be greater than or equal to \
                 remote-coprocessor-num-ranges"
                    .into(),
            );
        }
        let mut keyspace_set = HashSet::new();
        for cfg in &self.per_keyspace_configs {
            if cfg.split_size_factor > 10.0 || cfg.split_size_factor < 0.1 {
                return Err(format!(
                    "invalid split size factor {} for keyspace {}",
                    cfg.split_size_factor, cfg.keyspace
                )
                .into());
            }
            if !keyspace_set.insert(cfg.keyspace) {
                return Err(format!("duplicate keyspace {}", cfg.keyspace).into());
            }
        }
        if self.ia.io_rate_limiter_refill_period.0.is_zero() {
            return Err("ia.io-rate-limiter-refill-period must be positive".into());
        }
        if self.ia.ia_to_standard_transition_max_regions == 0 {
            return Err("kvengine.ia.ia-to-standard-transition-max-regions should be >= 1".into());
        }
        if self.ia.ia_to_standard_transition_wait_timeout.0.is_zero() {
            return Err("kvengine.ia.ia-to-standard-transition-wait-timeout should be > 0".into());
        }
        if let Err(err) = validate_cache_capacity_ratio(self.ia.cache_cap_to_total_data_size_ratio)
        {
            return Err(err.to_string().into());
        }
        if let Err(err) = validate_cache_capacity_shrink_step(self.ia.cache_capacity_shrink_step) {
            return Err(err.to_string().into());
        }
        if !self.compaction_old_data_ratio.is_finite()
            || self.compaction_old_data_ratio <= 0.0
            || self.compaction_old_data_ratio > 1.0
        {
            return Err("compaction-old-data-ratio must be finite and in (0.0, 1.0]".into());
        }
        // Reject values that turn the trigger into a runaway (`0.0`, `NaN`)
        // or a silent no-op (`> 1.0`, infinity).
        if !self.file_gc_compaction_ratio.is_finite()
            || self.file_gc_compaction_ratio <= 0.0
            || self.file_gc_compaction_ratio > 1.0
        {
            return Err("file-gc-compaction-ratio must be finite and in (0.0, 1.0]".into());
        }
        // `0.0` would skip every candidate with any lower overlap; `NaN` /
        // infinity would break the guard. No upper bound: very large
        // effectively disables it.
        if !self.file_gc_compaction_max_overlap_ratio.is_finite()
            || self.file_gc_compaction_max_overlap_ratio <= 0.0
        {
            return Err("file-gc-compaction-max-overlap-ratio must be finite and > 0.0".into());
        }
        Ok(())
    }

    pub fn get_per_keyspace_configs(&self) -> HashMap<u32, PerKeyspaceConfig> {
        let mut map = HashMap::new();
        for cfg in &self.per_keyspace_configs {
            map.insert(cfg.keyspace, cfg.clone());
        }
        map
    }
}

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

    #[test]
    fn test_remote_coprocessor_topn_max_pushdown_rows_default() {
        assert_eq!(
            Config::default().remote_coprocessor_topn_max_pushdown_rows,
            DEFAULT_REMOTE_COPROCESSOR_TOPN_MAX_PUSHDOWN_ROWS
        );
    }

    #[test]
    fn test_validate_remote_coprocessor_load_aware_thresholds() {
        let mut config = Config::default();
        config.remote_coprocessor_load_aware_idle_threshold = 0.8;
        config.remote_coprocessor_load_aware_busy_threshold = 0.7;
        assert!(config.validate().is_err());

        config.remote_coprocessor_load_aware_idle_threshold = -0.1;
        config.remote_coprocessor_load_aware_busy_threshold = 0.7;
        assert!(config.validate().is_err());

        config.remote_coprocessor_load_aware_idle_threshold = 0.5;
        config.remote_coprocessor_load_aware_busy_threshold = 1.1;
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_validate_remote_coprocessor_force_offload_threshold() {
        let mut config = Config::default();
        config.remote_coprocessor_load_aware_force_blocks_size =
            config.remote_coprocessor_min_blocks_size - 1;
        assert!(config.validate().is_err());

        config.remote_coprocessor_load_aware_force_blocks_size =
            config.remote_coprocessor_min_blocks_size;
        config.validate().unwrap();

        config.remote_coprocessor_load_aware_force_num_ranges =
            config.remote_coprocessor_num_ranges - 1;
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_crossbeam_memtable_is_enabled_and_startup_only() {
        let cfg = Config::default();
        assert!(cfg.enable_crossbeam_memtable);

        let mut disabled = cfg.clone();
        disabled.enable_crossbeam_memtable = false;
        assert!(cfg.diff(&disabled).is_empty());
    }

    // Lock the names of online-configurable fields. Renaming or removing
    // any listed field changes the diff key set and breaks the test.
    #[test]
    fn test_online_configurable_field_set_is_locked() {
        let cfg = Config::default();
        let mut new_cfg = cfg.clone();
        new_cfg.remote_worker_addr = "x".to_string();
        new_cfg.remote_coprocessor_addr = "x".to_string();
        new_cfg.remote_coprocessor_min_blocks_size = cfg.remote_coprocessor_min_blocks_size + 1;
        new_cfg.remote_coprocessor_num_ranges = cfg.remote_coprocessor_num_ranges + 1;
        new_cfg.remote_coprocessor_min_process_duration =
            cfg.remote_coprocessor_min_process_duration + ReadableDuration::millis(1);
        new_cfg.remote_coprocessor_topn_max_pushdown_rows =
            cfg.remote_coprocessor_topn_max_pushdown_rows + 1;
        new_cfg.compaction_old_data_ratio = 0.5;
        new_cfg.compaction_old_data_size = ReadableSize::mb(128);
        new_cfg.file_gc_compaction_ratio = 0.5;
        new_cfg.file_gc_compaction_max_overlap_ratio = 10.0;
        let got: HashSet<String> = cfg.diff(&new_cfg).into_keys().collect();
        let want: HashSet<String> = [
            "remote_worker_addr",
            "remote_coprocessor_addr",
            "remote_coprocessor_min_blocks_size",
            "remote_coprocessor_num_ranges",
            "remote_coprocessor_min_process_duration",
            "remote_coprocessor_topn_max_pushdown_rows",
            "compaction_old_data_ratio",
            "compaction_old_data_size",
            "file_gc_compaction_ratio",
            "file_gc_compaction_max_overlap_ratio",
        ]
        .iter()
        .map(|s| s.to_string())
        .collect();
        assert_eq!(got, want);
    }

    #[test]
    fn test_validate_rejects_invalid_ia_to_standard_transition_config() {
        let mut cfg = Config::default();
        cfg.ia.ia_to_standard_transition_max_regions = 0;
        let err = cfg.validate().unwrap_err().to_string();
        assert!(err.contains("ia-to-standard-transition-max-regions"));

        let mut cfg = Config::default();
        cfg.ia.ia_to_standard_transition_wait_timeout = ReadableDuration::secs(0);
        let err = cfg.validate().unwrap_err().to_string();
        assert!(err.contains("ia-to-standard-transition-wait-timeout"));
    }

    #[test]
    fn test_validate_rejects_invalid_ia_cache_capacity_ratio() {
        for invalid_ratio in [-0.1, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
            let mut cfg = Config::default();
            cfg.ia.cache_cap_to_total_data_size_ratio = invalid_ratio;
            let err = cfg.validate().unwrap_err().to_string();
            assert!(
                err.contains("cache-cap-to-total-data-size-ratio"),
                "unexpected error for {}: {}",
                invalid_ratio,
                err
            );
        }

        for valid_ratio in [-0.0, 0.0, f64::MIN_POSITIVE, 0.75, f64::MAX] {
            let mut cfg = Config::default();
            cfg.ia.cache_cap_to_total_data_size_ratio = valid_ratio;
            cfg.validate().unwrap();
        }
    }

    #[test]
    fn test_validate_rejects_zero_ia_cache_capacity_shrink_step() {
        let mut cfg = Config::default();
        cfg.ia.cache_capacity_shrink_step = ReadableSize(0);
        let err = cfg.validate().unwrap_err().to_string();
        assert!(err.contains("cache-capacity-shrink-step"));

        cfg.ia.cache_capacity_shrink_step = ReadableSize(1);
        cfg.validate().unwrap();
    }

    #[test]
    fn test_validate_rejects_invalid_compaction_old_data_ratio() {
        for invalid_ratio in [0.0, -0.1, 1.1, f64::NAN, f64::INFINITY] {
            let mut cfg = Config::default();
            cfg.compaction_old_data_ratio = invalid_ratio;
            let err = cfg.validate().unwrap_err().to_string();
            assert!(
                err.contains("compaction-old-data-ratio"),
                "unexpected error for {}: {}",
                invalid_ratio,
                err
            );
        }

        for valid_ratio in [f64::MIN_POSITIVE, 0.3, 1.0] {
            let mut cfg = Config::default();
            cfg.compaction_old_data_ratio = valid_ratio;
            cfg.validate().unwrap();
        }
    }

    #[test]
    fn test_validate_rejects_invalid_file_gc_compaction_ratio() {
        for invalid_ratio in [0.0, -0.1, 1.1, f64::NAN, f64::INFINITY] {
            let mut cfg = Config::default();
            cfg.file_gc_compaction_ratio = invalid_ratio;
            let err = cfg.validate().unwrap_err().to_string();
            assert!(
                err.contains("file-gc-compaction-ratio"),
                "unexpected error for {}: {}",
                invalid_ratio,
                err
            );
        }

        for valid_ratio in [f64::MIN_POSITIVE, 0.3, 1.0] {
            let mut cfg = Config::default();
            cfg.file_gc_compaction_ratio = valid_ratio;
            cfg.validate().unwrap();
        }
    }

    #[test]
    fn test_validate_rejects_invalid_file_gc_compaction_max_overlap_ratio() {
        for invalid_ratio in [0.0, -1.0, f64::NAN, f64::INFINITY] {
            let mut cfg = Config::default();
            cfg.file_gc_compaction_max_overlap_ratio = invalid_ratio;
            let err = cfg.validate().unwrap_err().to_string();
            assert!(
                err.contains("file-gc-compaction-max-overlap-ratio"),
                "unexpected error for {}: {}",
                invalid_ratio,
                err
            );
        }

        for valid_ratio in [f64::MIN_POSITIVE, 50.0, 1e12] {
            let mut cfg = Config::default();
            cfg.file_gc_compaction_max_overlap_ratio = valid_ratio;
            cfg.validate().unwrap();
        }
    }
}
