//! How to handle config files and arguments.

// TODO: Break this apart or do something a bit smarter.

pub mod args;
pub mod config;
mod error;

use std::{
    convert::TryInto,
    fs,
    io::Write,
    path::{Path, PathBuf},
    str::FromStr,
    time::{Duration, Instant},
};

use anyhow::{Context, Result};
pub use config::Config;
use config::style::Styles;
use data::TemperatureType;
pub(crate) use error::{OptionError, OptionResult};
use indexmap::IndexSet;
use regex::Regex;
use rustc_hash::{FxHashMap, FxHashSet};
#[cfg(feature = "battery")]
use starship_battery::Manager;

use self::{
    args::BottomArgs,
    config::{IgnoreList, StringOrNum, layout::Row},
};
use crate::{
    app::{filter::Filter, layout_manager::*, *},
    canvas::components::time_series::LegendPosition,
    components::time_series::TimeseriesConfig,
    constants::*,
    utils::data_units::DataUnit,
    widgets::*,
};

/// Macro to check whether a flag is enabled, either as an arg or in the config file.
macro_rules! is_flag_enabled {
    ($flag_name:ident, $arg:expr, $config:expr) => {
        if $arg.$flag_name {
            true
        } else if let Some(flags) = &$config.flags {
            flags.$flag_name.unwrap_or(false)
        } else {
            false
        }
    };

    ($cmd_flag:literal, $cfg_flag:ident, $matches:expr, $config:expr) => {
        if $matches.get_flag($cmd_flag) {
            true
        } else if let Some(flags) = &$config.flags {
            flags.$cfg_flag.unwrap_or(false)
        } else {
            false
        }
    };
}

/// A version of [`is_flag_enabled`] which instead expects the user to pass
/// in `config_section`, which is the section the flag is located, rather than
/// defaulting to `config.flags` where `config` is passed in.
macro_rules! is_flag_enabled_in {
    ($flag_name:ident, $arg:expr, $config_section:expr) => {
        if $arg.$flag_name {
            true
        } else if let Some(options) = &$config_section {
            options.$flag_name.unwrap_or(false)
        } else {
            false
        }
    };
}

/// A version of [`is_flag_enabled`] which should be used to deprecate old config flags.
macro_rules! enabled_option_with_deprecated {
    ($arg:expr, $config:expr, $section:ident . $field:ident, $flags:ident . $deprecated_flag:ident $(,)?) => {
        if $arg {
            true
        } else if let Some(section) = &$config.$section {
            section.$field.unwrap_or(false)
        } else if let Some(flags) = &$config.flags {
            flags
                .$deprecated_flag
                .inspect(|_| {
                    deprecated_warning(stringify!($deprecated_flag), stringify!($section.$field))
                })
                .unwrap_or(false)
        } else {
            false
        }
    };
    ($arg:expr, $config:expr, $section:ident . $field:ident, $flags:ident . $deprecated_flag:ident, $alias:literal $(,)?) => {
        if $arg {
            true
        } else if let Some(section) = &$config.$section {
            section.$field.unwrap_or(false)
        } else if let Some(flags) = &$config.flags {
            flags
                .$deprecated_flag
                .inspect(|_| {
                    deprecated_warning_with_alias(
                        stringify!($deprecated_flag),
                        stringify!($section.$field),
                        $alias,
                    )
                })
                .unwrap_or(false)
        } else {
            false
        }
    };
}

/// Get the value of a field for a specific section in the config file if set. If not set, the default is used.
macro_rules! config_or_default {
    ($config:expr, $section:ident . $field:ident) => {
        $config
            .$section
            .as_ref()
            .map(|s| s.$field)
            .unwrap_or_default()
    };
}

/// Get the value of a field for a specific section in the config file if set. If not set,
/// the provided default is used.
macro_rules! config_or {
    ($config:expr, $section:ident . $field:ident, $default:expr) => {
        $config
            .$section
            .as_ref()
            .and_then(|s| s.$field)
            .unwrap_or($default)
    };
}

/// The default config file sub-path.
const DEFAULT_CONFIG_FILE_LOCATION: &str = "bottom/bottom.toml";

/// Returns the config path to use. If `override_config_path` is specified, then
/// we will use that. If not, then return the "default" config path, which is:
///
/// - If a path already exists at `<HOME>/bottom/bottom.toml`, then use that for
///   legacy reasons.
/// - Otherwise, use `<SYSTEM_CONFIG_FOLDER>/bottom/bottom.toml`.
///
/// For more details on this, see [dirs](https://docs.rs/dirs/latest/dirs/fn.config_dir.html)'
/// documentation.
///
/// XXX: For macOS, we additionally will manually check `$XDG_CONFIG_HOME` as
/// well first before falling back to `dirs`.
fn get_config_path(override_config_path: Option<&Path>) -> Option<PathBuf> {
    if let Some(conf_loc) = override_config_path {
        return Some(conf_loc.to_path_buf());
    } else if let Some(home_path) = dirs::home_dir() {
        let mut old_home_path = home_path;
        old_home_path.push(".config/");
        old_home_path.push(DEFAULT_CONFIG_FILE_LOCATION);
        if let Ok(res) = old_home_path.try_exists()
            && res
        {
            // We used to create it at `<HOME>/DEFAULT_CONFIG_FILE_PATH`, but changed it
            // to be more correct later. However, for legacy reasons, if it already exists,
            // use the old one.
            return Some(old_home_path);
        }
    }

    let config_path = dirs::config_dir().map(|mut path| {
        path.push(DEFAULT_CONFIG_FILE_LOCATION);
        path
    });

    if cfg!(target_os = "macos")
        && let Ok(xdg_config_path) = std::env::var("XDG_CONFIG_HOME")
        && !xdg_config_path.is_empty()
    {
        // If XDG_CONFIG_HOME exists and is non-empty, _but_ we previously used the
        // Library-based path for a config and it exists, then use that
        // instead for backwards-compatibility.
        if let Some(old_macos_path) = &config_path
            && let Ok(res) = old_macos_path.try_exists()
            && res
        {
            return config_path;
        }

        // Otherwise, try and use the XDG_CONFIG_HOME-based path.
        let mut cfg_path = PathBuf::new();
        cfg_path.push(xdg_config_path);
        cfg_path.push(DEFAULT_CONFIG_FILE_LOCATION);

        return Some(cfg_path);
    }

    config_path
}

fn create_config_at_path(path: &Path) -> anyhow::Result<Config> {
    if let Some(parent_path) = path.parent() {
        fs::create_dir_all(parent_path)?;
    }

    let mut file = fs::File::create(path)?;
    file.write_all(CONFIG_TEXT.as_bytes())?;

    Ok(Config::default())
}

/// Get the config at `config_path`. If there is no config file at the specified
/// path, it will try to create a new file with the default settings, and return
/// the default config.
///
/// We're going to use the following behaviour on when we'll return an error
/// rather than just "silently" continuing on:
/// - If the user passed in a path explicitly, then we will be loud and error
///   out.
/// - If the user does NOT pass in a path explicitly, then just show a warning,
///   but continue. This is in case they do not want to write a default config
///   file at the XDG locations, for example.
pub(crate) fn get_or_create_config(config_path: Option<&Path>) -> anyhow::Result<Config> {
    let adjusted_config_path = get_config_path(config_path);

    match &adjusted_config_path {
        Some(path) => {
            if let Ok(config_string) = fs::read_to_string(path) {
                Ok(toml_edit::de::from_str(&config_string)?)
            } else {
                match create_config_at_path(path) {
                    Ok(cfg) => Ok(cfg),
                    Err(err) => {
                        if config_path.is_some() {
                            Err(err.context(format!(
                                "bottom could not create a new config file at '{}'.",
                                path.display()
                            )))
                        } else {
                            indoc::eprintdoc!(
                                "Note: bottom couldn't create a default config file at '{}', and the \
                                application has fallen back to the default configuration.

                                Caused by:
                                    {err}
                                ",
                                path.display()
                            );

                            Ok(Config::default())
                        }
                    }
                }
            }
        }
        None => {
            // If we somehow don't have any config path, then just assume the default config
            // but don't write to any file.
            //
            // TODO: For now, just print a message to stderr indicating this. In the future,
            // probably show in-app (too).

            eprintln!(
                "Note: bottom couldn't find a location to create or read a config file, so \
                the application has fallen back to the default configuration. \
                This could be for a variety of reasons, such as issues with file permissions."
            );

            Ok(Config::default())
        }
    }
}

/// Initialize the app.
pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomLayout, Styles)> {
    use BottomWidgetType::*;

    // Since everything takes a reference, but we want to take ownership here to
    // drop matches/config later...
    let args = &args;
    let config = &config;

    let styling = Styles::new(args, config)?;

    let (widget_layout, default_widget_id, default_widget_type_option) =
        get_widget_layout(args, config)
            .context("Found an issue while trying to build the widget layout.")?;

    let retention_ms = get_retention(args, config)?;
    let autohide_time = is_flag_enabled!(autohide_time, args.general, config);
    let default_time_value = get_default_time_value(args, config, retention_ms)?;

    let use_basic_mode = is_flag_enabled!(basic, args.general, config);
    let expanded = is_flag_enabled!(expanded, args.general, config);

    #[cfg(feature = "zfs")]
    let free_arc = enabled_option_with_deprecated!(
        args.memory.free_arc,
        config,
        memory_graph.free_arc,
        flags.free_arc,
        "memory.free_arc",
    );

    // For processes
    let is_grouped = enabled_option_with_deprecated!(
        args.process.group_processes,
        config,
        processes.default_grouped,
        flags.group_processes,
    );
    let is_case_sensitive = enabled_option_with_deprecated!(
        args.process.case_sensitive,
        config,
        processes.case_sensitive,
        flags.case_sensitive,
    );
    let is_match_whole_word = enabled_option_with_deprecated!(
        args.process.whole_word,
        config,
        processes.whole_word,
        flags.whole_word,
    );
    let is_use_regex =
        enabled_option_with_deprecated!(args.process.regex, config, processes.regex, flags.regex,);
    let is_default_tree = enabled_option_with_deprecated!(
        args.process.tree,
        config,
        processes.default_tree,
        flags.tree,
    );
    let is_default_command = enabled_option_with_deprecated!(
        args.process.process_command,
        config,
        processes.process_command,
        flags.process_command,
    );
    #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))]
    let is_advanced_kill = !(enabled_option_with_deprecated!(
        args.process.disable_advanced_kill,
        config,
        processes.disable_advanced_kill,
        flags.disable_advanced_kill,
    ));
    let is_read_only = is_flag_enabled!(read_only, args.process, config);
    #[cfg(target_os = "linux")]
    let hide_k_threads = enabled_option_with_deprecated!(
        args.process.hide_k_threads,
        config,
        processes.hide_k_threads,
        flags.hide_k_threads,
    );

    let process_memory_as_value = enabled_option_with_deprecated!(
        args.process.process_memory_as_value,
        config,
        processes.default_memory_value,
        flags.process_memory_as_value,
    );
    let is_default_tree_collapsed = enabled_option_with_deprecated!(
        args.process.tree_collapse,
        config,
        processes.tree_collapse,
        flags.tree_collapse,
    );

    // For CPU
    let default_cpu_selection = get_default_cpu_selection(args, config);

    let mut widget_map = FxHashMap::default();
    let mut cpu_state_map: FxHashMap<u64, CpuWidgetState> = FxHashMap::default();
    let mut mem_state_map: FxHashMap<u64, MemWidgetState> = FxHashMap::default();
    let mut net_state_map: FxHashMap<u64, NetWidgetState> = FxHashMap::default();
    let mut proc_state_map: FxHashMap<u64, ProcWidgetState> = FxHashMap::default();
    let mut temp_state_map: FxHashMap<u64, TempWidgetState> = FxHashMap::default();
    let mut temp_graph_state_map: FxHashMap<u64, TempGraphWidgetState> = FxHashMap::default();
    let mut disk_state_map: FxHashMap<u64, DiskTableWidget> = FxHashMap::default();
    let mut disk_io_graph_state_map: FxHashMap<u64, DiskIoGraphWidgetState> = FxHashMap::default();
    let mut battery_state_map: FxHashMap<u64, BatteryWidgetState> = FxHashMap::default();

    let autohide_timer = if autohide_time {
        Some(Instant::now())
    } else {
        None
    };

    let mut initial_widget_id: u64 = default_widget_id;
    let mut initial_widget_type = Proc;
    let is_custom_layout = config.row.is_some();
    let mut used_widget_set = FxHashSet::default();

    let network_unit_type = get_network_unit_type(args, config);
    let network_scale_type = get_network_scale_type(args, config);
    // Use + update this again after deprecation
    // let network_use_binary_prefix = is_flag_enabled!(network_use_binary_prefix, args.network, config);
    let network_use_binary_prefix = enabled_option_with_deprecated!(
        args.network.network_use_binary_prefix,
        config,
        network_graph.use_binary_prefix,
        flags.network_use_binary_prefix,
        "network.use_binary_prefix",
    );
    let network_show_packets =
        is_flag_enabled_in!(show_packets, args.network, config.network_graph);

    let proc_columns: Option<IndexSet<ProcWidgetColumn>> = {
        config.processes.as_ref().and_then(|cfg| {
            if cfg.columns.is_empty() {
                None
            } else {
                // TODO: Should we be using an indexmap? Or maybe allow dupes.
                Some(IndexSet::from_iter(
                    cfg.columns.iter().map(ProcWidgetColumn::from),
                ))
            }
        })
    };

    let network_legend_position = get_network_legend_position(args, config)?;
    let memory_legend_position = get_memory_legend_position(args, config)?;
    let temperature_legend_position = get_temperature_legend_position(config)?;
    let disk_io_legend_position = get_disk_io_legend_position(config)?;
    let disk_io_name_filter = match &config.disk_io_graph {
        Some(cfg) => get_ignore_list(&cfg.name_filter)
            .context("Update 'disk_io_graph.name_filter' in your config file")?,
        None => None,
    };
    let disk_show_unmounted = config
        .disk
        .as_ref()
        .and_then(|cfg| cfg.include_unmounted)
        .unwrap_or(false);
    let disk_io_graph_show_unmounted = config
        .disk_io_graph
        .as_ref()
        .and_then(|cfg| cfg.include_unmounted)
        .unwrap_or(false);

    let enable_cache_memory = get_enable_cache_memory(args, config);
    let enable_gpu = get_enable_gpu(args, config);

    // TODO: Can probably just reuse the options struct.
    let app_config_fields = AppConfigFields {
        update_rate: get_update_rate(args, config)?,
        temperature_type: get_temperature(args, config)
            .context("Update 'temperature_type' in your config file.")?,
        show_average_cpu: get_show_average_cpu(args, config),
        show_cpu_decimal: config_or!(config, cpu.show_decimal, false),
        use_dot: is_flag_enabled!(dot_marker, args.general, config),
        cpu_left_legend: enabled_option_with_deprecated!(
            args.cpu.cpu_left_legend,
            config,
            cpu.left_legend,
            flags.cpu_left_legend,
        ),
        use_current_cpu_total: enabled_option_with_deprecated!(
            args.process.current_usage,
            config,
            processes.current_usage,
            flags.current_usage,
        ),
        unnormalized_cpu: enabled_option_with_deprecated!(
            args.process.unnormalized_cpu,
            config,
            processes.unnormalized_cpu,
            flags.unnormalized_cpu,
        ),
        get_process_threads: is_flag_enabled_in!(get_threads, args.process, config.processes),
        use_basic_mode,
        default_time_value,
        time_interval: get_time_interval(args, config, retention_ms)?,
        hide_time: is_flag_enabled!(hide_time, args.general, config),
        autohide_time,
        use_old_network_legend: is_flag_enabled!(use_old_network_legend, args.network, config),
        table_gap: config_or_default!(config, flags.table_gap),
        disable_click: is_flag_enabled!(disable_click, args.general, config),
        disable_keys: is_flag_enabled!(disable_keys, args.general, config),
        enable_gpu,
        short_gpu_names: get_short_gpu_names(args, config),
        enable_cache_memory,
        show_table_scroll_position: is_flag_enabled!(
            show_table_scroll_position,
            args.general,
            config
        ),
        show_table_scroll_bar: config_or!(config, flags.show_table_scroll_bar, false),
        #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))]
        is_advanced_kill,
        is_read_only,
        #[cfg(target_os = "linux")]
        hide_k_threads,
        memory_legend_position,
        network_legend_position,
        network_scale_type,
        network_unit_type,
        network_use_binary_prefix,
        network_show_packets,
        retention_ms,
        dedicated_average_row: enabled_option_with_deprecated!(
            false,
            config,
            cpu.basic_average_cpu_row,
            flags.average_cpu_row,
        ),
        default_tree_collapse: is_default_tree_collapsed,
        #[cfg(feature = "zfs")]
        free_arc,
        default_temp_sort_column: config
            .temperature
            .as_ref()
            .and_then(|cfg| cfg.default_sort.to_owned()),
        default_disk_sort_column: config
            .disk
            .as_ref()
            .and_then(|cfg| cfg.default_sort.to_owned()),
        temperature_legend_position,
        disk_io_legend_position,
        disk_show_unmounted,
        disk_io_graph_show_unmounted,
    };

    let process_default_sort = match &args.process.process_default_sort {
        Some(name) => Some(ProcColumn::parse_column_name(name).ok_or_else(|| {
            OptionError::arg(format!(
                "'{name}' is not a valid process column for '--process_default_sort'"
            ))
        })?),
        None => config.processes.as_ref().and_then(|cfg| cfg.default_sort),
    };

    let ts_config = TimeseriesConfig {
        time_interval: app_config_fields.time_interval,
        retention_ms: app_config_fields.retention_ms,
        autohide_time: app_config_fields.autohide_time,
        default_time_value: app_config_fields.default_time_value,
    };

    let table_config = ProcTableConfig {
        is_case_sensitive,
        is_match_whole_word,
        is_use_regex,
        show_memory_as_values: process_memory_as_value,
        is_command: is_default_command,
        default_sort: process_default_sort,
    };

    for row in &widget_layout.rows {
        for col in &row.children {
            for col_row in &col.children {
                for widget in &col_row.children {
                    widget_map.insert(widget.widget_id, widget.clone());
                    if let Some(default_widget_type) = &default_widget_type_option
                        && (!is_custom_layout || use_basic_mode)
                    {
                        match widget.widget_type {
                            BasicCpu => {
                                if let Cpu = *default_widget_type {
                                    initial_widget_id = widget.widget_id;
                                    initial_widget_type = Cpu;
                                }
                            }
                            BasicMem => {
                                if let Mem = *default_widget_type {
                                    initial_widget_id = widget.widget_id;
                                    initial_widget_type = Cpu;
                                }
                            }
                            BasicNet => {
                                if let Net = *default_widget_type {
                                    initial_widget_id = widget.widget_id;
                                    initial_widget_type = Cpu;
                                }
                            }
                            _ => {
                                if *default_widget_type == widget.widget_type {
                                    initial_widget_id = widget.widget_id;
                                    initial_widget_type = widget.widget_type.clone();
                                }
                            }
                        }
                    }

                    used_widget_set.insert(widget.widget_type.clone());

                    match widget.widget_type {
                        Cpu => {
                            cpu_state_map.insert(
                                widget.widget_id,
                                CpuWidgetState::new(
                                    &app_config_fields,
                                    default_cpu_selection,
                                    autohide_timer,
                                    &styling,
                                ),
                            );
                        }
                        Mem => {
                            mem_state_map.insert(
                                widget.widget_id,
                                MemWidgetState::init(ts_config, autohide_timer),
                            );
                        }
                        Net => {
                            net_state_map.insert(
                                widget.widget_id,
                                NetWidgetState::init(ts_config, autohide_timer),
                            );
                        }
                        Proc => {
                            let mode = if is_grouped {
                                ProcWidgetMode::Grouped
                            } else if is_default_tree {
                                ProcWidgetMode::Tree(TreeCollapsed::new(is_default_tree_collapsed))
                            } else {
                                ProcWidgetMode::Normal
                            };

                            proc_state_map.insert(
                                widget.widget_id,
                                ProcWidgetState::new(
                                    &app_config_fields,
                                    mode,
                                    table_config,
                                    &styling,
                                    &proc_columns,
                                ),
                            );
                        }
                        Disk => {
                            disk_state_map.insert(
                                widget.widget_id,
                                DiskTableWidget::new(
                                    &app_config_fields,
                                    &styling,
                                    config.disk.as_ref().and_then(|cfg| cfg.columns.as_deref()),
                                    disk_show_unmounted,
                                ),
                            );
                        }
                        Temp => {
                            temp_state_map.insert(
                                widget.widget_id,
                                TempWidgetState::new(&app_config_fields, &styling),
                            );
                        }
                        TempGraph => {
                            let upper_limit = config
                                .temperature_graph
                                .as_ref()
                                .and_then(|cfg| cfg.max_temp)
                                .map(|v| v as f32);
                            temp_graph_state_map.insert(
                                widget.widget_id,
                                TempGraphWidgetState::new(ts_config, autohide_timer, upper_limit),
                            );
                        }
                        DiskIoGraph => {
                            let show_read = config
                                .disk_io_graph
                                .as_ref()
                                .and_then(|c| c.show_read)
                                .unwrap_or(true);

                            let show_write = config
                                .disk_io_graph
                                .as_ref()
                                .and_then(|c| c.show_write)
                                .unwrap_or(true);

                            let legend = config
                                .disk_io_graph
                                .as_ref()
                                .and_then(|c| c.legend.clone())
                                .unwrap_or_default();

                            let use_log = config
                                .disk_io_graph
                                .as_ref()
                                .and_then(|c| c.use_log)
                                .unwrap_or(false);

                            disk_io_graph_state_map.insert(
                                widget.widget_id,
                                DiskIoGraphWidgetState::new(
                                    ts_config,
                                    autohide_timer,
                                    show_read,
                                    show_write,
                                    legend,
                                    use_log,
                                ),
                            );
                        }
                        Battery => {
                            battery_state_map
                                .insert(widget.widget_id, BatteryWidgetState::default());
                        }
                        // FIXME: This is kind of a hack that we have these cases at all.
                        Empty | BasicCpu | BasicMem | BasicNet | BasicTables | CpuLegend
                        | ProcSort | ProcSearch => {}
                    }
                }
            }
        }
    }

    let basic_table_widget_state = if use_basic_mode {
        Some(match initial_widget_type {
            Proc | Disk | Temp => BasicTableWidgetState {
                currently_displayed_widget_type: initial_widget_type,
                currently_displayed_widget_id: initial_widget_id,
                left_tlc: None,
                left_brc: None,
                right_tlc: None,
                right_brc: None,
            },
            _ => BasicTableWidgetState {
                currently_displayed_widget_type: Proc,
                currently_displayed_widget_id: DEFAULT_WIDGET_ID,
                left_tlc: None,
                left_brc: None,
                right_tlc: None,
                right_brc: None,
            },
        })
    } else {
        None
    };

    let use_mem = used_widget_set.contains(&Mem) || used_widget_set.contains(&BasicMem);
    let used_widgets = UsedWidgets {
        use_cpu: used_widget_set.contains(&Cpu) || used_widget_set.contains(&BasicCpu),
        use_mem,
        use_cache: use_mem && enable_cache_memory,
        use_gpu: enable_gpu,
        use_net: used_widget_set.contains(&Net) || used_widget_set.contains(&BasicNet),
        use_proc: used_widget_set.contains(&Proc),
        use_disk: used_widget_set.contains(&Disk),
        use_temp: used_widget_set.contains(&Temp),
        use_temp_graph: used_widget_set.contains(&TempGraph),
        use_disk_io_graph: used_widget_set.contains(&DiskIoGraph),
        use_battery: used_widget_set.contains(&Battery),
    };

    let (disk_name_filter, disk_mount_filter) = {
        match &config.disk {
            Some(cfg) => {
                let df = get_ignore_list(&cfg.name_filter)
                    .context("Update 'disk.name_filter' in your config file")?;
                let mf = get_ignore_list(&cfg.mount_filter)
                    .context("Update 'disk.mount_filter' in your config file")?;

                (df, mf)
            }
            None => (None, None),
        }
    };
    let temp_sensor_filter = match &config.temperature {
        Some(cfg) => get_ignore_list(&cfg.sensor_filter)
            .context("Update 'temperature.sensor_filter' in your config file")?,
        None => None,
    };
    let temp_graph_sensor_filter = match &config.temperature_graph {
        Some(cfg) => get_ignore_list(&cfg.sensor_filter)
            .context("Update 'temperature_graph.sensor_filter' in your config file")?,
        None => None,
    };
    let net_interface_filter = match &config.network_graph {
        Some(cfg) => get_ignore_list(&cfg.interface_filter)
            .context("Update 'network_graph.interface_filter' in your config file")?,
        None => None,
    };

    let states = AppWidgetStates {
        cpu_state: CpuState::init(cpu_state_map),
        mem_state: MemState::init(mem_state_map),
        net_state: NetState::init(net_state_map),
        proc_state: ProcState::init(proc_state_map),
        temp_state: TempState::init(temp_state_map),
        temp_graph_state: TempGraphStates::init(temp_graph_state_map),
        disk_state: DiskState::init(disk_state_map),
        disk_io_graph_state: DiskIoGraphStates::init(disk_io_graph_state_map),
        battery_state: AppBatteryState::init(battery_state_map),
        basic_table_widget_state,
    };

    let current_widget = widget_map
        .get(&initial_widget_id)
        .expect("widget map should have the initial widget ID")
        .clone();
    let filters = DataFilters {
        disk_filter: disk_name_filter,
        mount_filter: disk_mount_filter,
        temp_filter: temp_sensor_filter,
        temp_graph_filter: temp_graph_sensor_filter,
        disk_io_graph_filter: disk_io_name_filter,
        net_filter: net_interface_filter,
    };
    let is_expanded = expanded && !use_basic_mode;

    Ok((
        App::new(
            app_config_fields,
            states,
            widget_map,
            current_widget,
            used_widgets,
            filters,
            is_expanded,
        ),
        widget_layout,
        styling,
    ))
}

fn get_widget_layout(
    args: &BottomArgs, config: &Config,
) -> OptionResult<(BottomLayout, u64, Option<BottomWidgetType>)> {
    let cpu_left_legend = enabled_option_with_deprecated!(
        args.cpu.cpu_left_legend,
        config,
        cpu.left_legend,
        flags.cpu_left_legend,
    );

    let (default_widget_type, mut default_widget_count) =
        get_default_widget_and_count(args, config)?;
    let mut default_widget_id = 1;

    let bottom_layout = if is_flag_enabled!(basic, args.general, config) {
        default_widget_id = DEFAULT_WIDGET_ID;

        BottomLayout::init_basic_default(get_use_battery(args, config))
    } else {
        let ref_row: Vec<Row>; // Required to handle reference
        let rows = match &config.row {
            Some(r) => r,
            None => {
                ref_row = toml_edit::de::from_str::<Config>(if get_use_battery(args, config) {
                    DEFAULT_BATTERY_LAYOUT
                } else {
                    DEFAULT_LAYOUT
                })?
                .row
                .expect("This cannot (like it really shouldn't) fail!");
                &ref_row
            }
        };

        let mut iter_id = 0; // A lazy way of forcing unique IDs *shrugs*
        let mut total_height_ratio = 0;

        let mut ret_bottom_layout = BottomLayout {
            rows: rows
                .iter()
                .map(|row| {
                    row.convert_row_to_bottom_row(
                        &mut iter_id,
                        &mut total_height_ratio,
                        &mut default_widget_id,
                        &default_widget_type,
                        &mut default_widget_count,
                        cpu_left_legend,
                    )
                    .map_err(|err| OptionError::config(err.to_string()))
                })
                .collect::<OptionResult<Vec<_>>>()?,
            total_row_height_ratio: total_height_ratio,
        };

        // Confirm that we have at least ONE widget left - if not, error out!
        if iter_id > 0 {
            ret_bottom_layout.get_movement_mappings();
            ret_bottom_layout
        } else {
            return Err(OptionError::config(
                "have at least one widget under the '[[row]]' section.",
            ));
        }
    };

    Ok((bottom_layout, default_widget_id, default_widget_type))
}

#[inline]
fn try_parse_ms(s: &str) -> Result<u64, ()> {
    Ok(if let Ok(val) = humantime::parse_duration(s) {
        val.as_millis().try_into().map_err(|_| ())?
    } else if let Ok(val) = s.parse::<u64>() {
        val
    } else {
        return Err(());
    })
}

macro_rules! parse_arg_value {
    ($to_try:expr, $flag:literal) => {
        $to_try.map_err(|_| OptionError::invalid_arg_value($flag))
    };
}

macro_rules! parse_config_value {
    ($to_try:expr, $setting:literal) => {
        $to_try.map_err(|_| OptionError::invalid_config_value($setting))
    };
}

macro_rules! parse_ms_option {
    ($arg_expr:expr, $config_expr:expr, $default_value:expr, $setting:literal, $low:expr, $high:expr $(,)?) => {{
        use humantime::format_duration;

        if let Some(to_parse) = $arg_expr {
            let value = parse_arg_value!(try_parse_ms(to_parse), $setting)?;

            if let Some(limit) = $low {
                if value < limit {
                    return Err(OptionError::arg(format!(
                        "'--{}' must be greater than {}",
                        $setting,
                        format_duration(Duration::from_millis(limit))
                    )));
                }
            }

            if let Some(limit) = $high {
                if value > limit {
                    return Err(OptionError::arg(format!(
                        "'--{}' must be less than {}",
                        $setting,
                        format_duration(Duration::from_millis(limit))
                    )));
                }
            }

            Ok(value)
        } else if let Some(to_parse) = $config_expr {
            let value = match to_parse {
                StringOrNum::String(s) => parse_config_value!(try_parse_ms(s), $setting)?,
                StringOrNum::Num(n) => *n,
            };

            if let Some(limit) = $low {
                if value < limit {
                    return Err(OptionError::arg(format!(
                        "'{}' must be greater than {}",
                        $setting,
                        format_duration(Duration::from_millis(limit))
                    )));
                }
            }

            if let Some(limit) = $high {
                if value > limit {
                    return Err(OptionError::arg(format!(
                        "'{}' must be less than {}",
                        $setting,
                        format_duration(Duration::from_millis(limit))
                    )));
                }
            }

            Ok(value)
        } else {
            Ok($default_value)
        }
    }};
}

/// Mark a config option field as deprecated, and what to use instead.
#[inline]
fn deprecated_warning(deprecated_field: &str, new_field: &str) {
    eprintln!(
        "Warning: The config option '{deprecated_field}' is deprecated and will eventually be removed. Please use '{new_field}' instead.",
    );
}

/// Mark a config option field as deprecated, and what to use instead with an alias.
#[inline]
fn deprecated_warning_with_alias(deprecated_field: &str, new_field: &str, alias: &str) {
    eprintln!(
        "Warning: The config option '{deprecated_field}' is deprecated and will eventually be removed. Please use '{new_field}' or '{alias}' instead.",
    );
}

/// How quickly we update data.
#[inline]
fn get_update_rate(args: &BottomArgs, config: &Config) -> OptionResult<u64> {
    const DEFAULT_REFRESH_RATE_IN_MILLISECONDS: u64 = 1000;

    parse_ms_option!(
        &args.general.rate,
        config.flags.as_ref().and_then(|flags| flags.rate.as_ref()),
        DEFAULT_REFRESH_RATE_IN_MILLISECONDS,
        "rate",
        Some(250),
        None,
    )
}

fn get_temperature(args: &BottomArgs, config: &Config) -> OptionResult<TemperatureType> {
    if args.temperature.fahrenheit {
        return Ok(TemperatureType::Fahrenheit);
    } else if args.temperature.kelvin {
        return Ok(TemperatureType::Kelvin);
    } else if args.temperature.celsius {
        return Ok(TemperatureType::Celsius);
    } else if let Some(flags) = &config.flags
        && let Some(temp_type) = &flags.temperature_type
    {
        return parse_config_value!(TemperatureType::from_str(temp_type), "temperature_type");
    }
    Ok(TemperatureType::Celsius)
}

/// Yes, this function gets whether to show average CPU (true) or not (false).
fn get_show_average_cpu(args: &BottomArgs, config: &Config) -> bool {
    if args.cpu.hide_avg_cpu {
        return false;
    } else if let Some(cpu) = &config.cpu {
        return !cpu.hide_avg_cpu.unwrap_or(false);
    } else if let Some(flags) = &config.flags
        && let Some(hide) = flags.hide_avg_cpu
    {
        deprecated_warning("hide_avg_cpu", "cpu.hide_avg_cpu");
        return !hide;
    }

    true
}

// I hate this too.
fn get_default_cpu_selection(args: &BottomArgs, config: &Config) -> config::cpu::CpuDefault {
    match &args.cpu.default_cpu_entry {
        Some(default) => match default {
            args::CpuDefault::All => config::cpu::CpuDefault::All,
            args::CpuDefault::Average => config::cpu::CpuDefault::Average,
        },
        None => config.cpu.as_ref().map(|c| c.default).unwrap_or_default(),
    }
}

#[inline]
fn get_default_time_value(
    args: &BottomArgs, config: &Config, retention_ms: u64,
) -> OptionResult<u64> {
    const DEFAULT_TIME_MILLISECONDS: u64 = 60 * 1000; // Defaults to 1 min.

    parse_ms_option!(
        &args.general.default_time_value,
        config
            .flags
            .as_ref()
            .and_then(|flags| flags.default_time_value.as_ref()),
        DEFAULT_TIME_MILLISECONDS,
        "default_time_value",
        Some(30000),
        Some(retention_ms),
    )
}

#[inline]
fn get_time_interval(args: &BottomArgs, config: &Config, retention_ms: u64) -> OptionResult<u64> {
    const TIME_CHANGE_MILLISECONDS: u64 = 15 * 1000; // How much to increment each time

    parse_ms_option!(
        &args.general.time_delta,
        config
            .flags
            .as_ref()
            .and_then(|flags| flags.time_delta.as_ref()),
        TIME_CHANGE_MILLISECONDS,
        "time_delta",
        Some(1000),
        Some(retention_ms),
    )
}

fn get_default_widget_and_count(
    args: &BottomArgs, config: &Config,
) -> OptionResult<(Option<BottomWidgetType>, u64)> {
    let widget_type = if let Some(widget_type) = &args.general.default_widget_type {
        let parsed_widget = parse_arg_value!(widget_type.parse(), "default_widget_type")?;
        if let BottomWidgetType::Empty = parsed_widget {
            None
        } else {
            Some(parsed_widget)
        }
    } else if let Some(flags) = &config.flags {
        if let Some(widget_type) = &flags.default_widget_type {
            let parsed_widget = parse_config_value!(widget_type.parse(), "default_widget_type")?;
            if let BottomWidgetType::Empty = parsed_widget {
                None
            } else {
                Some(parsed_widget)
            }
        } else {
            None
        }
    } else {
        None
    };

    let widget_count: Option<u128> = if let Some(widget_count) = args.general.default_widget_count {
        Some(widget_count.into())
    } else {
        config.flags.as_ref().and_then(|flags| {
            flags
                .default_widget_count
                .map(|widget_count| widget_count.into())
        })
    };

    match (widget_type, widget_count) {
        (Some(widget_type), Some(widget_count)) => {
            let widget_count = widget_count.try_into().map_err(|_| OptionError::other(
                "set your widget count to be at most 18446744073709551615.".to_string()
            ))?;
            Ok((Some(widget_type), widget_count))
        }
        (Some(widget_type), None) => Ok((Some(widget_type), 1)),
        (None, Some(_widget_count)) =>  Err(OptionError::other(
            "cannot set 'default_widget_count' by itself, it must be used with 'default_widget_type'.".to_string(),
        )),
        (None, None) => Ok((None, 1))
    }
}

#[cfg(feature = "battery")]
fn get_use_battery(args: &BottomArgs, config: &Config) -> bool {
    // TODO: Move this so it's dynamic in the app itself and automatically hide if
    // there are no batteries?
    if let Ok(battery_manager) = Manager::new()
        && let Ok(batteries) = battery_manager.batteries()
        && batteries.count() == 0
    {
        return false;
    }

    if args.battery.battery {
        return true;
    } else if let Some(flags) = &config.flags
        && let Some(battery) = flags.battery
    {
        return battery;
    }

    false
}

#[cfg(not(feature = "battery"))]
fn get_use_battery(_args: &BottomArgs, _config: &Config) -> bool {
    false
}

#[cfg(feature = "gpu")]
fn get_enable_gpu(args: &BottomArgs, config: &Config) -> bool {
    if args.gpu.disable_gpu {
        return false;
    }

    !config
        .flags
        .as_ref()
        .and_then(|f| f.disable_gpu)
        .unwrap_or(false)
}

#[cfg(not(feature = "gpu"))]
fn get_enable_gpu(_: &BottomArgs, _: &Config) -> bool {
    false
}

#[cfg(feature = "gpu")]
fn get_short_gpu_names(args: &BottomArgs, config: &Config) -> bool {
    if args.memory.short_gpu_names {
        return true;
    }

    config
        .memory_graph
        .as_ref()
        .and_then(|m| m.short_gpu_names)
        .unwrap_or(false)
}

#[cfg(not(feature = "gpu"))]
fn get_short_gpu_names(_: &BottomArgs, _: &Config) -> bool {
    false
}

#[cfg(not(target_os = "windows"))]
fn get_enable_cache_memory(args: &BottomArgs, config: &Config) -> bool {
    enabled_option_with_deprecated!(
        args.memory.enable_cache_memory,
        config,
        memory_graph.cache_memory,
        flags.enable_cache_memory,
        "memory.cache_memory",
    )
}

#[cfg(target_os = "windows")]
fn get_enable_cache_memory(_args: &BottomArgs, _config: &Config) -> bool {
    false
}

fn get_ignore_list(ignore_list: &Option<IgnoreList>) -> OptionResult<Option<Filter>> {
    if let Some(ignore_list) = ignore_list {
        let list: Result<Vec<_>, _> = ignore_list
            .list
            .iter()
            .map(|name| {
                let escaped_string: String;
                let res = format!(
                    "{}{}{}{}",
                    if ignore_list.whole_word { "^" } else { "" },
                    if ignore_list.case_sensitive {
                        ""
                    } else {
                        "(?i)"
                    },
                    if ignore_list.regex {
                        name
                    } else {
                        escaped_string = regex::escape(name);
                        &escaped_string
                    },
                    if ignore_list.whole_word { "$" } else { "" },
                );

                Regex::new(&res)
            })
            .collect();

        let list = list.map_err(|err| OptionError::config(err.to_string()))?;

        Ok(Some(Filter::new(ignore_list.is_list_ignored, list)))
    } else {
        Ok(None)
    }
}

fn get_network_unit_type(args: &BottomArgs, config: &Config) -> DataUnit {
    if args.network.network_use_bytes {
        return DataUnit::Byte;
    } else if let Some(use_bytes) = config.network_graph.as_ref().and_then(|cfg| cfg.use_bytes) {
        if use_bytes {
            return DataUnit::Byte;
        }
    } else if let Some(flags) = &config.flags
        && let Some(network_use_bytes) = flags.network_use_bytes
    {
        deprecated_warning_with_alias(
            "network_use_bytes",
            "network_graph.use_bytes",
            "network.use_bytes",
        );

        if network_use_bytes {
            return DataUnit::Byte;
        }
    }

    DataUnit::Bit
}

fn get_network_scale_type(args: &BottomArgs, config: &Config) -> AxisScaling {
    if args.network.network_use_log {
        return AxisScaling::Log;
    } else if let Some(use_log) = config.network_graph.as_ref().and_then(|cfg| cfg.use_log) {
        if use_log {
            return AxisScaling::Log;
        }
    } else if let Some(flags) = &config.flags
        && let Some(network_use_log) = flags.network_use_log
    {
        deprecated_warning_with_alias(
            "network_use_log",
            "network_graph.use_log",
            "network.use_log",
        );

        if network_use_log {
            return AxisScaling::Log;
        }
    }

    AxisScaling::Linear
}

fn get_retention(args: &BottomArgs, config: &Config) -> OptionResult<u64> {
    const DEFAULT_RETENTION_MS: u64 = 600 * 1000; // Keep 10 minutes of data.

    parse_ms_option!(
        &args.general.retention,
        config
            .flags
            .as_ref()
            .and_then(|flags| flags.retention.as_ref()),
        DEFAULT_RETENTION_MS,
        "retention",
        None,
        None,
    )
}

#[inline]
fn parse_legend_position(
    arg: Option<&String>, cfg: Option<&String>, deprecated_cfg: Option<(&String, &'static str)>,
    setting: &'static str, alias: Option<&'static str>,
) -> OptionResult<Option<LegendPosition>> {
    #[inline]
    fn parse_position_or_err<F: FnOnce(&'static str) -> OptionError>(
        s: &str, setting: &'static str, err_fn: F,
    ) -> OptionResult<Option<LegendPosition>> {
        Ok(match s.to_ascii_lowercase().trim() {
            "none" => None,
            position => Some(position.parse().map_err(|_| err_fn(setting))?),
        })
    }

    if let Some(s) = arg {
        parse_position_or_err(s, setting, OptionError::invalid_arg_value)
    } else if let Some(s) = cfg {
        parse_position_or_err(s, setting, OptionError::invalid_config_value)
    } else if let Some((s, name)) = deprecated_cfg {
        // Remove the deprecated args/code paths and copy back to the function above later.
        if let Some(alias) = alias {
            deprecated_warning_with_alias(name, setting, alias);
        } else {
            deprecated_warning(name, setting);
        }
        parse_position_or_err(s, setting, OptionError::invalid_config_value)
    } else {
        Ok(Some(LegendPosition::default()))
    }
}

fn get_network_legend_position(
    args: &BottomArgs, config: &Config,
) -> OptionResult<Option<LegendPosition>> {
    parse_legend_position(
        args.network.network_legend.as_ref(),
        config
            .network_graph
            .as_ref()
            .and_then(|cfg| cfg.legend_position.as_ref()),
        config
            .flags
            .as_ref()
            .and_then(|flags| flags.network_legend.as_ref())
            .map(|s| (s, "network_legend")),
        "network_graph.legend_position",
        Some("network.legend_position"),
    )
}

fn get_memory_legend_position(
    args: &BottomArgs, config: &Config,
) -> OptionResult<Option<LegendPosition>> {
    parse_legend_position(
        args.memory.memory_legend.as_ref(),
        config
            .memory_graph
            .as_ref()
            .and_then(|cfg| cfg.legend_position.as_ref()),
        config
            .flags
            .as_ref()
            .and_then(|flags| flags.memory_legend.as_ref())
            .map(|s| (s, "memory_legend")),
        "memory_graph.legend_position",
        Some("memory.legend_position"),
    )
}

fn get_temperature_legend_position(config: &Config) -> OptionResult<Option<LegendPosition>> {
    parse_legend_position(
        None,
        config
            .temperature_graph
            .as_ref()
            .and_then(|settings| settings.legend_position.as_ref()),
        None,
        "legend_position",
        None,
    )
}

fn get_disk_io_legend_position(config: &Config) -> OptionResult<Option<LegendPosition>> {
    parse_legend_position(
        None,
        config
            .disk_io_graph
            .as_ref()
            .and_then(|settings| settings.legend_position.as_ref()),
        None,
        "disk_io_graph.legend_position",
        None,
    )
}

#[cfg(test)]
mod test {
    use clap::Parser;

    use super::*;
    use crate::{
        app::App,
        args::BottomArgs,
        canvas::components::time_series::LegendPosition,
        options::{
            OptionError, config::flags::GeneralConfig, get_default_time_value, get_retention,
            get_update_rate, parse_legend_position, try_parse_ms,
        },
    };

    #[test]
    fn verify_try_parse_ms() {
        let a = "100s";
        let b = "100";
        let c = "1 min";
        let d = "1 hour 1 min";

        assert_eq!(try_parse_ms(a), Ok(100 * 1000));
        assert_eq!(try_parse_ms(b), Ok(100));
        assert_eq!(try_parse_ms(c), Ok(60 * 1000));
        assert_eq!(try_parse_ms(d), Ok(3660 * 1000));

        let a_bad = "1 test";
        let b_bad = "-100";

        assert!(try_parse_ms(a_bad).is_err());
        assert!(try_parse_ms(b_bad).is_err());
    }

    #[test]
    fn verify_parse_legend_position() {
        let setting = "network_legend";

        // No arg, no config.
        assert_eq!(
            parse_legend_position(None, None, None, setting, None),
            Ok(Some(LegendPosition::default()))
        );

        // Arg takes precedence and is parsed (case-insensitive, trimmed).
        let arg = "  ToP-lEfT  ".to_string();
        let cfg = "bottom-right".to_string();
        assert_eq!(
            parse_legend_position(Some(&arg), Some(&cfg), None, setting, None),
            Ok(Some(LegendPosition::TopLeft))
        );

        // "none" disables the legend, from either source.
        let none_arg = "None".to_string();
        assert_eq!(
            parse_legend_position(Some(&none_arg), None, None, setting, None),
            Ok(None)
        );
        let none_cfg = "none".to_string();
        assert_eq!(
            parse_legend_position(None, Some(&none_cfg), None, setting, None),
            Ok(None)
        );

        // Config value is used when no arg is provided.
        let cfg_only = "left".to_string();
        assert_eq!(
            parse_legend_position(None, Some(&cfg_only), None, setting, None),
            Ok(Some(LegendPosition::Left))
        );

        // Invalid arg value.
        let bad_arg = "bad".to_string();
        assert_eq!(
            parse_legend_position(Some(&bad_arg), None, None, setting, None),
            Err(OptionError::invalid_arg_value(setting))
        );

        // Invalid config value.
        let bad_cfg = "bad".to_string();
        assert_eq!(
            parse_legend_position(None, Some(&bad_cfg), None, setting, None),
            Err(OptionError::invalid_config_value(setting))
        );
    }

    #[test]
    fn matches_human_times() {
        let config = Config::default();

        {
            let delta_args = vec!["btm", "--time_delta", "2 min"];
            let args = BottomArgs::parse_from(delta_args);

            assert_eq!(
                get_time_interval(&args, &config, 60 * 60 * 1000),
                Ok(2 * 60 * 1000)
            );
        }

        {
            let default_time_args = vec!["btm", "--default_time_value", "300s"];
            let args = BottomArgs::parse_from(default_time_args);

            assert_eq!(
                get_default_time_value(&args, &config, 60 * 60 * 1000),
                Ok(5 * 60 * 1000)
            );
        }
    }

    #[test]
    fn matches_number_times() {
        let config = Config::default();

        {
            let delta_args = vec!["btm", "--time_delta", "120000"];
            let args = BottomArgs::parse_from(delta_args);

            assert_eq!(
                get_time_interval(&args, &config, 60 * 60 * 1000),
                Ok(2 * 60 * 1000)
            );
        }

        {
            let default_time_args = vec!["btm", "--default_time_value", "300000"];
            let args = BottomArgs::parse_from(default_time_args);

            assert_eq!(
                get_default_time_value(&args, &config, 60 * 60 * 1000),
                Ok(5 * 60 * 1000)
            );
        }
    }

    #[test]
    fn config_human_times() {
        let args = BottomArgs::parse_from(["btm"]);

        let mut config = Config::default();
        let flags = GeneralConfig {
            time_delta: Some("2 min".to_string().into()),
            default_time_value: Some("300s".to_string().into()),
            rate: Some("1s".to_string().into()),
            retention: Some("10m".to_string().into()),
            ..Default::default()
        };

        config.flags = Some(flags);

        assert_eq!(
            get_time_interval(&args, &config, 60 * 60 * 1000),
            Ok(2 * 60 * 1000)
        );

        assert_eq!(
            get_default_time_value(&args, &config, 60 * 60 * 1000),
            Ok(5 * 60 * 1000)
        );

        assert_eq!(get_update_rate(&args, &config), Ok(1000));

        assert_eq!(get_retention(&args, &config), Ok(600000));
    }

    #[test]
    fn config_number_times_as_string() {
        let args = BottomArgs::parse_from(["btm"]);

        let mut config = Config::default();
        let flags = GeneralConfig {
            time_delta: Some("120000".to_string().into()),
            default_time_value: Some("300000".to_string().into()),
            rate: Some("1000".to_string().into()),
            retention: Some("600000".to_string().into()),
            ..Default::default()
        };

        config.flags = Some(flags);

        assert_eq!(
            get_time_interval(&args, &config, 60 * 60 * 1000),
            Ok(2 * 60 * 1000)
        );

        assert_eq!(
            get_default_time_value(&args, &config, 60 * 60 * 1000),
            Ok(5 * 60 * 1000)
        );

        assert_eq!(get_update_rate(&args, &config), Ok(1000));

        assert_eq!(get_retention(&args, &config), Ok(600000));
    }

    #[test]
    fn config_number_times_as_num() {
        let args = BottomArgs::parse_from(["btm"]);

        let mut config = Config::default();
        let flags = GeneralConfig {
            time_delta: Some(120000.into()),
            default_time_value: Some(300000.into()),
            rate: Some(1000.into()),
            retention: Some(600000.into()),
            ..Default::default()
        };

        config.flags = Some(flags);

        assert_eq!(
            get_time_interval(&args, &config, 60 * 60 * 1000),
            Ok(2 * 60 * 1000)
        );

        assert_eq!(
            get_default_time_value(&args, &config, 60 * 60 * 1000),
            Ok(5 * 60 * 1000)
        );

        assert_eq!(get_update_rate(&args, &config), Ok(1000));

        assert_eq!(get_retention(&args, &config), Ok(600000));
    }

    fn create_app(args: BottomArgs) -> App {
        let config = Config::default();
        super::init_app(args, config).unwrap().0
    }

    // TODO: There's probably a better way to create clap options AND unify together
    // to avoid the possibility of typos/mixing up. Use proc macros to unify on
    // one struct?
    #[test]
    fn verify_cli_options_build() {
        let app = crate::args::build_cmd();

        let default_app = create_app(BottomArgs::parse_from(["btm"]));

        // Skip battery since it's tricky to test depending on the platform/features
        // we're testing with.
        let skip = ["help", "version", "celsius", "battery", "generate_schema"];

        for arg in app.get_arguments().collect::<Vec<_>>() {
            let arg_name = arg
                .get_long_and_visible_aliases()
                .unwrap()
                .first()
                .unwrap()
                .to_owned();

            if !arg.get_action().takes_values() && !skip.contains(&arg_name) {
                let arg = format!("--{arg_name}");

                let arguments = vec!["btm", &arg];
                let args = BottomArgs::parse_from(arguments);
                let testing_app = create_app(args);

                if (default_app.app_config_fields == testing_app.app_config_fields)
                    && default_app.is_expanded == testing_app.is_expanded
                    && default_app
                        .states
                        .proc_state
                        .widget_states
                        .iter()
                        .zip(testing_app.states.proc_state.widget_states.iter())
                        .all(|(a, b)| a.1.test_equality(b.1))
                {
                    panic!("failed on {arg_name}");
                }
            }
        }
    }

    /// This one has slightly more complex behaviour due to `dirs` not
    /// respecting XDG on macOS, so we manually handle it. However, to
    /// ensure backwards-compatibility, we also have to do some special cases.
    #[cfg(target_os = "macos")]
    #[test]
    fn test_get_config_path_macos() {
        use std::path::PathBuf;

        use super::{DEFAULT_CONFIG_FILE_LOCATION, get_config_path};

        // Case three: no previous config, no XDG var.
        // SAFETY: This is fine, this is just a test, and no other test affects env
        // vars.
        unsafe {
            std::env::remove_var("XDG_CONFIG_HOME");
        }

        let case_1 = dirs::config_dir()
            .map(|mut path| {
                path.push(DEFAULT_CONFIG_FILE_LOCATION);
                path
            })
            .unwrap();

        // Skip this test if the file already exists.
        if !case_1.exists() {
            assert_eq!(get_config_path(None), Some(case_1));
        }

        // Case two: no previous config, XDG var exists.
        // SAFETY: This is fine, this is just a test, and no other test affects env
        // vars.
        unsafe {
            std::env::set_var("XDG_CONFIG_HOME", "/tmp");
        }
        let mut case_2 = PathBuf::new();
        case_2.push("/tmp");
        case_2.push(DEFAULT_CONFIG_FILE_LOCATION);

        // Skip this test if the file already exists.
        if !case_2.exists() {
            assert_eq!(get_config_path(None), Some(case_2));
        }

        // Case one: old non-XDG exists already, XDG var exists.
        // let case_3 = case_1;
        // assert_eq!(get_config_path(None), Some(case_1));
    }
}
