// Copyright 2022 The Jujutsu Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::borrow::Cow;
use std::collections::BTreeSet;
use std::collections::HashMap;
use std::env;
use std::env::split_paths;
use std::fmt;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::sync::Arc;
use std::sync::LazyLock;
use std::sync::Mutex;

use etcetera::BaseStrategy as _;
use itertools::Itertools as _;
use jj_lib::config::ConfigFile;
use jj_lib::config::ConfigGetError;
use jj_lib::config::ConfigLayer;
use jj_lib::config::ConfigLoadError;
use jj_lib::config::ConfigMigrationRule;
use jj_lib::config::ConfigNamePathBuf;
use jj_lib::config::ConfigResolutionContext;
use jj_lib::config::ConfigSource;
use jj_lib::config::ConfigValue;
use jj_lib::config::StackedConfig;
use jj_lib::dsl_util::AliasDeclarationParser;
use jj_lib::dsl_util::AliasesMap;
use jj_lib::secure_config::LoadedSecureConfig;
use jj_lib::secure_config::SecureConfig;
use rand::SeedableRng as _;
use rand_chacha::ChaCha20Rng;
use regex::Captures;
use regex::Regex;
use serde::Serialize as _;
use tracing::instrument;

use crate::command_error::CommandError;
use crate::command_error::config_error;
use crate::command_error::config_error_with_message;
use crate::ui::Ui;

// TODO(#879): Consider generating entire schema dynamically vs. static file.
pub const CONFIG_SCHEMA: &str = include_str!("config-schema.json");

const REPO_CONFIG_DIR: &str = "repos";
const WORKSPACE_CONFIG_DIR: &str = "workspaces";

/// Parses a TOML value expression. Interprets the given value as string if it
/// can't be parsed and doesn't look like a TOML expression.
pub fn parse_value_or_bare_string(value_str: &str) -> Result<ConfigValue, toml_edit::TomlError> {
    match value_str.parse() {
        Ok(value) => Ok(value),
        Err(_) if is_bare_string(value_str) => Ok(value_str.into()),
        Err(err) => Err(err),
    }
}

fn is_bare_string(value_str: &str) -> bool {
    // leading whitespace isn't ignored when parsing TOML value expression, but
    // "\n[]" doesn't look like a bare string.
    let trimmed = value_str.trim_ascii().as_bytes();
    if let (Some(&first), Some(&last)) = (trimmed.first(), trimmed.last()) {
        // string, array, or table constructs?
        !matches!(first, b'"' | b'\'' | b'[' | b'{') && !matches!(last, b'"' | b'\'' | b']' | b'}')
    } else {
        true // empty or whitespace only
    }
}

/// Converts [`ConfigValue`] (or [`toml_edit::Value`]) to [`toml::Value`] which
/// implements [`serde::Serialize`].
pub fn to_serializable_value(value: ConfigValue) -> toml::Value {
    match value {
        ConfigValue::String(v) => toml::Value::String(v.into_value()),
        ConfigValue::Integer(v) => toml::Value::Integer(v.into_value()),
        ConfigValue::Float(v) => toml::Value::Float(v.into_value()),
        ConfigValue::Boolean(v) => toml::Value::Boolean(v.into_value()),
        ConfigValue::Datetime(v) => toml::Value::Datetime(v.into_value()),
        ConfigValue::Array(array) => {
            let array = array.into_iter().map(to_serializable_value).collect();
            toml::Value::Array(array)
        }
        ConfigValue::InlineTable(table) => {
            let table = table
                .into_iter()
                .map(|(k, v)| (k, to_serializable_value(v)))
                .collect();
            toml::Value::Table(table)
        }
    }
}

/// Configuration variable with its source information.
#[derive(Clone, Debug, serde::Serialize)]
pub struct AnnotatedValue {
    /// Dotted name path to the configuration variable.
    #[serde(serialize_with = "serialize_name")]
    pub name: ConfigNamePathBuf,
    /// Configuration value.
    #[serde(serialize_with = "serialize_value")]
    pub value: ConfigValue,
    /// Source of the configuration value.
    #[serde(serialize_with = "serialize_source")]
    pub source: ConfigSource,
    /// Path to the source file, if available.
    pub path: Option<PathBuf>,
    /// True if this value is overridden in higher precedence layers.
    pub is_overridden: bool,
}

fn serialize_name<S>(name: &ConfigNamePathBuf, serializer: S) -> Result<S::Ok, S::Error>
where
    S: serde::Serializer,
{
    name.to_string().serialize(serializer)
}

fn serialize_value<S>(value: &ConfigValue, serializer: S) -> Result<S::Ok, S::Error>
where
    S: serde::Serializer,
{
    to_serializable_value(value.clone()).serialize(serializer)
}

fn serialize_source<S>(source: &ConfigSource, serializer: S) -> Result<S::Ok, S::Error>
where
    S: serde::Serializer,
{
    source.to_string().serialize(serializer)
}

/// Collects values under the given `filter_prefix` name recursively, from all
/// layers.
pub fn resolved_config_values(
    stacked_config: &StackedConfig,
    filter_prefix: &ConfigNamePathBuf,
) -> Vec<AnnotatedValue> {
    // Collect annotated values in reverse order and mark each value shadowed by
    // value or table in upper layers.
    let mut config_vals = vec![];
    let mut upper_value_names = BTreeSet::new();
    for layer in stacked_config.layers().iter().rev() {
        let top_item = match layer.look_up_item(filter_prefix) {
            Ok(Some(item)) => item,
            Ok(None) => continue, // parent is a table, but no value found
            Err(_) => {
                // parent is not a table, shadows lower layers
                upper_value_names.insert(filter_prefix.clone());
                continue;
            }
        };
        let mut config_stack = vec![(filter_prefix.clone(), top_item, false)];
        while let Some((name, item, is_parent_overridden)) = config_stack.pop() {
            // Cannot retain inline table formatting because inner values may be
            // overridden independently.
            if let Some(table) = item.as_table_like() {
                // current table and children may be shadowed by value in upper layer
                let is_overridden = is_parent_overridden || upper_value_names.contains(&name);
                for (k, v) in table.iter() {
                    let mut sub_name = name.clone();
                    sub_name.push(k);
                    config_stack.push((sub_name, v, is_overridden)); // in reverse order
                }
            } else {
                // current value may be shadowed by value or table in upper layer
                let maybe_child = upper_value_names
                    .range(&name..)
                    .next()
                    .filter(|next| next.starts_with(&name));
                let is_overridden = is_parent_overridden || maybe_child.is_some();
                if maybe_child != Some(&name) {
                    upper_value_names.insert(name.clone());
                }
                let value = item
                    .clone()
                    .into_value()
                    .expect("Item::None should not exist in table");
                config_vals.push(AnnotatedValue {
                    name,
                    value,
                    source: layer.source,
                    path: layer.path.clone(),
                    is_overridden,
                });
            }
        }
    }
    config_vals.reverse();
    config_vals
}

/// Newtype for unprocessed (or unresolved) [`StackedConfig`].
///
/// This doesn't provide any strict guarantee about the underlying config
/// object. It just requires an explicit cast to access to the config object.
#[derive(Clone, Debug)]
pub struct RawConfig(StackedConfig);

impl AsRef<StackedConfig> for RawConfig {
    fn as_ref(&self) -> &StackedConfig {
        &self.0
    }
}

impl AsMut<StackedConfig> for RawConfig {
    fn as_mut(&mut self) -> &mut StackedConfig {
        &mut self.0
    }
}

#[derive(Clone, Debug)]
enum ConfigPathState {
    New,
    Exists,
}

/// A ConfigPath can be in one of two states:
///
/// - exists(): a config file exists at the path
/// - !exists(): a config file doesn't exist here, but a new file _can_ be
///   created at this path
#[derive(Clone, Debug)]
struct ConfigPath {
    path: PathBuf,
    state: ConfigPathState,
}

impl ConfigPath {
    fn new(path: PathBuf) -> Self {
        use ConfigPathState::*;
        Self {
            state: if path.exists() { Exists } else { New },
            path,
        }
    }

    fn as_path(&self) -> &Path {
        &self.path
    }
    fn exists(&self) -> bool {
        match self.state {
            ConfigPathState::Exists => true,
            ConfigPathState::New => false,
        }
    }
}

/// Like std::fs::create_dir_all but creates new directories to be accessible to
/// the user only on Unix (chmod 700).
fn create_dir_all(path: &Path) -> std::io::Result<()> {
    let mut dir = std::fs::DirBuilder::new();
    dir.recursive(true);
    #[cfg(unix)]
    {
        use std::os::unix::fs::DirBuilderExt as _;
        dir.mode(0o700);
    }
    dir.create(path)
}

// The struct exists so that we can mock certain global values in unit tests.
#[derive(Clone, Default, Debug)]
struct UnresolvedConfigEnv {
    config_dir: Option<PathBuf>,
    home_dir: Option<PathBuf>,
    jj_config: Option<String>,
}

impl UnresolvedConfigEnv {
    fn root_config_dir(&self) -> Option<PathBuf> {
        self.config_dir.as_deref().map(|c| c.join("jj"))
    }

    fn resolve(self) -> Vec<ConfigPath> {
        if let Some(paths) = self.jj_config {
            return split_paths(&paths)
                .filter(|path| !path.as_os_str().is_empty())
                .map(ConfigPath::new)
                .collect();
        }

        let mut paths = vec![];
        let home_config_path = self.home_dir.map(|mut home_dir| {
            home_dir.push(".jjconfig.toml");
            ConfigPath::new(home_dir)
        });
        let platform_config_path = self.config_dir.clone().map(|mut config_dir| {
            config_dir.push("jj");
            config_dir.push("config.toml");
            ConfigPath::new(config_dir)
        });
        let platform_config_dir = self.config_dir.map(|mut config_dir| {
            config_dir.push("jj");
            config_dir.push("conf.d");
            ConfigPath::new(config_dir)
        });

        if let Some(path) = home_config_path
            && (path.exists() || platform_config_path.is_none())
        {
            paths.push(path);
        }

        // This should be the default config created if there's
        // no user config and `jj config edit` is executed.
        if let Some(path) = platform_config_path {
            paths.push(path);
        }

        if let Some(path) = platform_config_dir
            && path.exists()
        {
            paths.push(path);
        }

        paths
    }
}

#[derive(Clone, Debug)]
pub struct ConfigEnv {
    home_dir: Option<PathBuf>,
    root_config_dir: Option<PathBuf>,
    repo_path: Option<PathBuf>,
    workspace_path: Option<PathBuf>,
    user_config_paths: Vec<ConfigPath>,
    repo_config: Option<SecureConfig>,
    workspace_config: Option<SecureConfig>,
    command: Option<String>,
    hostname: Option<String>,
    environment: HashMap<String, String>,
    rng: Arc<Mutex<ChaCha20Rng>>,
}

impl ConfigEnv {
    /// Initializes configuration loader based on environment variables.
    pub fn from_environment() -> Self {
        let config_dir = etcetera::choose_base_strategy()
            .ok()
            .map(|s| s.config_dir());

        // Canonicalize home as we do canonicalize cwd in CliRunner. $HOME might
        // point to symlink.
        let home_dir = etcetera::home_dir()
            .ok()
            .map(|d| dunce::canonicalize(&d).unwrap_or(d));

        let env = UnresolvedConfigEnv {
            config_dir,
            home_dir: home_dir.clone(),
            jj_config: env::var("JJ_CONFIG").ok(),
        };
        let environment = env::vars_os()
            .filter_map(|(k, v)| {
                // Silently ignore non-Unicode environment variables. Don't panic like vars()
                let k = k.into_string().ok()?;
                let v = v.into_string().ok()?;
                Some((k, v))
            })
            .collect();
        Self {
            home_dir,
            root_config_dir: env.root_config_dir(),
            repo_path: None,
            workspace_path: None,
            user_config_paths: env.resolve(),
            repo_config: None,
            workspace_config: None,
            command: None,
            hostname: whoami::hostname().ok(),
            environment,
            // We would ideally use JjRng, but that requires the seed from the
            // config, which requires the config to be loaded.
            rng: Arc::new(Mutex::new(
                if let Ok(Ok(value)) = env::var("JJ_RANDOMNESS_SEED").map(|s| s.parse::<u64>()) {
                    ChaCha20Rng::seed_from_u64(value)
                } else {
                    rand::make_rng()
                },
            )),
        }
    }

    pub fn set_command_name(&mut self, command: String) {
        self.command = Some(command);
    }

    fn load_secure_config(
        &self,
        ui: &Ui,
        config: Option<&SecureConfig>,
        kind: &str,
        force: bool,
    ) -> Result<Option<LoadedSecureConfig>, CommandError> {
        Ok(match (config, self.root_config_dir.as_ref()) {
            (Some(config), Some(root_config_dir)) => {
                let mut guard = self.rng.lock().unwrap();
                let loaded_config = if force {
                    config.load_config(&mut guard, &root_config_dir.join(kind))
                } else {
                    config.maybe_load_config(&mut guard, &root_config_dir.join(kind))
                }?;
                for warning in &loaded_config.warnings {
                    writeln!(ui.warning_default(), "{warning}")?;
                }
                Some(loaded_config)
            }
            _ => None,
        })
    }

    /// Returns the paths to the user-specific config files or directories.
    pub fn user_config_paths(&self) -> impl Iterator<Item = &Path> {
        self.user_config_paths.iter().map(ConfigPath::as_path)
    }

    /// Returns the paths to the existing user-specific config files or
    /// directories.
    pub fn existing_user_config_paths(&self) -> impl Iterator<Item = &Path> {
        self.user_config_paths
            .iter()
            .filter(|p| p.exists())
            .map(ConfigPath::as_path)
    }

    /// Returns user configuration files for modification. Instantiates one if
    /// `config` has no user configuration layers.
    ///
    /// The parent directory for the new file may be created by this function.
    /// If the user configuration path is unknown, this function returns an
    /// empty `Vec`.
    pub fn user_config_files(&self, config: &RawConfig) -> Result<Vec<ConfigFile>, CommandError> {
        config_files_for(config, ConfigSource::User, || {
            Ok(self.new_user_config_file()?)
        })
    }

    fn new_user_config_file(&self) -> Result<Option<ConfigFile>, ConfigLoadError> {
        self.user_config_paths()
            .next()
            .map(|path| {
                // No need to propagate io::Error here. If the directory
                // couldn't be created, file.save() would fail later.
                if let Some(dir) = path.parent() {
                    create_dir_all(dir).ok();
                }
                // The path doesn't usually exist, but we shouldn't overwrite it
                // with an empty config if it did exist.
                ConfigFile::load_or_empty(ConfigSource::User, path)
            })
            .transpose()
    }

    /// Loads user-specific config files into the given `config`. The old
    /// user-config layers will be replaced if any.
    #[instrument]
    pub fn reload_user_config(&self, config: &mut RawConfig) -> Result<(), ConfigLoadError> {
        config.as_mut().remove_layers(ConfigSource::User);
        for path in self.existing_user_config_paths() {
            if path.is_dir() {
                config.as_mut().load_dir(ConfigSource::User, path)?;
            } else {
                config.as_mut().load_file(ConfigSource::User, path)?;
            }
        }
        Ok(())
    }

    /// Sets the directory where the repo-specific config file is stored. The
    /// path is usually `$REPO/.jj/repo`.
    pub fn reset_repo_path(&mut self, path: &Path) {
        self.repo_config = Some(SecureConfig::new_repo(path.to_path_buf()));
        self.repo_path = Some(path.to_owned());
    }

    /// Returns a path to the existing repo-specific config file.
    fn maybe_repo_config_path(&self, ui: &Ui) -> Result<Option<PathBuf>, CommandError> {
        Ok(self
            .load_secure_config(ui, self.repo_config.as_ref(), REPO_CONFIG_DIR, false)?
            .and_then(|c| c.config_file))
    }

    /// Returns a path to the existing repo-specific config file.
    /// If the config file does not exist, will create a new config ID and
    /// create a new directory for this.
    pub fn repo_config_path(&self, ui: &Ui) -> Result<Option<PathBuf>, CommandError> {
        Ok(self
            .load_secure_config(ui, self.repo_config.as_ref(), REPO_CONFIG_DIR, true)?
            .and_then(|c| c.config_file))
    }

    /// Returns repo configuration files for modification. Instantiates one if
    /// `config` has no repo configuration layers.
    ///
    /// If the repo path is unknown, this function returns an empty `Vec`. Since
    /// the repo config path cannot be a directory, the returned `Vec` should
    /// have at most one config file.
    pub fn repo_config_files(
        &self,
        ui: &Ui,
        config: &RawConfig,
    ) -> Result<Vec<ConfigFile>, CommandError> {
        config_files_for(config, ConfigSource::Repo, || self.new_repo_config_file(ui))
    }

    fn new_repo_config_file(&self, ui: &Ui) -> Result<Option<ConfigFile>, CommandError> {
        Ok(self
            .repo_config_path(ui)?
            // The path doesn't usually exist, but we shouldn't overwrite it
            // with an empty config if it did exist.
            .map(|path| ConfigFile::load_or_empty(ConfigSource::Repo, path))
            .transpose()?)
    }

    /// Loads repo-specific config file into the given `config`. The old
    /// repo-config layer will be replaced if any.
    #[instrument(skip(ui))]
    pub fn reload_repo_config(&self, ui: &Ui, config: &mut RawConfig) -> Result<(), CommandError> {
        config.as_mut().remove_layers(ConfigSource::Repo);
        if let Some(path) = self.maybe_repo_config_path(ui)?
            && path.exists()
        {
            config.as_mut().load_file(ConfigSource::Repo, path)?;
        }
        Ok(())
    }

    /// Sets the directory where the workspace-specific config file is stored.
    pub fn reset_workspace_path(&mut self, path: &Path) {
        self.workspace_config = Some(SecureConfig::new_workspace(path.join(".jj")));
        self.workspace_path = Some(path.to_owned());
    }

    /// Returns a path to the workspace-specific config file, if it exists.
    fn maybe_workspace_config_path(&self, ui: &Ui) -> Result<Option<PathBuf>, CommandError> {
        Ok(self
            .load_secure_config(
                ui,
                self.workspace_config.as_ref(),
                WORKSPACE_CONFIG_DIR,
                false,
            )?
            .and_then(|c| c.config_file))
    }

    /// Returns a path to the existing workspace-specific config file.
    /// If the config file does not exist, will create a new config ID and
    /// create a new directory for this.
    pub fn workspace_config_path(&self, ui: &Ui) -> Result<Option<PathBuf>, CommandError> {
        Ok(self
            .load_secure_config(
                ui,
                self.workspace_config.as_ref(),
                WORKSPACE_CONFIG_DIR,
                true,
            )?
            .and_then(|c| c.config_file))
    }

    /// Returns workspace configuration files for modification. Instantiates one
    /// if `config` has no workspace configuration layers.
    ///
    /// If the workspace path is unknown, this function returns an empty `Vec`.
    /// Since the workspace config path cannot be a directory, the returned
    /// `Vec` should have at most one config file.
    pub fn workspace_config_files(
        &self,
        ui: &Ui,
        config: &RawConfig,
    ) -> Result<Vec<ConfigFile>, CommandError> {
        config_files_for(config, ConfigSource::Workspace, || {
            self.new_workspace_config_file(ui)
        })
    }

    fn new_workspace_config_file(&self, ui: &Ui) -> Result<Option<ConfigFile>, CommandError> {
        Ok(self
            .workspace_config_path(ui)?
            .map(|path| ConfigFile::load_or_empty(ConfigSource::Workspace, path))
            .transpose()?)
    }

    /// Loads workspace-specific config file into the given `config`. The old
    /// workspace-config layer will be replaced if any.
    #[instrument(skip(ui))]
    pub fn reload_workspace_config(
        &self,
        ui: &Ui,
        config: &mut RawConfig,
    ) -> Result<(), CommandError> {
        config.as_mut().remove_layers(ConfigSource::Workspace);
        if let Some(path) = self.maybe_workspace_config_path(ui)?
            && path.exists()
        {
            config.as_mut().load_file(ConfigSource::Workspace, path)?;
        }
        Ok(())
    }

    /// Resolves conditional scopes within the current environment. Returns new
    /// resolved config.
    pub fn resolve_config(&self, config: &RawConfig) -> Result<StackedConfig, ConfigGetError> {
        let context = ConfigResolutionContext {
            home_dir: self.home_dir.as_deref(),
            repo_path: self.repo_path.as_deref(),
            workspace_path: self.workspace_path.as_deref(),
            command: self.command.as_deref(),
            hostname: self.hostname.as_deref().unwrap_or(""),
            environment: &self.environment,
        };
        jj_lib::config::resolve(config.as_ref(), &context)
    }
}

/// Similar to [`ConfigEnv::repo_config_files()`], but doesn't attempt to
/// initialize new config ID and its storage directory.
pub fn existing_repo_config_file(config: &RawConfig) -> Option<ConfigFile> {
    // There should be at most one repo-level config file.
    config
        .as_ref()
        .layers_for(ConfigSource::Repo)
        .iter()
        .find_map(|layer| ConfigFile::from_layer(layer.clone()).ok())
}

fn config_files_for(
    config: &RawConfig,
    source: ConfigSource,
    new_file: impl FnOnce() -> Result<Option<ConfigFile>, CommandError>,
) -> Result<Vec<ConfigFile>, CommandError> {
    let mut files = config
        .as_ref()
        .layers_for(source)
        .iter()
        .filter_map(|layer| ConfigFile::from_layer(layer.clone()).ok())
        .collect_vec();
    if files.is_empty() {
        files.extend(new_file()?);
    }
    Ok(files)
}

/// Initializes stacked config with the given `default_layers` and infallible
/// sources.
///
/// Sources from the lowest precedence:
/// 1. Default
/// 2. Base environment variables
/// 3. [User configs](https://docs.jj-vcs.dev/latest/config/)
/// 4. Repo config
/// 5. Workspace config
/// 6. Override environment variables
/// 7. Command-line arguments `--config` and `--config-file`
///
/// This function sets up 1, 2, and 6.
pub fn config_from_environment(default_layers: impl IntoIterator<Item = ConfigLayer>) -> RawConfig {
    let mut config = StackedConfig::with_defaults();
    config.extend_layers(default_layers);
    config.add_layer(env_base_layer());
    config.add_layer(env_overrides_layer());
    RawConfig(config)
}

const OP_HOSTNAME: &str = "operation.hostname";
const OP_USERNAME: &str = "operation.username";

/// Environment variables that should be overridden by config values
fn env_base_layer() -> ConfigLayer {
    let mut layer = ConfigLayer::empty(ConfigSource::EnvBase);
    if let Ok(value) =
        whoami::hostname().inspect_err(|err| tracing::warn!(?err, "failed to get hostname"))
    {
        layer.set_value(OP_HOSTNAME, value).unwrap();
    }
    if let Ok(value) =
        whoami::username().inspect_err(|err| tracing::warn!(?err, "failed to get username"))
    {
        layer.set_value(OP_USERNAME, value).unwrap();
    } else if let Ok(value) = env::var("USER") {
        // On Unix, $USER is set by login(1). Use it as a fallback because
        // getpwuid() of musl libc appears not (fully?) supporting nsswitch.
        layer.set_value(OP_USERNAME, value).unwrap();
    }
    if !env::var("NO_COLOR").unwrap_or_default().is_empty() {
        // "User-level configuration files and per-instance command-line arguments
        // should override $NO_COLOR." https://no-color.org/
        layer.set_value("ui.color", "never").unwrap();
    }
    if let Ok(value) = env::var("VISUAL") {
        layer.set_value("ui.editor", value).unwrap();
    } else if let Ok(value) = env::var("EDITOR") {
        layer.set_value("ui.editor", value).unwrap();
    }
    // Intentionally NOT respecting $PAGER here as it often creates a bad
    // out-of-the-box experience for users, see http://github.com/jj-vcs/jj/issues/3502.
    layer
}

pub fn default_config_layers() -> Vec<ConfigLayer> {
    // Syntax error in default config isn't a user error. That's why defaults are
    // loaded by separate builder.
    let parse = |text: &'static str| ConfigLayer::parse(ConfigSource::Default, text).unwrap();
    let mut layers = vec![
        parse(include_str!("config/colors.toml")),
        parse(include_str!("config/hints.toml")),
        parse(include_str!("config/merge_tools.toml")),
        parse(include_str!("config/misc.toml")),
        parse(include_str!("config/revsets.toml")),
        parse(include_str!("config/templates.toml")),
    ];
    if cfg!(unix) {
        layers.push(parse(include_str!("config/unix.toml")));
    }
    if cfg!(windows) {
        layers.push(parse(include_str!("config/windows.toml")));
    }
    layers
}

/// Environment variables that override config values
fn env_overrides_layer() -> ConfigLayer {
    let mut layer = ConfigLayer::empty(ConfigSource::EnvOverrides);
    if let Ok(value) = env::var("JJ_USER") {
        layer.set_value("user.name", value).unwrap();
    }
    if let Ok(value) = env::var("JJ_EMAIL") {
        layer.set_value("user.email", value).unwrap();
    }
    if let Ok(value) = env::var("JJ_TIMESTAMP") {
        layer.set_value("debug.commit-timestamp", value).unwrap();
    }
    if let Ok(Ok(value)) = env::var("JJ_RANDOMNESS_SEED").map(|s| s.parse::<i64>()) {
        layer.set_value("debug.randomness-seed", value).unwrap();
    }
    if let Ok(value) = env::var("JJ_OP_TIMESTAMP") {
        layer.set_value("debug.operation-timestamp", value).unwrap();
    }
    if let Ok(value) = env::var("JJ_OP_HOSTNAME") {
        layer.set_value(OP_HOSTNAME, value).unwrap();
    }
    if let Ok(value) = env::var("JJ_OP_USERNAME") {
        layer.set_value(OP_USERNAME, value).unwrap();
    }
    if let Ok(value) = env::var("JJ_EDITOR") {
        layer.set_value("ui.editor", value).unwrap();
    }
    if let Ok(value) = env::var("JJ_PAGER") {
        layer.set_value("ui.pager", value).unwrap();
    }
    layer
}

/// Configuration source/data type provided as command-line argument.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ConfigArgKind {
    /// `--config=NAME=VALUE`
    Item,
    /// `--config-file=PATH`
    File,
}

/// Parses `--config*` arguments.
pub fn parse_config_args(
    toml_strs: &[(ConfigArgKind, &str)],
) -> Result<Vec<ConfigLayer>, CommandError> {
    let source = ConfigSource::CommandArg;
    let mut layers = Vec::new();
    for (kind, chunk) in &toml_strs.iter().chunk_by(|&(kind, _)| kind) {
        match kind {
            ConfigArgKind::Item => {
                let mut layer = ConfigLayer::empty(source);
                for (_, item) in chunk {
                    let (name, value) = parse_config_arg_item(item)?;
                    // Can fail depending on the argument order, but that
                    // wouldn't matter in practice.
                    layer.set_value(name, value).map_err(|err| {
                        config_error_with_message("--config argument cannot be set", err)
                    })?;
                }
                layers.push(layer);
            }
            ConfigArgKind::File => {
                for (_, path) in chunk {
                    layers.push(ConfigLayer::load_from_file(source, path.into())?);
                }
            }
        }
    }
    Ok(layers)
}

/// Parses `NAME=VALUE` string.
fn parse_config_arg_item(item_str: &str) -> Result<(ConfigNamePathBuf, ConfigValue), CommandError> {
    // split NAME=VALUE at the first parsable position
    let split_candidates = item_str.as_bytes().iter().positions(|&b| b == b'=');
    let Some((name, value_str)) = split_candidates
        .map(|p| (&item_str[..p], &item_str[p + 1..]))
        .map(|(name, value)| name.parse().map(|name| (name, value)))
        .find_or_last(Result::is_ok)
        .transpose()
        .map_err(|err| config_error_with_message("--config name cannot be parsed", err))?
    else {
        return Err(config_error("--config must be specified as NAME=VALUE"));
    };
    let value = parse_value_or_bare_string(value_str)
        .map_err(|err| config_error_with_message("--config value cannot be parsed", err))?;
    Ok((name, value))
}

/// List of rules to migrate deprecated config variables.
pub fn default_config_migrations() -> Vec<ConfigMigrationRule> {
    vec![
        // TODO: Delete in jj 0.42.0+
        ConfigMigrationRule::custom(
            |layer| {
                let Ok(Some(val)) = layer.look_up_item("git.auto-local-bookmark") else {
                    return false;
                };
                val.as_bool().is_some_and(|b| b)
            },
            |_| {
                Ok("`git.auto-local-bookmark` is deprecated; use \
                    `remotes.<name>.auto-track-bookmarks` instead.
Example: jj config set --user remotes.origin.auto-track-bookmarks '*'
For details, see: https://docs.jj-vcs.dev/latest/config/#automatic-tracking-of-bookmarks"
                    .into())
            },
        ),
        // TODO: Delete in jj 0.42.0+
        ConfigMigrationRule::custom(
            |layer| {
                let Ok(Some(val)) = layer.look_up_item("git.push-new-bookmarks") else {
                    return false;
                };
                val.as_bool().is_some_and(|b| b)
            },
            |_| {
                Ok("`git.push-new-bookmarks` is deprecated; use \
                    `remotes.<name>.auto-track-bookmarks` instead.
Example: jj config set --user remotes.origin.auto-track-bookmarks '*'
For details, see: https://docs.jj-vcs.dev/latest/config/#automatic-tracking-of-bookmarks"
                    .into())
            },
        ),
    ]
}

/// Command name and arguments specified by config.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize)]
#[serde(untagged)]
pub enum CommandNameAndArgs {
    String(String),
    Vec(NonEmptyCommandArgsVec),
    Structured {
        env: HashMap<String, String>,
        command: NonEmptyCommandArgsVec,
    },
}

impl CommandNameAndArgs {
    /// Returns command name without arguments.
    pub fn split_name(&self) -> Cow<'_, str> {
        let (name, _) = self.split_name_and_args();
        name
    }

    /// Returns command name and arguments.
    ///
    /// The command name may be an empty string (as well as each argument.)
    pub fn split_name_and_args(&self) -> (Cow<'_, str>, Cow<'_, [String]>) {
        match self {
            Self::String(s) => {
                if s.contains('"') || s.contains('\'') {
                    let mut parts = shlex::Shlex::new(s);
                    let res = (
                        parts.next().unwrap_or_default().into(),
                        parts.by_ref().collect(),
                    );
                    if !parts.had_error {
                        return res;
                    }
                }
                let mut args = s.split(' ').map(|s| s.to_owned());
                (args.next().unwrap().into(), args.collect())
            }
            Self::Vec(NonEmptyCommandArgsVec(a)) => (Cow::Borrowed(&a[0]), Cow::Borrowed(&a[1..])),
            Self::Structured {
                env: _,
                command: cmd,
            } => (Cow::Borrowed(&cmd.0[0]), Cow::Borrowed(&cmd.0[1..])),
        }
    }

    /// Returns command string only if the underlying type is a string.
    ///
    /// Use this to parse enum strings such as `":builtin"`, which can be
    /// escaped as `[":builtin"]`.
    pub fn as_str(&self) -> Option<&str> {
        match self {
            Self::String(s) => Some(s),
            Self::Vec(_) | Self::Structured { .. } => None,
        }
    }

    /// Returns process builder configured with this.
    pub fn to_command(&self) -> Command {
        let empty: HashMap<&str, &str> = HashMap::new();
        self.to_command_with_variables(&empty)
    }

    /// Returns process builder configured with this after interpolating
    /// variables into the arguments.
    pub fn to_command_with_variables<V: AsRef<str>>(
        &self,
        variables: &HashMap<&str, V>,
    ) -> Command {
        let (name, args) = self.split_name_and_args();
        let mut cmd = Command::new(interpolate_variables_single(name.as_ref(), variables));
        if let Self::Structured { env, .. } = self {
            cmd.envs(env);
        }
        cmd.args(interpolate_variables(&args, variables));
        cmd
    }
}

impl<T: AsRef<str> + ?Sized> From<&T> for CommandNameAndArgs {
    fn from(s: &T) -> Self {
        Self::String(s.as_ref().to_owned())
    }
}

impl fmt::Display for CommandNameAndArgs {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::String(s) => write!(f, "{s}"),
            // TODO: format with shell escapes
            Self::Vec(a) => write!(f, "{}", a.0.join(" ")),
            Self::Structured { env, command } => {
                for (k, v) in env {
                    write!(f, "{k}={v} ")?;
                }
                write!(f, "{}", command.0.join(" "))
            }
        }
    }
}

pub fn load_aliases_map<P>(
    ui: &Ui,
    config: &StackedConfig,
    table_name: &ConfigNamePathBuf,
) -> Result<AliasesMap<P, String>, CommandError>
where
    P: AliasDeclarationParser + Default,
    P::Error: fmt::Display,
{
    let mut aliases_map = AliasesMap::new();
    // Load from all config layers in order. 'f(x)' in default layer should be
    // overridden by 'f(a)' in user.
    for layer in config.layers() {
        let table = match layer.look_up_table(table_name) {
            Ok(Some(table)) => table,
            Ok(None) => continue,
            Err(item) => {
                return Err(ConfigGetError::Type {
                    name: table_name.to_string(),
                    error: format!("Expected a table, but is {}", item.type_name()).into(),
                    source_path: layer.path.clone(),
                }
                .into());
            }
        };
        for (decl, item) in table.iter() {
            let r = item
                .as_str()
                .ok_or_else(|| format!("Expected a string, but is {}", item.type_name()))
                .and_then(|v| aliases_map.insert(decl, v).map_err(|e| format!("{e}")));
            if let Err(s) = r {
                writeln!(
                    ui.warning_default(),
                    "Failed to load `{table_name}.{decl}`: {s}"
                )?;
            }
        }
    }
    Ok(aliases_map)
}

// Not interested in $UPPER_CASE_VARIABLES
static VARIABLE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\$([a-z0-9_]+)\b").unwrap());

pub fn interpolate_variables<V: AsRef<str>>(
    args: &[String],
    variables: &HashMap<&str, V>,
) -> Vec<String> {
    args.iter()
        .map(|arg| interpolate_variables_single(arg, variables))
        .collect()
}

fn interpolate_variables_single<V: AsRef<str>>(arg: &str, variables: &HashMap<&str, V>) -> String {
    VARIABLE_REGEX
        .replace_all(arg, |caps: &Captures| {
            let name = &caps[1];
            if let Some(subst) = variables.get(name) {
                subst.as_ref().to_owned()
            } else {
                caps[0].to_owned()
            }
        })
        .into_owned()
}

/// Return all variable names found in the args, without the dollar sign
pub fn find_all_variables(args: &[String]) -> impl Iterator<Item = &str> {
    let regex = &*VARIABLE_REGEX;
    args.iter()
        .flat_map(|arg| regex.find_iter(arg))
        .map(|single_match| {
            let s = single_match.as_str();
            &s[1..]
        })
}

/// Wrapper to reject an array without command name.
// Based on https://github.com/serde-rs/serde/issues/939
#[derive(Clone, Debug, Eq, Hash, PartialEq, serde::Deserialize)]
#[serde(try_from = "Vec<String>")]
pub struct NonEmptyCommandArgsVec(Vec<String>);

impl TryFrom<Vec<String>> for NonEmptyCommandArgsVec {
    type Error = &'static str;

    fn try_from(args: Vec<String>) -> Result<Self, Self::Error> {
        if args.is_empty() {
            Err("command arguments should not be empty")
        } else {
            Ok(Self(args))
        }
    }
}

#[cfg(test)]
mod tests {
    use std::env::join_paths;
    use std::fmt::Write as _;

    use indoc::indoc;
    use maplit::hashmap;
    use test_case::test_case;
    use testutils::TestResult;

    use super::*;

    fn insta_settings() -> insta::Settings {
        let mut settings = insta::Settings::clone_current();
        // Suppress Decor { .. } which is uninteresting
        settings.add_filter(r"\bDecor \{[^}]*\}", "Decor { .. }");
        settings
    }

    #[test]
    fn test_parse_value_or_bare_string() -> TestResult {
        let parse = |s: &str| parse_value_or_bare_string(s);

        // Value in TOML syntax
        assert_eq!(parse("true")?.as_bool(), Some(true));
        assert_eq!(parse("42")?.as_integer(), Some(42));
        assert_eq!(parse("-1")?.as_integer(), Some(-1));
        assert_eq!(parse("'a'")?.as_str(), Some("a"));
        assert!(parse("[]")?.is_array());
        assert!(parse("{ a = 'b' }")?.is_inline_table());

        // Bare string
        assert_eq!(parse("")?.as_str(), Some(""));
        assert_eq!(parse("John Doe")?.as_str(), Some("John Doe"));
        assert_eq!(parse("Doe, John")?.as_str(), Some("Doe, John"));
        assert_eq!(parse("It's okay")?.as_str(), Some("It's okay"));
        assert_eq!(
            parse("<foo+bar@example.org>")?.as_str(),
            Some("<foo+bar@example.org>")
        );
        assert_eq!(parse("#ff00aa")?.as_str(), Some("#ff00aa"));
        assert_eq!(parse("all()")?.as_str(), Some("all()"));
        assert_eq!(parse("glob:*.*")?.as_str(), Some("glob:*.*"));
        assert_eq!(parse("柔術")?.as_str(), Some("柔術"));

        // Error in TOML value
        assert!(parse("'foo").is_err());
        assert!(parse(r#" bar" "#).is_err());
        assert!(parse("[0 1]").is_err());
        assert!(parse("{ x = y }").is_err());
        assert!(parse("\n { x").is_err());
        assert!(parse(" x ] ").is_err());
        assert!(parse("[table]\nkey = 'value'").is_err());
        Ok(())
    }

    #[test]
    fn test_parse_config_arg_item() {
        assert!(parse_config_arg_item("").is_err());
        assert!(parse_config_arg_item("a").is_err());
        assert!(parse_config_arg_item("=").is_err());
        // The value parser is sensitive to leading whitespaces, which seems
        // good because the parsing falls back to a bare string.
        assert!(parse_config_arg_item("a = 'b'").is_err());

        let (name, value) = parse_config_arg_item("a=b").unwrap();
        assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
        assert_eq!(value.as_str(), Some("b"));

        let (name, value) = parse_config_arg_item("a=").unwrap();
        assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
        assert_eq!(value.as_str(), Some(""));

        let (name, value) = parse_config_arg_item("a= ").unwrap();
        assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
        assert_eq!(value.as_str(), Some(" "));

        // This one is a bit cryptic, but b=c can be a bare string.
        let (name, value) = parse_config_arg_item("a=b=c").unwrap();
        assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
        assert_eq!(value.as_str(), Some("b=c"));

        let (name, value) = parse_config_arg_item("a.b=true").unwrap();
        assert_eq!(name, ConfigNamePathBuf::from_iter(["a", "b"]));
        assert_eq!(value.as_bool(), Some(true));

        let (name, value) = parse_config_arg_item("a='b=c'").unwrap();
        assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
        assert_eq!(value.as_str(), Some("b=c"));

        let (name, value) = parse_config_arg_item("'a=b'=c").unwrap();
        assert_eq!(name, ConfigNamePathBuf::from_iter(["a=b"]));
        assert_eq!(value.as_str(), Some("c"));

        let (name, value) = parse_config_arg_item("'a = b=c '={d = 'e=f'}").unwrap();
        assert_eq!(name, ConfigNamePathBuf::from_iter(["a = b=c "]));
        assert!(value.is_inline_table());
        assert_eq!(value.to_string(), "{d = 'e=f'}");
    }

    #[test]
    fn test_command_args() -> TestResult {
        let mut config = StackedConfig::empty();
        config.add_layer(ConfigLayer::parse(
            ConfigSource::User,
            indoc! {"
                    empty_array = []
                    empty_string = ''
                    array = ['emacs', '-nw']
                    string = 'emacs -nw'
                    string_quoted = '\"spaced path/to/emacs\" -nw'
                    structured.env = { KEY1 = 'value1', KEY2 = 'value2' }
                    structured.command = ['emacs', '-nw']
                "},
        )?);

        assert!(config.get::<CommandNameAndArgs>("empty_array").is_err());

        let command_args: CommandNameAndArgs = config.get("empty_string")?;
        assert_eq!(command_args, CommandNameAndArgs::String("".to_owned()));
        let (name, args) = command_args.split_name_and_args();
        assert_eq!(name, "");
        assert!(args.is_empty());

        let command_args: CommandNameAndArgs = config.get("array")?;
        assert_eq!(
            command_args,
            CommandNameAndArgs::Vec(NonEmptyCommandArgsVec(
                ["emacs", "-nw",].map(|s| s.to_owned()).to_vec()
            ))
        );
        let (name, args) = command_args.split_name_and_args();
        assert_eq!(name, "emacs");
        assert_eq!(args, ["-nw"].as_ref());

        let command_args: CommandNameAndArgs = config.get("string")?;
        assert_eq!(
            command_args,
            CommandNameAndArgs::String("emacs -nw".to_owned())
        );
        let (name, args) = command_args.split_name_and_args();
        assert_eq!(name, "emacs");
        assert_eq!(args, ["-nw"].as_ref());

        let command_args: CommandNameAndArgs = config.get("string_quoted")?;
        assert_eq!(
            command_args,
            CommandNameAndArgs::String("\"spaced path/to/emacs\" -nw".to_owned())
        );
        let (name, args) = command_args.split_name_and_args();
        assert_eq!(name, "spaced path/to/emacs");
        assert_eq!(args, ["-nw"].as_ref());

        let command_args: CommandNameAndArgs = config.get("structured")?;
        assert_eq!(
            command_args,
            CommandNameAndArgs::Structured {
                env: hashmap! {
                    "KEY1".to_string() => "value1".to_string(),
                    "KEY2".to_string() => "value2".to_string(),
                },
                command: NonEmptyCommandArgsVec(["emacs", "-nw",].map(|s| s.to_owned()).to_vec())
            }
        );
        let (name, args) = command_args.split_name_and_args();
        assert_eq!(name, "emacs");
        assert_eq!(args, ["-nw"].as_ref());
        Ok(())
    }

    #[test]
    fn test_resolved_config_values_empty() {
        let config = StackedConfig::empty();
        assert!(resolved_config_values(&config, &ConfigNamePathBuf::root()).is_empty());
    }

    #[test]
    fn test_resolved_config_values_single_key() -> TestResult {
        let settings = insta_settings();
        let _guard = settings.bind_to_scope();
        let mut env_base_layer = ConfigLayer::empty(ConfigSource::EnvBase);
        env_base_layer.set_value("user.name", "base-user-name")?;
        env_base_layer.set_value("user.email", "base@user.email")?;
        let mut repo_layer = ConfigLayer::empty(ConfigSource::Repo);
        repo_layer.set_value("user.email", "repo@user.email")?;
        let mut config = StackedConfig::empty();
        config.add_layer(env_base_layer);
        config.add_layer(repo_layer);
        // Note: "email" is alphabetized, before "name" from same layer.
        insta::assert_debug_snapshot!(
            resolved_config_values(&config, &ConfigNamePathBuf::root()),
            @r#"
        [
            AnnotatedValue {
                name: ConfigNamePathBuf(
                    [
                        Key {
                            key: "user",
                            repr: None,
                            leaf_decor: Decor { .. },
                            dotted_decor: Decor { .. },
                        },
                        Key {
                            key: "name",
                            repr: None,
                            leaf_decor: Decor { .. },
                            dotted_decor: Decor { .. },
                        },
                    ],
                ),
                value: String(
                    Formatted {
                        value: "base-user-name",
                        repr: "default",
                        decor: Decor { .. },
                    },
                ),
                source: EnvBase,
                path: None,
                is_overridden: false,
            },
            AnnotatedValue {
                name: ConfigNamePathBuf(
                    [
                        Key {
                            key: "user",
                            repr: None,
                            leaf_decor: Decor { .. },
                            dotted_decor: Decor { .. },
                        },
                        Key {
                            key: "email",
                            repr: None,
                            leaf_decor: Decor { .. },
                            dotted_decor: Decor { .. },
                        },
                    ],
                ),
                value: String(
                    Formatted {
                        value: "base@user.email",
                        repr: "default",
                        decor: Decor { .. },
                    },
                ),
                source: EnvBase,
                path: None,
                is_overridden: true,
            },
            AnnotatedValue {
                name: ConfigNamePathBuf(
                    [
                        Key {
                            key: "user",
                            repr: None,
                            leaf_decor: Decor { .. },
                            dotted_decor: Decor { .. },
                        },
                        Key {
                            key: "email",
                            repr: None,
                            leaf_decor: Decor { .. },
                            dotted_decor: Decor { .. },
                        },
                    ],
                ),
                value: String(
                    Formatted {
                        value: "repo@user.email",
                        repr: "default",
                        decor: Decor { .. },
                    },
                ),
                source: Repo,
                path: None,
                is_overridden: false,
            },
        ]
        "#
        );
        Ok(())
    }

    #[test]
    fn test_resolved_config_values_filter_path() -> TestResult {
        let settings = insta_settings();
        let _guard = settings.bind_to_scope();
        let mut user_layer = ConfigLayer::empty(ConfigSource::User);
        user_layer.set_value("test-table1.foo", "user-FOO")?;
        user_layer.set_value("test-table2.bar", "user-BAR")?;
        let mut repo_layer = ConfigLayer::empty(ConfigSource::Repo);
        repo_layer.set_value("test-table1.bar", "repo-BAR")?;
        let mut config = StackedConfig::empty();
        config.add_layer(user_layer);
        config.add_layer(repo_layer);
        insta::assert_debug_snapshot!(
            resolved_config_values(&config, &ConfigNamePathBuf::from_iter(["test-table1"])),
            @r#"
        [
            AnnotatedValue {
                name: ConfigNamePathBuf(
                    [
                        Key {
                            key: "test-table1",
                            repr: None,
                            leaf_decor: Decor { .. },
                            dotted_decor: Decor { .. },
                        },
                        Key {
                            key: "foo",
                            repr: None,
                            leaf_decor: Decor { .. },
                            dotted_decor: Decor { .. },
                        },
                    ],
                ),
                value: String(
                    Formatted {
                        value: "user-FOO",
                        repr: "default",
                        decor: Decor { .. },
                    },
                ),
                source: User,
                path: None,
                is_overridden: false,
            },
            AnnotatedValue {
                name: ConfigNamePathBuf(
                    [
                        Key {
                            key: "test-table1",
                            repr: None,
                            leaf_decor: Decor { .. },
                            dotted_decor: Decor { .. },
                        },
                        Key {
                            key: "bar",
                            repr: None,
                            leaf_decor: Decor { .. },
                            dotted_decor: Decor { .. },
                        },
                    ],
                ),
                value: String(
                    Formatted {
                        value: "repo-BAR",
                        repr: "default",
                        decor: Decor { .. },
                    },
                ),
                source: Repo,
                path: None,
                is_overridden: false,
            },
        ]
        "#
        );
        Ok(())
    }

    #[test]
    fn test_resolved_config_values_overridden() -> TestResult {
        let list = |layers: &[&ConfigLayer], prefix: &str| -> String {
            let mut config = StackedConfig::empty();
            config.extend_layers(layers.iter().copied().cloned());
            let prefix = if prefix.is_empty() {
                ConfigNamePathBuf::root()
            } else {
                prefix.parse().unwrap()
            };
            let mut output = String::new();
            for annotated in resolved_config_values(&config, &prefix) {
                let AnnotatedValue { name, value, .. } = &annotated;
                let sigil = if annotated.is_overridden { '!' } else { ' ' };
                writeln!(output, "{sigil}{name} = {value}").unwrap();
            }
            output
        };

        let mut layer0 = ConfigLayer::empty(ConfigSource::User);
        layer0.set_value("a.b.e", "0.0")?;
        layer0.set_value("a.b.c.f", "0.1")?;
        layer0.set_value("a.b.d", "0.2")?;
        let mut layer1 = ConfigLayer::empty(ConfigSource::User);
        layer1.set_value("a.b", "1.0")?;
        layer1.set_value("a.c", "1.1")?;
        let mut layer2 = ConfigLayer::empty(ConfigSource::User);
        layer2.set_value("a.b.g", "2.0")?;
        layer2.set_value("a.b.d", "2.1")?;

        // a.b.* is shadowed by a.b
        let layers = [&layer0, &layer1];
        insta::assert_snapshot!(list(&layers, ""), @r#"
        !a.b.e = "0.0"
        !a.b.c.f = "0.1"
        !a.b.d = "0.2"
         a.b = "1.0"
         a.c = "1.1"
        "#);
        insta::assert_snapshot!(list(&layers, "a.b"), @r#"
        !a.b.e = "0.0"
        !a.b.c.f = "0.1"
        !a.b.d = "0.2"
         a.b = "1.0"
        "#);
        insta::assert_snapshot!(list(&layers, "a.b.c"), @r#"!a.b.c.f = "0.1""#);
        insta::assert_snapshot!(list(&layers, "a.b.d"), @r#"!a.b.d = "0.2""#);

        // a.b is shadowed by a.b.*
        let layers = [&layer1, &layer2];
        insta::assert_snapshot!(list(&layers, ""), @r#"
        !a.b = "1.0"
         a.c = "1.1"
         a.b.g = "2.0"
         a.b.d = "2.1"
        "#);
        insta::assert_snapshot!(list(&layers, "a.b"), @r#"
        !a.b = "1.0"
         a.b.g = "2.0"
         a.b.d = "2.1"
        "#);

        // a.b.d is shadowed by a.b.d
        let layers = [&layer0, &layer2];
        insta::assert_snapshot!(list(&layers, ""), @r#"
         a.b.e = "0.0"
         a.b.c.f = "0.1"
        !a.b.d = "0.2"
         a.b.g = "2.0"
         a.b.d = "2.1"
        "#);
        insta::assert_snapshot!(list(&layers, "a.b"), @r#"
         a.b.e = "0.0"
         a.b.c.f = "0.1"
        !a.b.d = "0.2"
         a.b.g = "2.0"
         a.b.d = "2.1"
        "#);
        insta::assert_snapshot!(list(&layers, "a.b.c"), @r#" a.b.c.f = "0.1""#);
        insta::assert_snapshot!(list(&layers, "a.b.d"), @r#"
        !a.b.d = "0.2"
         a.b.d = "2.1"
        "#);

        // a.b.* is shadowed by a.b, which is shadowed by a.b.*
        let layers = [&layer0, &layer1, &layer2];
        insta::assert_snapshot!(list(&layers, ""), @r#"
        !a.b.e = "0.0"
        !a.b.c.f = "0.1"
        !a.b.d = "0.2"
        !a.b = "1.0"
         a.c = "1.1"
         a.b.g = "2.0"
         a.b.d = "2.1"
        "#);
        insta::assert_snapshot!(list(&layers, "a.b"), @r#"
        !a.b.e = "0.0"
        !a.b.c.f = "0.1"
        !a.b.d = "0.2"
        !a.b = "1.0"
         a.b.g = "2.0"
         a.b.d = "2.1"
        "#);
        insta::assert_snapshot!(list(&layers, "a.b.c"), @r#"!a.b.c.f = "0.1""#);
        Ok(())
    }

    struct TestCase {
        files: &'static [&'static str],
        env: UnresolvedConfigEnv,
        wants: Vec<Want>,
    }

    #[derive(Debug)]
    enum WantState {
        New,
        Existing,
    }
    #[derive(Debug)]
    struct Want {
        path: &'static str,
        state: WantState,
    }

    impl Want {
        const fn new(path: &'static str) -> Self {
            Self {
                path,
                state: WantState::New,
            }
        }

        const fn existing(path: &'static str) -> Self {
            Self {
                path,
                state: WantState::Existing,
            }
        }

        fn rooted_path(&self, root: &Path) -> PathBuf {
            root.join(self.path)
        }

        fn exists(&self) -> bool {
            matches!(self.state, WantState::Existing)
        }
    }

    fn config_path_home_existing() -> TestCase {
        TestCase {
            files: &["home/.jjconfig.toml"],
            env: UnresolvedConfigEnv {
                home_dir: Some("home".into()),
                ..Default::default()
            },
            wants: vec![Want::existing("home/.jjconfig.toml")],
        }
    }

    fn config_path_home_new() -> TestCase {
        TestCase {
            files: &[],
            env: UnresolvedConfigEnv {
                home_dir: Some("home".into()),
                ..Default::default()
            },
            wants: vec![Want::new("home/.jjconfig.toml")],
        }
    }

    fn config_path_home_existing_platform_new() -> TestCase {
        TestCase {
            files: &["home/.jjconfig.toml"],
            env: UnresolvedConfigEnv {
                home_dir: Some("home".into()),
                config_dir: Some("config".into()),
                ..Default::default()
            },
            wants: vec![
                Want::existing("home/.jjconfig.toml"),
                Want::new("config/jj/config.toml"),
            ],
        }
    }

    fn config_path_platform_existing() -> TestCase {
        TestCase {
            files: &["config/jj/config.toml"],
            env: UnresolvedConfigEnv {
                home_dir: Some("home".into()),
                config_dir: Some("config".into()),
                ..Default::default()
            },
            wants: vec![Want::existing("config/jj/config.toml")],
        }
    }

    fn config_path_platform_new() -> TestCase {
        TestCase {
            files: &[],
            env: UnresolvedConfigEnv {
                config_dir: Some("config".into()),
                ..Default::default()
            },
            wants: vec![Want::new("config/jj/config.toml")],
        }
    }

    fn config_path_new_prefer_platform() -> TestCase {
        TestCase {
            files: &[],
            env: UnresolvedConfigEnv {
                home_dir: Some("home".into()),
                config_dir: Some("config".into()),
                ..Default::default()
            },
            wants: vec![Want::new("config/jj/config.toml")],
        }
    }

    fn config_path_jj_config_existing() -> TestCase {
        TestCase {
            files: &["custom.toml"],
            env: UnresolvedConfigEnv {
                jj_config: Some("custom.toml".into()),
                ..Default::default()
            },
            wants: vec![Want::existing("custom.toml")],
        }
    }

    fn config_path_jj_config_new() -> TestCase {
        TestCase {
            files: &[],
            env: UnresolvedConfigEnv {
                jj_config: Some("custom.toml".into()),
                ..Default::default()
            },
            wants: vec![Want::new("custom.toml")],
        }
    }

    fn config_path_jj_config_existing_multiple() -> TestCase {
        TestCase {
            files: &["custom1.toml", "custom2.toml"],
            env: UnresolvedConfigEnv {
                jj_config: Some(
                    join_paths(["custom1.toml", "custom2.toml"])
                        .unwrap()
                        .into_string()
                        .unwrap(),
                ),
                ..Default::default()
            },
            wants: vec![
                Want::existing("custom1.toml"),
                Want::existing("custom2.toml"),
            ],
        }
    }

    fn config_path_jj_config_new_multiple() -> TestCase {
        TestCase {
            files: &["custom1.toml"],
            env: UnresolvedConfigEnv {
                jj_config: Some(
                    join_paths(["custom1.toml", "custom2.toml"])
                        .unwrap()
                        .into_string()
                        .unwrap(),
                ),
                ..Default::default()
            },
            wants: vec![Want::existing("custom1.toml"), Want::new("custom2.toml")],
        }
    }

    fn config_path_jj_config_empty_paths_filtered() -> TestCase {
        TestCase {
            files: &["custom1.toml"],
            env: UnresolvedConfigEnv {
                jj_config: Some(
                    join_paths(["custom1.toml", "", "custom2.toml"])
                        .unwrap()
                        .into_string()
                        .unwrap(),
                ),
                ..Default::default()
            },
            wants: vec![Want::existing("custom1.toml"), Want::new("custom2.toml")],
        }
    }

    fn config_path_jj_config_empty() -> TestCase {
        TestCase {
            files: &[],
            env: UnresolvedConfigEnv {
                jj_config: Some("".to_owned()),
                ..Default::default()
            },
            wants: vec![],
        }
    }

    fn config_path_config_pick_platform() -> TestCase {
        TestCase {
            files: &["config/jj/config.toml"],
            env: UnresolvedConfigEnv {
                home_dir: Some("home".into()),
                config_dir: Some("config".into()),
                ..Default::default()
            },
            wants: vec![Want::existing("config/jj/config.toml")],
        }
    }

    fn config_path_config_pick_home() -> TestCase {
        TestCase {
            files: &["home/.jjconfig.toml"],
            env: UnresolvedConfigEnv {
                home_dir: Some("home".into()),
                config_dir: Some("config".into()),
                ..Default::default()
            },
            wants: vec![
                Want::existing("home/.jjconfig.toml"),
                Want::new("config/jj/config.toml"),
            ],
        }
    }

    fn config_path_platform_new_conf_dir_existing() -> TestCase {
        TestCase {
            files: &["config/jj/conf.d/_"],
            env: UnresolvedConfigEnv {
                home_dir: Some("home".into()),
                config_dir: Some("config".into()),
                ..Default::default()
            },
            wants: vec![
                Want::new("config/jj/config.toml"),
                Want::existing("config/jj/conf.d"),
            ],
        }
    }

    fn config_path_platform_existing_conf_dir_existing() -> TestCase {
        TestCase {
            files: &["config/jj/config.toml", "config/jj/conf.d/_"],
            env: UnresolvedConfigEnv {
                home_dir: Some("home".into()),
                config_dir: Some("config".into()),
                ..Default::default()
            },
            wants: vec![
                Want::existing("config/jj/config.toml"),
                Want::existing("config/jj/conf.d"),
            ],
        }
    }

    fn config_path_all_existing() -> TestCase {
        TestCase {
            files: &[
                "config/jj/conf.d/_",
                "config/jj/config.toml",
                "home/.jjconfig.toml",
            ],
            env: UnresolvedConfigEnv {
                home_dir: Some("home".into()),
                config_dir: Some("config".into()),
                ..Default::default()
            },
            // Precedence order is important
            wants: vec![
                Want::existing("home/.jjconfig.toml"),
                Want::existing("config/jj/config.toml"),
                Want::existing("config/jj/conf.d"),
            ],
        }
    }

    fn config_path_none() -> TestCase {
        TestCase {
            files: &[],
            env: Default::default(),
            wants: vec![],
        }
    }

    #[test_case(config_path_home_existing())]
    #[test_case(config_path_home_new())]
    #[test_case(config_path_home_existing_platform_new())]
    #[test_case(config_path_platform_existing())]
    #[test_case(config_path_platform_new())]
    #[test_case(config_path_new_prefer_platform())]
    #[test_case(config_path_jj_config_existing())]
    #[test_case(config_path_jj_config_new())]
    #[test_case(config_path_jj_config_existing_multiple())]
    #[test_case(config_path_jj_config_new_multiple())]
    #[test_case(config_path_jj_config_empty_paths_filtered())]
    #[test_case(config_path_jj_config_empty())]
    #[test_case(config_path_config_pick_platform())]
    #[test_case(config_path_config_pick_home())]
    #[test_case(config_path_platform_new_conf_dir_existing())]
    #[test_case(config_path_platform_existing_conf_dir_existing())]
    #[test_case(config_path_all_existing())]
    #[test_case(config_path_none())]
    fn test_config_path(case: TestCase) {
        let tmp = setup_config_fs(case.files);
        let env = resolve_config_env(&case.env, tmp.path());

        let all_expected_paths = case
            .wants
            .iter()
            .map(|w| w.rooted_path(tmp.path()))
            .collect_vec();
        let exists_expected_paths = case
            .wants
            .iter()
            .filter(|w| w.exists())
            .map(|w| w.rooted_path(tmp.path()))
            .collect_vec();

        let all_paths = env.user_config_paths().collect_vec();
        let exists_paths = env.existing_user_config_paths().collect_vec();

        assert_eq!(all_paths, all_expected_paths);
        assert_eq!(exists_paths, exists_expected_paths);
    }

    fn setup_config_fs(files: &[&str]) -> tempfile::TempDir {
        let tmp = testutils::new_temp_dir();
        for file in files {
            let path = tmp.path().join(file);
            if let Some(parent) = path.parent() {
                std::fs::create_dir_all(parent).unwrap();
            }
            std::fs::File::create(path).unwrap();
        }
        tmp
    }

    fn resolve_config_env(env: &UnresolvedConfigEnv, root: &Path) -> ConfigEnv {
        let home_dir = env.home_dir.as_ref().map(|p| root.join(p));
        let env = UnresolvedConfigEnv {
            config_dir: env.config_dir.as_ref().map(|p| root.join(p)),
            home_dir: home_dir.clone(),
            jj_config: env.jj_config.as_ref().map(|p| {
                join_paths(split_paths(p).map(|p| {
                    if p.as_os_str().is_empty() {
                        return p;
                    }
                    root.join(p)
                }))
                .unwrap()
                .into_string()
                .unwrap()
            }),
        };
        ConfigEnv {
            home_dir,
            root_config_dir: None,
            repo_path: None,
            workspace_path: None,
            user_config_paths: env.resolve(),
            repo_config: None,
            workspace_config: None,
            command: None,
            hostname: None,
            environment: HashMap::new(),
            rng: Arc::new(Mutex::new(ChaCha20Rng::seed_from_u64(0))),
        }
    }
}
