//! Tests for config settings.

use std::borrow::Borrow;
use std::collections::{BTreeMap, HashMap};
use std::fs;
use std::io;
use std::os;
use std::path::{Path, PathBuf};

use cargo::core::features::{GitFeatures, GitoxideFeatures};
use cargo::core::{PackageIdSpec, Shell};
use cargo::util::context::{
    self, Definition, GlobalContext, JobsConfig, SslVersionConfig, StringList,
};
use cargo::CargoResult;
use cargo_test_support::compare::assert_e2e;
use cargo_test_support::prelude::*;
use cargo_test_support::str;
use cargo_test_support::{paths, project, project_in_home, symlink_supported, t};
use cargo_util_schemas::manifest::TomlTrimPaths;
use cargo_util_schemas::manifest::TomlTrimPathsValue;
use cargo_util_schemas::manifest::{self as cargo_toml, TomlDebugInfo, VecStringOrBool as VSOB};
use serde::Deserialize;

/// Helper for constructing a `GlobalContext` object.
pub struct GlobalContextBuilder {
    env: HashMap<String, String>,
    unstable: Vec<String>,
    config_args: Vec<String>,
    cwd: Option<PathBuf>,
    root: Option<PathBuf>,
    enable_nightly_features: bool,
}

impl GlobalContextBuilder {
    pub fn new() -> GlobalContextBuilder {
        GlobalContextBuilder {
            env: HashMap::new(),
            unstable: Vec::new(),
            config_args: Vec::new(),
            root: None,
            cwd: None,
            enable_nightly_features: false,
        }
    }

    /// Passes a `-Z` flag.
    pub fn unstable_flag(&mut self, s: impl Into<String>) -> &mut Self {
        self.unstable.push(s.into());
        self
    }

    /// Sets an environment variable.
    pub fn env(&mut self, key: impl Into<String>, val: impl Into<String>) -> &mut Self {
        self.env.insert(key.into(), val.into());
        self
    }

    /// Unconditionally enable nightly features, even on stable channels.
    pub fn nightly_features_allowed(&mut self, allowed: bool) -> &mut Self {
        self.enable_nightly_features = allowed;
        self
    }

    /// Passes a `--config` flag.
    pub fn config_arg(&mut self, arg: impl Into<String>) -> &mut Self {
        self.config_args.push(arg.into());
        self
    }

    /// Sets the current working directory where config files will be loaded.
    ///
    /// Default is the root from [`GlobalContextBuilder::root`] or [`paths::root`].
    pub fn cwd(&mut self, path: impl AsRef<Path>) -> &mut Self {
        let path = path.as_ref();
        let cwd = self
            .root
            .as_ref()
            .map_or_else(|| paths::root().join(path), |r| r.join(path));
        self.cwd = Some(cwd);
        self
    }

    /// Sets the test root directory.
    ///
    /// This generally should not be necessary. It is only useful if you want
    /// to create a [`GlobalContext`] from within a thread. Since Cargo's
    /// testsuite uses thread-local storage, this can be used to avoid accessing
    /// that thread-local storage.
    ///
    /// Default is [`paths::root`].
    pub fn root(&mut self, path: impl Into<PathBuf>) -> &mut Self {
        self.root = Some(path.into());
        self
    }

    /// Creates the [`GlobalContext`].
    pub fn build(&self) -> GlobalContext {
        self.build_err().unwrap()
    }

    /// Creates the [`GlobalContext`], returning a Result.
    pub fn build_err(&self) -> CargoResult<GlobalContext> {
        let root = self.root.clone().unwrap_or_else(|| paths::root());
        let output = Box::new(fs::File::create(root.join("shell.out")).unwrap());
        let shell = Shell::from_write(output);
        let cwd = self.cwd.clone().unwrap_or_else(|| root.clone());
        let homedir = root.join("home").join(".cargo");
        let mut gctx = GlobalContext::new(shell, cwd, homedir);
        gctx.nightly_features_allowed = self.enable_nightly_features || !self.unstable.is_empty();
        gctx.set_env(self.env.clone());
        gctx.set_search_stop_path(&root);
        gctx.configure(
            0,
            false,
            None,
            false,
            false,
            false,
            &None,
            &self.unstable,
            &self.config_args,
        )?;
        Ok(gctx)
    }
}

fn new_gctx() -> GlobalContext {
    GlobalContextBuilder::new().build()
}

/// Read the output from Config.
pub fn read_output(gctx: GlobalContext) -> String {
    drop(gctx); // Paranoid about flushing the file.
    let path = paths::root().join("shell.out");
    fs::read_to_string(path).unwrap()
}

#[cargo_test]
fn read_env_vars_for_config() {
    let p = project()
        .file(
            "Cargo.toml",
            r#"
                [package]
                name = "foo"
                authors = []
                version = "0.0.0"
                build = "build.rs"
            "#,
        )
        .file("src/lib.rs", "")
        .file(
            "build.rs",
            r#"
                use std::env;
                fn main() {
                    assert_eq!(env::var("NUM_JOBS").unwrap(), "100");
                }
            "#,
        )
        .build();

    p.cargo("check").env("CARGO_BUILD_JOBS", "100").run();
}

pub fn write_config_extless(config: &str) {
    write_config_at(paths::root().join(".cargo/config"), config);
}

pub fn write_config_at(path: impl AsRef<Path>, contents: &str) {
    let path = paths::root().join(path.as_ref());
    fs::create_dir_all(path.parent().unwrap()).unwrap();
    fs::write(path, contents).unwrap();
}

pub fn write_config_toml(config: &str) {
    write_config_at(paths::root().join(".cargo/config.toml"), config);
}

#[cfg(unix)]
fn symlink_file(target: &Path, link: &Path) -> io::Result<()> {
    os::unix::fs::symlink(target, link)
}

#[cfg(windows)]
fn symlink_file(target: &Path, link: &Path) -> io::Result<()> {
    os::windows::fs::symlink_file(target, link)
}

fn make_config_symlink_to_config_toml_absolute() {
    let toml_path = paths::root().join(".cargo/config.toml");
    let symlink_path = paths::root().join(".cargo/config");
    t!(symlink_file(&toml_path, &symlink_path));
}

fn make_config_symlink_to_config_toml_relative() {
    let symlink_path = paths::root().join(".cargo/config");
    t!(symlink_file(Path::new("config.toml"), &symlink_path));
}

fn rename_config_toml_to_config_replacing_with_symlink() {
    let root = paths::root();
    t!(fs::rename(
        root.join(".cargo/config.toml"),
        root.join(".cargo/config")
    ));
    t!(symlink_file(
        Path::new("config"),
        &root.join(".cargo/config.toml")
    ));
}

#[track_caller]
pub fn assert_error<E: Borrow<anyhow::Error>>(error: E, msgs: impl IntoData) {
    let causes = error
        .borrow()
        .chain()
        .enumerate()
        .map(|(i, e)| {
            if i == 0 {
                e.to_string()
            } else {
                format!("Caused by:\n  {}", e)
            }
        })
        .collect::<Vec<_>>()
        .join("\n\n");
    assert_e2e().eq(&causes, msgs);
}

#[cargo_test]
fn get_config() {
    write_config_toml(
        "\
[S]
f1 = 123
",
    );

    let gctx = new_gctx();

    #[derive(Debug, Deserialize, Eq, PartialEq)]
    struct S {
        f1: Option<i64>,
    }
    let s: S = gctx.get("S").unwrap();
    assert_eq!(s, S { f1: Some(123) });
    let gctx = GlobalContextBuilder::new().env("CARGO_S_F1", "456").build();
    let s: S = gctx.get("S").unwrap();
    assert_eq!(s, S { f1: Some(456) });
}

#[cfg(windows)]
#[cargo_test]
fn environment_variable_casing() {
    // Issue #11814: Environment variable names are case-insensitive on Windows.
    let gctx = GlobalContextBuilder::new()
        .env("Path", "abc")
        .env("Two-Words", "abc")
        .env("two_words", "def")
        .build();

    let var = gctx.get_env("PATH").unwrap();
    assert_eq!(var, String::from("abc"));

    let var = gctx.get_env("path").unwrap();
    assert_eq!(var, String::from("abc"));

    let var = gctx.get_env("TWO-WORDS").unwrap();
    assert_eq!(var, String::from("abc"));

    // Make sure that we can still distinguish between dashes and underscores
    // in variable names.
    let var = gctx.get_env("Two_Words").unwrap();
    assert_eq!(var, String::from("def"));
}

#[cargo_test]
fn config_works_without_extension() {
    write_config_extless(
        "\
[foo]
f1 = 1
",
    );

    let gctx = new_gctx();

    assert_eq!(gctx.get::<Option<i32>>("foo.f1").unwrap(), Some(1));

    // It should NOT have warned for the symlink.
    let output = read_output(gctx);
    let expected = str![[r#"
[WARNING] `[ROOT]/.cargo/config` is deprecated in favor of `config.toml`
[NOTE] if you need to support cargo 1.38 or earlier, you can symlink `config` to `config.toml`

"#]];
    assert_e2e().eq(&output, expected);
}

#[cargo_test]
fn home_config_works_without_extension() {
    write_config_at(
        paths::cargo_home().join("config"),
        "\
[foo]
f1 = 1
",
    );
    let p = project_in_home("foo").file("src/lib.rs", "").build();

    p.cargo("-vV")
        .with_stderr_data(str![[r#"
[WARNING] `[ROOT]/home/.cargo/config` is deprecated in favor of `config.toml`
[NOTE] if you need to support cargo 1.38 or earlier, you can symlink `config` to `config.toml`

"#]])
        .run();
}

#[cargo_test]
fn config_ambiguous_filename_symlink_doesnt_warn() {
    // Windows requires special permissions to create symlinks.
    // If we don't have permission, just skip this test.
    if !symlink_supported() {
        return;
    };

    write_config_toml(
        "\
[foo]
f1 = 1
",
    );

    make_config_symlink_to_config_toml_absolute();

    let gctx = new_gctx();

    assert_eq!(gctx.get::<Option<i32>>("foo.f1").unwrap(), Some(1));

    // It should NOT have warned for the symlink.
    let output = read_output(gctx);
    assert_e2e().eq(&output, str![[""]]);
}

#[cargo_test]
fn config_ambiguous_filename_symlink_doesnt_warn_relative() {
    // Windows requires special permissions to create symlinks.
    // If we don't have permission, just skip this test.
    if !symlink_supported() {
        return;
    };

    write_config_toml(
        "\
[foo]
f1 = 1
",
    );

    make_config_symlink_to_config_toml_relative();

    let gctx = new_gctx();

    assert_eq!(gctx.get::<Option<i32>>("foo.f1").unwrap(), Some(1));

    // It should NOT have warned for the symlink.
    let output = read_output(gctx);
    assert_e2e().eq(&output, str![[""]]);
}

#[cargo_test]
fn config_ambiguous_filename_symlink_doesnt_warn_backward() {
    // Windows requires special permissions to create symlinks.
    // If we don't have permission, just skip this test.
    if !symlink_supported() {
        return;
    };

    write_config_toml(
        "\
[foo]
f1 = 1
",
    );

    rename_config_toml_to_config_replacing_with_symlink();

    let gctx = new_gctx();

    assert_eq!(gctx.get::<Option<i32>>("foo.f1").unwrap(), Some(1));

    // It should NOT have warned for this situation.
    let output = read_output(gctx);
    assert_e2e().eq(&output, str![[""]]);
}

#[cargo_test]
fn config_ambiguous_filename() {
    write_config_extless(
        "\
[foo]
f1 = 1
",
    );

    write_config_toml(
        "\
[foo]
f1 = 2
",
    );

    let gctx = new_gctx();

    // It should use the value from the one without the extension for
    // backwards compatibility.
    assert_eq!(gctx.get::<Option<i32>>("foo.f1").unwrap(), Some(1));

    // But it also should have warned.
    let output = read_output(gctx);
    let expected = str![[r#"
[WARNING] both `[ROOT]/.cargo/config` and `[ROOT]/.cargo/config.toml` exist. Using `[ROOT]/.cargo/config`

"#]];
    assert_e2e().eq(&output, expected);
}

#[cargo_test]
fn config_unused_fields() {
    write_config_toml(
        "\
[S]
unused = 456
",
    );

    let gctx = GlobalContextBuilder::new()
        .env("CARGO_S_UNUSED2", "1")
        .env("CARGO_S2_UNUSED", "2")
        .build();

    #[derive(Debug, Deserialize, Eq, PartialEq)]
    struct S {
        f1: Option<i64>,
    }
    // This prints a warning (verified below).
    let s: S = gctx.get("S").unwrap();
    assert_eq!(s, S { f1: None });
    // This does not print anything, we cannot easily/reliably warn for
    // environment variables.
    let s: S = gctx.get("S2").unwrap();
    assert_eq!(s, S { f1: None });

    // Verify the warnings.
    let output = read_output(gctx);
    let expected = str![[r#"
[WARNING] unused config key `S.unused` in `[ROOT]/.cargo/config.toml`

"#]];
    assert_e2e().eq(&output, expected);
}

#[cargo_test]
fn config_load_toml_profile() {
    write_config_toml(
        "\
[profile.dev]
opt-level = 's'
lto = true
codegen-units=4
debug = true
debug-assertions = true
rpath = true
panic = 'abort'
overflow-checks = true
incremental = true

[profile.dev.build-override]
opt-level = 1

[profile.dev.package.bar]
codegen-units = 9

[profile.no-lto]
inherits = 'dev'
dir-name = 'without-lto'
lto = false
",
    );

    let gctx = GlobalContextBuilder::new()
        .unstable_flag("advanced-env")
        .env("CARGO_PROFILE_DEV_CODEGEN_UNITS", "5")
        .env("CARGO_PROFILE_DEV_BUILD_OVERRIDE_CODEGEN_UNITS", "11")
        .env("CARGO_PROFILE_DEV_PACKAGE_env_CODEGEN_UNITS", "13")
        .env("CARGO_PROFILE_DEV_PACKAGE_bar_OPT_LEVEL", "2")
        .build();

    // TODO: don't use actual `tomlprofile`.
    let p: cargo_toml::TomlProfile = gctx.get("profile.dev").unwrap();
    let mut packages = BTreeMap::new();
    let key =
        cargo_toml::ProfilePackageSpec::Spec(::cargo::core::PackageIdSpec::parse("bar").unwrap());
    let o_profile = cargo_toml::TomlProfile {
        opt_level: Some(cargo_toml::TomlOptLevel("2".to_string())),
        codegen_units: Some(9),
        ..Default::default()
    };
    packages.insert(key, o_profile);
    let key =
        cargo_toml::ProfilePackageSpec::Spec(::cargo::core::PackageIdSpec::parse("env").unwrap());
    let o_profile = cargo_toml::TomlProfile {
        codegen_units: Some(13),
        ..Default::default()
    };
    packages.insert(key, o_profile);

    assert_eq!(
        p,
        cargo_toml::TomlProfile {
            opt_level: Some(cargo_toml::TomlOptLevel("s".to_string())),
            lto: Some(cargo_toml::StringOrBool::Bool(true)),
            codegen_units: Some(5),
            debug: Some(cargo_toml::TomlDebugInfo::Full),
            debug_assertions: Some(true),
            rpath: Some(true),
            panic: Some("abort".to_string()),
            overflow_checks: Some(true),
            incremental: Some(true),
            package: Some(packages),
            build_override: Some(Box::new(cargo_toml::TomlProfile {
                opt_level: Some(cargo_toml::TomlOptLevel("1".to_string())),
                codegen_units: Some(11),
                ..Default::default()
            })),
            ..Default::default()
        }
    );

    let p: cargo_toml::TomlProfile = gctx.get("profile.no-lto").unwrap();
    assert_eq!(
        p,
        cargo_toml::TomlProfile {
            lto: Some(cargo_toml::StringOrBool::Bool(false)),
            dir_name: Some(String::from("without-lto")),
            inherits: Some(String::from("dev")),
            ..Default::default()
        }
    );
}

#[cargo_test]
fn profile_env_var_prefix() {
    // Check for a bug with collision on DEBUG vs DEBUG_ASSERTIONS.
    let gctx = GlobalContextBuilder::new()
        .env("CARGO_PROFILE_DEV_DEBUG_ASSERTIONS", "false")
        .build();
    let p: cargo_toml::TomlProfile = gctx.get("profile.dev").unwrap();
    assert_eq!(p.debug_assertions, Some(false));
    assert_eq!(p.debug, None);

    let gctx = GlobalContextBuilder::new()
        .env("CARGO_PROFILE_DEV_DEBUG", "1")
        .build();
    let p: cargo_toml::TomlProfile = gctx.get("profile.dev").unwrap();
    assert_eq!(p.debug_assertions, None);
    assert_eq!(p.debug, Some(cargo_toml::TomlDebugInfo::Limited));

    let gctx = GlobalContextBuilder::new()
        .env("CARGO_PROFILE_DEV_DEBUG_ASSERTIONS", "false")
        .env("CARGO_PROFILE_DEV_DEBUG", "1")
        .build();
    let p: cargo_toml::TomlProfile = gctx.get("profile.dev").unwrap();
    assert_eq!(p.debug_assertions, Some(false));
    assert_eq!(p.debug, Some(cargo_toml::TomlDebugInfo::Limited));
}

#[cargo_test]
fn config_deserialize_any() {
    // Some tests to exercise deserialize_any for deserializers that need to
    // be told the format.
    write_config_toml(
        "\
a = true
b = ['b']
c = ['c']
",
    );

    // advanced-env
    let gctx = GlobalContextBuilder::new()
        .unstable_flag("advanced-env")
        .env("CARGO_ENVB", "false")
        .env("CARGO_C", "['d']")
        .env("CARGO_ENVL", "['a', 'b']")
        .build();
    assert_eq!(gctx.get::<VSOB>("a").unwrap(), VSOB::Bool(true));
    assert_eq!(
        gctx.get::<VSOB>("b").unwrap(),
        VSOB::VecString(vec!["b".to_string()])
    );
    assert_eq!(
        gctx.get::<VSOB>("c").unwrap(),
        VSOB::VecString(vec!["c".to_string(), "d".to_string()])
    );
    assert_eq!(gctx.get::<VSOB>("envb").unwrap(), VSOB::Bool(false));
    assert_eq!(
        gctx.get::<VSOB>("envl").unwrap(),
        VSOB::VecString(vec!["a".to_string(), "b".to_string()])
    );

    // Demonstrate where merging logic isn't very smart. This could be improved.
    let gctx = GlobalContextBuilder::new().env("CARGO_A", "x y").build();
    assert_error(
        gctx.get::<VSOB>("a").unwrap_err(),
        "\
error in environment variable `CARGO_A`: could not load config key `a`

Caused by:
  invalid type: string \"x y\", expected a boolean or vector of strings",
    );

    // Normal env.
    let gctx = GlobalContextBuilder::new()
        .unstable_flag("advanced-env")
        .env("CARGO_B", "d e")
        .env("CARGO_C", "f g")
        .build();
    assert_eq!(
        gctx.get::<VSOB>("b").unwrap(),
        VSOB::VecString(vec!["b".to_string(), "d".to_string(), "e".to_string()])
    );
    assert_eq!(
        gctx.get::<VSOB>("c").unwrap(),
        VSOB::VecString(vec!["c".to_string(), "f".to_string(), "g".to_string()])
    );

    // config-cli
    // This test demonstrates that ConfigValue::merge isn't very smart.
    // It would be nice if it was smarter.
    let gctx = GlobalContextBuilder::new()
        .config_arg("a = ['a']")
        .build_err();
    assert_error(
        gctx.unwrap_err(),
        "\
failed to merge --config key `a` into `[..]/.cargo/config.toml`

Caused by:
  failed to merge config value from `--config cli option` into `[..]/.cargo/config.toml`: \
expected boolean, but found array",
    );

    // config-cli and advanced-env
    let gctx = GlobalContextBuilder::new()
        .unstable_flag("advanced-env")
        .config_arg("b=['clib']")
        .config_arg("c=['clic']")
        .env("CARGO_B", "env1 env2")
        .env("CARGO_C", "['e1', 'e2']")
        .build();
    assert_eq!(
        gctx.get::<VSOB>("b").unwrap(),
        VSOB::VecString(vec![
            "b".to_string(),
            "env1".to_string(),
            "env2".to_string(),
            "clib".to_string(),
        ])
    );
    assert_eq!(
        gctx.get::<VSOB>("c").unwrap(),
        VSOB::VecString(vec![
            "c".to_string(),
            "e1".to_string(),
            "e2".to_string(),
            "clic".to_string(),
        ])
    );
}

#[cargo_test]
fn config_toml_errors() {
    write_config_toml(
        "\
[profile.dev]
opt-level = 'foo'
",
    );

    let gctx = new_gctx();

    assert_error(
        gctx.get::<cargo_toml::TomlProfile>("profile.dev")
            .unwrap_err(),
        "\
error in [..]/.cargo/config.toml: could not load config key `profile.dev.opt-level`

Caused by:
  must be `0`, `1`, `2`, `3`, `s` or `z`, but found the string: \"foo\"",
    );

    let gctx = GlobalContextBuilder::new()
        .env("CARGO_PROFILE_DEV_OPT_LEVEL", "asdf")
        .build();

    assert_error(
        gctx.get::<cargo_toml::TomlProfile>("profile.dev").unwrap_err(),
        "\
error in environment variable `CARGO_PROFILE_DEV_OPT_LEVEL`: could not load config key `profile.dev.opt-level`

Caused by:
  must be `0`, `1`, `2`, `3`, `s` or `z`, but found the string: \"asdf\"",
    );
}

#[cargo_test]
fn load_nested() {
    write_config_toml(
        "\
[nest.foo]
f1 = 1
f2 = 2
[nest.bar]
asdf = 3
",
    );

    let gctx = GlobalContextBuilder::new()
        .unstable_flag("advanced-env")
        .env("CARGO_NEST_foo_f2", "3")
        .env("CARGO_NESTE_foo_f1", "1")
        .env("CARGO_NESTE_foo_f2", "3")
        .env("CARGO_NESTE_bar_asdf", "3")
        .build();

    type Nested = HashMap<String, HashMap<String, u8>>;

    let n: Nested = gctx.get("nest").unwrap();
    let mut expected = HashMap::new();
    let mut foo = HashMap::new();
    foo.insert("f1".to_string(), 1);
    foo.insert("f2".to_string(), 3);
    expected.insert("foo".to_string(), foo);
    let mut bar = HashMap::new();
    bar.insert("asdf".to_string(), 3);
    expected.insert("bar".to_string(), bar);
    assert_eq!(n, expected);

    let n: Nested = gctx.get("neste").unwrap();
    assert_eq!(n, expected);
}

#[cargo_test]
fn get_errors() {
    write_config_toml(
        "\
[S]
f1 = 123
f2 = 'asdf'
big = 123456789
",
    );

    let gctx = GlobalContextBuilder::new()
        .env("CARGO_E_S", "asdf")
        .env("CARGO_E_BIG", "123456789")
        .build();
    assert_error(
        gctx.get::<i64>("foo").unwrap_err(),
        "missing config key `foo`",
    );
    assert_error(
        gctx.get::<i64>("foo.bar").unwrap_err(),
        "missing config key `foo.bar`",
    );
    assert_error(
        gctx.get::<i64>("S.f2").unwrap_err(),
        "error in [..]/.cargo/config.toml: `S.f2` expected an integer, but found a string",
    );
    assert_error(
        gctx.get::<u8>("S.big").unwrap_err(),
        "\
error in [..].cargo/config.toml: could not load config key `S.big`

Caused by:
  invalid value: integer `123456789`, expected u8",
    );

    // Environment variable type errors.
    assert_error(
        gctx.get::<i64>("e.s").unwrap_err(),
        "error in environment variable `CARGO_E_S`: invalid digit found in string",
    );
    assert_error(
        gctx.get::<i8>("e.big").unwrap_err(),
        "\
error in environment variable `CARGO_E_BIG`: could not load config key `e.big`

Caused by:
  invalid value: integer `123456789`, expected i8",
    );

    #[derive(Debug, Deserialize)]
    #[allow(dead_code)]
    struct S {
        f1: i64,
        f2: String,
        f3: i64,
        big: i64,
    }
    assert_error(gctx.get::<S>("S").unwrap_err(), "missing field `f3`");
}

#[cargo_test]
fn config_get_option() {
    write_config_toml(
        "\
[foo]
f1 = 1
",
    );

    let gctx = GlobalContextBuilder::new()
        .env("CARGO_BAR_ASDF", "3")
        .build();

    assert_eq!(gctx.get::<Option<i32>>("a").unwrap(), None);
    assert_eq!(gctx.get::<Option<i32>>("a.b").unwrap(), None);
    assert_eq!(gctx.get::<Option<i32>>("foo.f1").unwrap(), Some(1));
    assert_eq!(gctx.get::<Option<i32>>("bar.asdf").unwrap(), Some(3));
    assert_eq!(gctx.get::<Option<i32>>("bar.zzzz").unwrap(), None);
}

#[cargo_test]
fn config_bad_toml() {
    write_config_toml("asdf");
    let gctx = new_gctx();
    assert_error(
        gctx.get::<i32>("foo").unwrap_err(),
        "\
could not load Cargo configuration

Caused by:
  could not parse TOML configuration in `[..]/.cargo/config.toml`

Caused by:
  TOML parse error at line 1, column 5
  |
1 | asdf
  |     ^
expected `.`, `=`
",
    );
}

#[cargo_test]
fn config_get_list() {
    write_config_toml(
        "\
l1 = []
l2 = ['one', 'two']
l3 = 123
l4 = ['one', 'two']

[nested]
l = ['x']

[nested2]
l = ['y']

[nested-empty]
",
    );

    type L = Vec<String>;

    let gctx = GlobalContextBuilder::new()
        .unstable_flag("advanced-env")
        .env("CARGO_L4", "['three', 'four']")
        .env("CARGO_L5", "['a']")
        .env("CARGO_ENV_EMPTY", "[]")
        .env("CARGO_ENV_BLANK", "")
        .env("CARGO_ENV_NUM", "1")
        .env("CARGO_ENV_NUM_LIST", "[1]")
        .env("CARGO_ENV_TEXT", "asdf")
        .env("CARGO_LEPAIR", "['a', 'b']")
        .env("CARGO_NESTED2_L", "['z']")
        .env("CARGO_NESTEDE_L", "['env']")
        .env("CARGO_BAD_ENV", "[zzz]")
        .build();

    assert_eq!(gctx.get::<L>("unset").unwrap(), vec![] as Vec<String>);
    assert_eq!(gctx.get::<L>("l1").unwrap(), vec![] as Vec<String>);
    assert_eq!(gctx.get::<L>("l2").unwrap(), vec!["one", "two"]);
    assert_error(
        gctx.get::<L>("l3").unwrap_err(),
        "\
invalid configuration for key `l3`
expected a list, but found a integer for `l3` in [..]/.cargo/config.toml",
    );
    assert_eq!(
        gctx.get::<L>("l4").unwrap(),
        vec!["one", "two", "three", "four"]
    );
    assert_eq!(gctx.get::<L>("l5").unwrap(), vec!["a"]);
    assert_eq!(gctx.get::<L>("env-empty").unwrap(), vec![] as Vec<String>);
    assert_eq!(gctx.get::<L>("env-blank").unwrap(), vec![] as Vec<String>);
    assert_eq!(gctx.get::<L>("env-num").unwrap(), vec!["1".to_string()]);
    assert_error(
        gctx.get::<L>("env-num-list").unwrap_err(),
        "error in environment variable `CARGO_ENV_NUM_LIST`: \
         expected string, found integer",
    );
    assert_eq!(gctx.get::<L>("env-text").unwrap(), vec!["asdf".to_string()]);
    // "invalid number" here isn't the best error, but I think it's just toml.rs.
    assert_error(
        gctx.get::<L>("bad-env").unwrap_err(),
        "\
error in environment variable `CARGO_BAD_ENV`: could not parse TOML list: TOML parse error at line 1, column 2
  |
1 | [zzz]
  |  ^
invalid array
expected `]`
",
    );

    // Try some other sequence-like types.
    assert_eq!(
        gctx.get::<(String, String, String, String)>("l4").unwrap(),
        (
            "one".to_string(),
            "two".to_string(),
            "three".to_string(),
            "four".to_string()
        )
    );
    assert_eq!(gctx.get::<(String,)>("l5").unwrap(), ("a".to_string(),));

    // Tuple struct
    #[derive(Debug, Deserialize, Eq, PartialEq)]
    struct TupS(String, String);
    assert_eq!(
        gctx.get::<TupS>("lepair").unwrap(),
        TupS("a".to_string(), "b".to_string())
    );

    // Nested with an option.
    #[derive(Debug, Deserialize, Eq, PartialEq)]
    struct S {
        l: Option<Vec<String>>,
    }
    assert_eq!(gctx.get::<S>("nested-empty").unwrap(), S { l: None });
    assert_eq!(
        gctx.get::<S>("nested").unwrap(),
        S {
            l: Some(vec!["x".to_string()]),
        }
    );
    assert_eq!(
        gctx.get::<S>("nested2").unwrap(),
        S {
            l: Some(vec!["y".to_string(), "z".to_string()]),
        }
    );
    assert_eq!(
        gctx.get::<S>("nestede").unwrap(),
        S {
            l: Some(vec!["env".to_string()]),
        }
    );
}

#[cargo_test]
fn config_get_other_types() {
    write_config_toml(
        "\
ns = 123
ns2 = 456
",
    );

    let gctx = GlobalContextBuilder::new()
        .env("CARGO_NSE", "987")
        .env("CARGO_NS2", "654")
        .build();

    #[derive(Debug, Deserialize, Eq, PartialEq)]
    #[serde(transparent)]
    struct NewS(i32);
    assert_eq!(gctx.get::<NewS>("ns").unwrap(), NewS(123));
    assert_eq!(gctx.get::<NewS>("ns2").unwrap(), NewS(654));
    assert_eq!(gctx.get::<NewS>("nse").unwrap(), NewS(987));
    assert_error(
        gctx.get::<NewS>("unset").unwrap_err(),
        "missing config key `unset`",
    );
}

#[cargo_test]
fn config_relative_path() {
    write_config_toml(&format!(
        "\
p1 = 'foo/bar'
p2 = '../abc'
p3 = 'b/c'
abs = '{}'
",
        paths::home().display(),
    ));

    let gctx = GlobalContextBuilder::new()
        .env("CARGO_EPATH", "a/b")
        .env("CARGO_P3", "d/e")
        .build();

    assert_eq!(
        gctx.get::<context::ConfigRelativePath>("p1")
            .unwrap()
            .resolve_path(&gctx),
        paths::root().join("foo/bar")
    );
    assert_eq!(
        gctx.get::<context::ConfigRelativePath>("p2")
            .unwrap()
            .resolve_path(&gctx),
        paths::root().join("../abc")
    );
    assert_eq!(
        gctx.get::<context::ConfigRelativePath>("p3")
            .unwrap()
            .resolve_path(&gctx),
        paths::root().join("d/e")
    );
    assert_eq!(
        gctx.get::<context::ConfigRelativePath>("abs")
            .unwrap()
            .resolve_path(&gctx),
        paths::home()
    );
    assert_eq!(
        gctx.get::<context::ConfigRelativePath>("epath")
            .unwrap()
            .resolve_path(&gctx),
        paths::root().join("a/b")
    );
}

#[cargo_test]
fn config_get_integers() {
    write_config_toml(
        "\
npos = 123456789
nneg = -123456789
i64max = 9223372036854775807
",
    );

    let gctx = GlobalContextBuilder::new()
        .env("CARGO_EPOS", "123456789")
        .env("CARGO_ENEG", "-1")
        .env("CARGO_EI64MAX", "9223372036854775807")
        .build();

    assert_eq!(
        gctx.get::<u64>("i64max").unwrap(),
        9_223_372_036_854_775_807
    );
    assert_eq!(
        gctx.get::<i64>("i64max").unwrap(),
        9_223_372_036_854_775_807
    );
    assert_eq!(
        gctx.get::<u64>("ei64max").unwrap(),
        9_223_372_036_854_775_807
    );
    assert_eq!(
        gctx.get::<i64>("ei64max").unwrap(),
        9_223_372_036_854_775_807
    );

    assert_error(
        gctx.get::<u32>("nneg").unwrap_err(),
        "\
error in [..].cargo/config.toml: could not load config key `nneg`

Caused by:
  invalid value: integer `-123456789`, expected u32",
    );
    assert_error(
        gctx.get::<u32>("eneg").unwrap_err(),
        "\
error in environment variable `CARGO_ENEG`: could not load config key `eneg`

Caused by:
  invalid value: integer `-1`, expected u32",
    );
    assert_error(
        gctx.get::<i8>("npos").unwrap_err(),
        "\
error in [..].cargo/config.toml: could not load config key `npos`

Caused by:
  invalid value: integer `123456789`, expected i8",
    );
    assert_error(
        gctx.get::<i8>("epos").unwrap_err(),
        "\
error in environment variable `CARGO_EPOS`: could not load config key `epos`

Caused by:
  invalid value: integer `123456789`, expected i8",
    );
}

#[cargo_test]
fn config_get_ssl_version_missing() {
    write_config_toml(
        "\
[http]
hello = 'world'
",
    );

    let gctx = new_gctx();

    assert!(gctx
        .get::<Option<SslVersionConfig>>("http.ssl-version")
        .unwrap()
        .is_none());
}

#[cargo_test]
fn config_get_ssl_version_single() {
    write_config_toml(
        "\
[http]
ssl-version = 'tlsv1.2'
",
    );

    let gctx = new_gctx();

    let a = gctx
        .get::<Option<SslVersionConfig>>("http.ssl-version")
        .unwrap()
        .unwrap();
    match a {
        SslVersionConfig::Single(v) => assert_eq!(&v, "tlsv1.2"),
        SslVersionConfig::Range(_) => panic!("Did not expect ssl version min/max."),
    };
}

#[cargo_test]
fn config_get_ssl_version_min_max() {
    write_config_toml(
        "\
[http]
ssl-version.min = 'tlsv1.2'
ssl-version.max = 'tlsv1.3'
",
    );

    let gctx = new_gctx();

    let a = gctx
        .get::<Option<SslVersionConfig>>("http.ssl-version")
        .unwrap()
        .unwrap();
    match a {
        SslVersionConfig::Single(_) => panic!("Did not expect exact ssl version."),
        SslVersionConfig::Range(range) => {
            assert_eq!(range.min, Some(String::from("tlsv1.2")));
            assert_eq!(range.max, Some(String::from("tlsv1.3")));
        }
    };
}

#[cargo_test]
fn config_get_ssl_version_both_forms_configured() {
    // this is not allowed
    write_config_toml(
        "\
[http]
ssl-version = 'tlsv1.1'
ssl-version.min = 'tlsv1.2'
ssl-version.max = 'tlsv1.3'
",
    );

    let gctx = new_gctx();

    assert_error(
        gctx.get::<SslVersionConfig>("http.ssl-version")
            .unwrap_err(),
        "\
could not load Cargo configuration

Caused by:
  could not parse TOML configuration in `[..]/.cargo/config.toml`

Caused by:
  TOML parse error at line 3, column 1
  |
3 | ssl-version.min = 'tlsv1.2'
  | ^
dotted key `ssl-version` attempted to extend non-table type (string)
",
    );
}

#[cargo_test]
/// Assert that unstable options can be configured with the `unstable` table in
/// cargo config files
fn unstable_table_notation() {
    write_config_toml(
        "\
[unstable]
print-im-a-teapot = true
",
    );
    let gctx = GlobalContextBuilder::new()
        .nightly_features_allowed(true)
        .build();
    assert_eq!(gctx.cli_unstable().print_im_a_teapot, true);
}

#[cargo_test]
/// Assert that dotted notation works for configuring unstable options
fn unstable_dotted_notation() {
    write_config_toml(
        "\
unstable.print-im-a-teapot = true
",
    );
    let gctx = GlobalContextBuilder::new()
        .nightly_features_allowed(true)
        .build();
    assert_eq!(gctx.cli_unstable().print_im_a_teapot, true);
}

#[cargo_test]
/// Assert that Zflags on the CLI take precedence over those from config
fn unstable_cli_precedence() {
    write_config_toml(
        "\
unstable.print-im-a-teapot = true
",
    );
    let gctx = GlobalContextBuilder::new()
        .nightly_features_allowed(true)
        .build();
    assert_eq!(gctx.cli_unstable().print_im_a_teapot, true);

    let gctx = GlobalContextBuilder::new()
        .unstable_flag("print-im-a-teapot=no")
        .build();
    assert_eq!(gctx.cli_unstable().print_im_a_teapot, false);
}

#[cargo_test]
/// Assert that attempting to set an unstable flag that doesn't exist via config
/// is ignored on stable
fn unstable_invalid_flag_ignored_on_stable() {
    write_config_toml(
        "\
unstable.an-invalid-flag = 'yes'
",
    );
    assert!(GlobalContextBuilder::new().build_err().is_ok());
}

#[cargo_test]
/// Assert that unstable options can be configured with the `unstable` table in
/// cargo config files
fn unstable_flags_ignored_on_stable() {
    write_config_toml(
        "\
[unstable]
print-im-a-teapot = true
",
    );
    // Enforce stable channel even when testing on nightly.
    let gctx = GlobalContextBuilder::new()
        .nightly_features_allowed(false)
        .build();
    assert_eq!(gctx.cli_unstable().print_im_a_teapot, false);
}

#[cargo_test]
fn table_merge_failure() {
    // Config::merge fails to merge entries in two tables.
    write_config_at(
        "foo/.cargo/config.toml",
        "
        [table]
        key = ['foo']
        ",
    );
    write_config_at(
        ".cargo/config.toml",
        "
        [table]
        key = 'bar'
        ",
    );

    #[derive(Debug, Deserialize)]
    #[allow(dead_code)]
    struct Table {
        key: StringList,
    }
    let gctx = GlobalContextBuilder::new().cwd("foo").build();
    assert_error(
        gctx.get::<Table>("table").unwrap_err(),
        "\
could not load Cargo configuration

Caused by:
  failed to merge configuration at `[..]/.cargo/config.toml`

Caused by:
  failed to merge key `table` between [..]/foo/.cargo/config.toml and [..]/.cargo/config.toml

Caused by:
  failed to merge key `key` between [..]/foo/.cargo/config.toml and [..]/.cargo/config.toml

Caused by:
  failed to merge config value from `[..]/.cargo/config.toml` into `[..]/foo/.cargo/config.toml`: \
  expected array, but found string",
    );
}

#[cargo_test]
fn non_string_in_array() {
    // Currently only strings are supported.
    write_config_toml("foo = [1, 2, 3]");
    let gctx = new_gctx();
    assert_error(
        gctx.get::<Vec<i32>>("foo").unwrap_err(),
        "\
could not load Cargo configuration

Caused by:
  failed to load TOML configuration from `[..]/.cargo/config.toml`

Caused by:
  failed to parse key `foo`

Caused by:
  expected string but found integer in list",
    );
}

#[cargo_test]
fn struct_with_opt_inner_struct() {
    // Struct with a key that is Option of another struct.
    // Check that can be defined with environment variable.
    #[derive(Deserialize)]
    struct Inner {
        value: Option<i32>,
    }
    #[derive(Deserialize)]
    struct Foo {
        inner: Option<Inner>,
    }
    let gctx = GlobalContextBuilder::new()
        .env("CARGO_FOO_INNER_VALUE", "12")
        .build();
    let f: Foo = gctx.get("foo").unwrap();
    assert_eq!(f.inner.unwrap().value.unwrap(), 12);
}

#[cargo_test]
fn struct_with_default_inner_struct() {
    // Struct with serde defaults.
    // Check that can be defined with environment variable.
    #[derive(Deserialize, Default)]
    #[serde(default)]
    struct Inner {
        value: i32,
    }
    #[derive(Deserialize, Default)]
    #[serde(default)]
    struct Foo {
        inner: Inner,
    }
    let gctx = GlobalContextBuilder::new()
        .env("CARGO_FOO_INNER_VALUE", "12")
        .build();
    let f: Foo = gctx.get("foo").unwrap();
    assert_eq!(f.inner.value, 12);
}

#[cargo_test]
fn overlapping_env_config() {
    // Issue where one key is a prefix of another.
    #[derive(Deserialize)]
    #[serde(rename_all = "kebab-case")]
    struct Ambig {
        debug: Option<u32>,
        debug_assertions: Option<bool>,
    }
    let gctx = GlobalContextBuilder::new()
        .env("CARGO_AMBIG_DEBUG_ASSERTIONS", "true")
        .build();

    let s: Ambig = gctx.get("ambig").unwrap();
    assert_eq!(s.debug_assertions, Some(true));
    assert_eq!(s.debug, None);

    let gctx = GlobalContextBuilder::new()
        .env("CARGO_AMBIG_DEBUG", "0")
        .build();
    let s: Ambig = gctx.get("ambig").unwrap();
    assert_eq!(s.debug_assertions, None);
    assert_eq!(s.debug, Some(0));

    let gctx = GlobalContextBuilder::new()
        .env("CARGO_AMBIG_DEBUG", "1")
        .env("CARGO_AMBIG_DEBUG_ASSERTIONS", "true")
        .build();
    let s: Ambig = gctx.get("ambig").unwrap();
    assert_eq!(s.debug_assertions, Some(true));
    assert_eq!(s.debug, Some(1));
}

#[cargo_test]
fn overlapping_env_with_defaults_errors_out() {
    // Issue where one key is a prefix of another.
    // This is a limitation of mapping environment variables on to a hierarchy.
    // Check that we error out when we hit ambiguity in this way, rather than
    // the more-surprising defaulting through.
    // If, in the future, we can handle this more correctly, feel free to delete
    // this test.
    #[derive(Deserialize, Default)]
    #[serde(default, rename_all = "kebab-case")]
    struct Ambig {
        debug: u32,
        debug_assertions: bool,
    }
    let gctx = GlobalContextBuilder::new()
        .env("CARGO_AMBIG_DEBUG_ASSERTIONS", "true")
        .build();
    let err = gctx.get::<Ambig>("ambig").err().unwrap();
    assert!(format!("{}", err).contains("missing config key `ambig.debug`"));

    let gctx = GlobalContextBuilder::new()
        .env("CARGO_AMBIG_DEBUG", "5")
        .build();
    let s: Ambig = gctx.get("ambig").unwrap();
    assert_eq!(s.debug_assertions, bool::default());
    assert_eq!(s.debug, 5);

    let gctx = GlobalContextBuilder::new()
        .env("CARGO_AMBIG_DEBUG", "1")
        .env("CARGO_AMBIG_DEBUG_ASSERTIONS", "true")
        .build();
    let s: Ambig = gctx.get("ambig").unwrap();
    assert_eq!(s.debug_assertions, true);
    assert_eq!(s.debug, 1);
}

#[cargo_test]
fn struct_with_overlapping_inner_struct_and_defaults() {
    // Struct with serde defaults.
    // Check that can be defined with environment variable.
    #[derive(Deserialize, Default)]
    #[serde(default)]
    struct Inner {
        value: i32,
    }

    // Containing struct with a prefix of inner
    //
    // This is a limitation of mapping environment variables on to a hierarchy.
    // Check that we error out when we hit ambiguity in this way, rather than
    // the more-surprising defaulting through.
    // If, in the future, we can handle this more correctly, feel free to delete
    // this case.
    #[derive(Deserialize, Default)]
    struct PrefixContainer {
        inn: bool,
        inner: Inner,
    }
    let gctx = GlobalContextBuilder::new()
        .env("CARGO_PREFIXCONTAINER_INNER_VALUE", "12")
        .build();
    let err = gctx
        .get::<PrefixContainer>("prefixcontainer")
        .err()
        .unwrap();
    assert!(format!("{}", err).contains("missing field `inn`"));
    let gctx = GlobalContextBuilder::new()
        .env("CARGO_PREFIXCONTAINER_INNER_VALUE", "12")
        .env("CARGO_PREFIXCONTAINER_INN", "true")
        .build();
    let f: PrefixContainer = gctx.get("prefixcontainer").unwrap();
    assert_eq!(f.inner.value, 12);
    assert_eq!(f.inn, true);

    // Use default attribute of serde, then we can skip setting the inn field
    #[derive(Deserialize, Default)]
    #[serde(default)]
    struct PrefixContainerFieldDefault {
        inn: bool,
        inner: Inner,
    }
    let gctx = GlobalContextBuilder::new()
        .env("CARGO_PREFIXCONTAINER_INNER_VALUE", "12")
        .build();
    let f = gctx
        .get::<PrefixContainerFieldDefault>("prefixcontainer")
        .unwrap();
    assert_eq!(f.inner.value, 12);
    assert_eq!(f.inn, false);

    // Containing struct where the inner value's field is a prefix of another
    //
    // This is a limitation of mapping environment variables on to a hierarchy.
    // Check that we error out when we hit ambiguity in this way, rather than
    // the more-surprising defaulting through.
    // If, in the future, we can handle this more correctly, feel free to delete
    // this case.
    #[derive(Deserialize, Default)]
    #[serde(default)]
    struct InversePrefixContainer {
        inner_field: bool,
        inner: Inner,
    }
    let gctx = GlobalContextBuilder::new()
        .env("CARGO_INVERSEPREFIXCONTAINER_INNER_VALUE", "12")
        .build();
    let f: InversePrefixContainer = gctx.get("inverseprefixcontainer").unwrap();
    assert_eq!(f.inner_field, bool::default());
    assert_eq!(f.inner.value, 12);
}

#[cargo_test]
fn string_list_tricky_env() {
    // Make sure StringList handles typed env values.
    let gctx = GlobalContextBuilder::new()
        .env("CARGO_KEY1", "123")
        .env("CARGO_KEY2", "true")
        .env("CARGO_KEY3", "1 2")
        .build();
    let x = gctx.get::<StringList>("key1").unwrap();
    assert_eq!(x.as_slice(), &["123".to_string()]);
    let x = gctx.get::<StringList>("key2").unwrap();
    assert_eq!(x.as_slice(), &["true".to_string()]);
    let x = gctx.get::<StringList>("key3").unwrap();
    assert_eq!(x.as_slice(), &["1".to_string(), "2".to_string()]);
}

#[cargo_test]
fn string_list_wrong_type() {
    // What happens if StringList is given then wrong type.
    write_config_toml("some_list = 123");
    let gctx = GlobalContextBuilder::new().build();
    assert_error(
        gctx.get::<StringList>("some_list").unwrap_err(),
        "\
invalid configuration for key `some_list`
expected a string or array of strings, but found a integer for `some_list` in [..]/.cargo/config.toml",
    );

    write_config_toml("some_list = \"1 2\"");
    let gctx = GlobalContextBuilder::new().build();
    let x = gctx.get::<StringList>("some_list").unwrap();
    assert_eq!(x.as_slice(), &["1".to_string(), "2".to_string()]);
}

#[cargo_test]
fn string_list_advanced_env() {
    // StringList with advanced env.
    let gctx = GlobalContextBuilder::new()
        .unstable_flag("advanced-env")
        .env("CARGO_KEY1", "[]")
        .env("CARGO_KEY2", "['1 2', '3']")
        .env("CARGO_KEY3", "[123]")
        .build();
    let x = gctx.get::<StringList>("key1").unwrap();
    assert_eq!(x.as_slice(), &[] as &[String]);
    let x = gctx.get::<StringList>("key2").unwrap();
    assert_eq!(x.as_slice(), &["1 2".to_string(), "3".to_string()]);
    assert_error(
        gctx.get::<StringList>("key3").unwrap_err(),
        "error in environment variable `CARGO_KEY3`: expected string, found integer",
    );
}

#[cargo_test]
fn parse_strip_with_string() {
    write_config_toml(
        "\
[profile.release]
strip = 'debuginfo'
",
    );

    let gctx = new_gctx();

    let p: cargo_toml::TomlProfile = gctx.get("profile.release").unwrap();
    let strip = p.strip.unwrap();
    assert_eq!(
        strip,
        cargo_toml::StringOrBool::String("debuginfo".to_string())
    );
}

#[cargo_test]
fn cargo_target_empty_cfg() {
    write_config_toml(
        "\
[build]
target-dir = ''
",
    );

    let gctx = new_gctx();

    assert_error(
        gctx.target_dir().unwrap_err(),
        "the target directory is set to an empty string in [..]/.cargo/config.toml",
    );
}

#[cargo_test]
fn cargo_target_empty_env() {
    let project = project().build();

    project.cargo("check")
        .env("CARGO_TARGET_DIR", "")
        .with_stderr_data(str![[r#"
[ERROR] the target directory is set to an empty string in the `CARGO_TARGET_DIR` environment variable

"#]])
        .with_status(101)
        .run()
}

#[cargo_test]
fn all_profile_options() {
    // Check that all profile options can be serialized/deserialized.
    let base_settings = cargo_toml::TomlProfile {
        opt_level: Some(cargo_toml::TomlOptLevel("0".to_string())),
        lto: Some(cargo_toml::StringOrBool::String("thin".to_string())),
        codegen_backend: Some(String::from("example")),
        codegen_units: Some(123),
        debug: Some(cargo_toml::TomlDebugInfo::Limited),
        split_debuginfo: Some("packed".to_string()),
        debug_assertions: Some(true),
        rpath: Some(true),
        panic: Some("abort".to_string()),
        overflow_checks: Some(true),
        incremental: Some(true),
        dir_name: Some(String::from("dir_name")),
        inherits: Some(String::from("debug")),
        strip: Some(cargo_toml::StringOrBool::String("symbols".to_string())),
        package: None,
        build_override: None,
        rustflags: None,
        trim_paths: None,
    };
    let mut overrides = BTreeMap::new();
    let key = cargo_toml::ProfilePackageSpec::Spec(PackageIdSpec::parse("foo").unwrap());
    overrides.insert(key, base_settings.clone());
    let profile = cargo_toml::TomlProfile {
        build_override: Some(Box::new(base_settings.clone())),
        package: Some(overrides),
        ..base_settings
    };
    let profile_toml = toml::to_string(&profile).unwrap();
    let roundtrip: cargo_toml::TomlProfile = toml::from_str(&profile_toml).unwrap();
    let roundtrip_toml = toml::to_string(&roundtrip).unwrap();
    assert_e2e().eq(&roundtrip_toml, &profile_toml);
}

#[cargo_test]
fn value_in_array() {
    // Value<String> in an array should work
    let root_path = paths::root().join(".cargo/config.toml");
    write_config_at(
        &root_path,
        "\
[net.ssh]
known-hosts = [
    \"example.com ...\",
    \"example.net ...\",
]
",
    );

    let foo_path = paths::root().join("foo/.cargo/config.toml");
    write_config_at(
        &foo_path,
        "\
[net.ssh]
known-hosts = [
    \"example.org ...\",
]
",
    );

    let gctx = GlobalContextBuilder::new()
        .cwd("foo")
        // environment variables don't actually work for known-hosts due to
        // space splitting, but this is included here just to validate that
        // they work (particularly if other Vec<Value> config vars are added
        // in the future).
        .env("CARGO_NET_SSH_KNOWN_HOSTS", "env-example")
        .build();
    let net_config = gctx.net_config().unwrap();
    let kh = net_config
        .ssh
        .as_ref()
        .unwrap()
        .known_hosts
        .as_ref()
        .unwrap();
    assert_eq!(kh.len(), 4);
    assert_eq!(kh[0].val, "example.com ...");
    assert_eq!(kh[0].definition, Definition::Path(root_path.clone()));
    assert_eq!(kh[1].val, "example.net ...");
    assert_eq!(kh[1].definition, Definition::Path(root_path.clone()));
    assert_eq!(kh[2].val, "example.org ...");
    assert_eq!(kh[2].definition, Definition::Path(foo_path.clone()));
    assert_eq!(kh[3].val, "env-example");
    assert_eq!(
        kh[3].definition,
        Definition::Environment("CARGO_NET_SSH_KNOWN_HOSTS".to_string())
    );
}

#[cargo_test]
fn debuginfo_parsing() {
    let gctx = GlobalContextBuilder::new().build();
    let p: cargo_toml::TomlProfile = gctx.get("profile.dev").unwrap();
    assert_eq!(p.debug, None);

    let env_test_cases = [
        (TomlDebugInfo::None, ["false", "0", "none"].as_slice()),
        (TomlDebugInfo::LineDirectivesOnly, &["line-directives-only"]),
        (TomlDebugInfo::LineTablesOnly, &["line-tables-only"]),
        (TomlDebugInfo::Limited, &["1", "limited"]),
        (TomlDebugInfo::Full, &["true", "2", "full"]),
    ];
    for (expected, config_strs) in env_test_cases {
        for &val in config_strs {
            let gctx = GlobalContextBuilder::new()
                .env("CARGO_PROFILE_DEV_DEBUG", val)
                .build();
            let debug: TomlDebugInfo = gctx.get("profile.dev.debug").unwrap();
            assert_eq!(debug, expected, "failed to parse {val}");
        }
    }

    let toml_test_cases = [
        (TomlDebugInfo::None, ["false", "0", "\"none\""].as_slice()),
        (
            TomlDebugInfo::LineDirectivesOnly,
            &["\"line-directives-only\""],
        ),
        (TomlDebugInfo::LineTablesOnly, &["\"line-tables-only\""]),
        (TomlDebugInfo::Limited, &["1", "\"limited\""]),
        (TomlDebugInfo::Full, &["true", "2", "\"full\""]),
    ];
    for (expected, config_strs) in toml_test_cases {
        for &val in config_strs {
            let gctx = GlobalContextBuilder::new()
                .config_arg(format!("profile.dev.debug={val}"))
                .build();
            let debug: TomlDebugInfo = gctx.get("profile.dev.debug").unwrap();
            assert_eq!(debug, expected, "failed to parse {val}");
        }
    }

    let toml_err_cases = ["\"\"", "\"unrecognized\"", "3"];
    for err_val in toml_err_cases {
        let gctx = GlobalContextBuilder::new()
            .config_arg(format!("profile.dev.debug={err_val}"))
            .build();
        let err = gctx.get::<TomlDebugInfo>("profile.dev.debug").unwrap_err();
        assert!(err
            .to_string()
            .ends_with("could not load config key `profile.dev.debug`"));
    }
}

#[cargo_test]
fn build_jobs_missing() {
    write_config_toml(
        "\
[build]
",
    );

    let gctx = new_gctx();

    assert!(gctx
        .get::<Option<JobsConfig>>("build.jobs")
        .unwrap()
        .is_none());
}

#[cargo_test]
fn build_jobs_default() {
    write_config_toml(
        "\
[build]
jobs = \"default\"
",
    );

    let gctx = new_gctx();

    let a = gctx
        .get::<Option<JobsConfig>>("build.jobs")
        .unwrap()
        .unwrap();

    match a {
        JobsConfig::String(v) => assert_eq!(&v, "default"),
        JobsConfig::Integer(_) => panic!("Did not except an integer."),
    }
}

#[cargo_test]
fn build_jobs_integer() {
    write_config_toml(
        "\
[build]
jobs = 2
",
    );

    let gctx = new_gctx();

    let a = gctx
        .get::<Option<JobsConfig>>("build.jobs")
        .unwrap()
        .unwrap();

    match a {
        JobsConfig::String(_) => panic!("Did not except an integer."),
        JobsConfig::Integer(v) => assert_eq!(v, 2),
    }
}

#[cargo_test]
fn trim_paths_parsing() {
    let gctx = GlobalContextBuilder::new().build();
    let p: cargo_toml::TomlProfile = gctx.get("profile.dev").unwrap();
    assert_eq!(p.trim_paths, None);

    let test_cases = [
        (TomlTrimPathsValue::Diagnostics.into(), "diagnostics"),
        (TomlTrimPathsValue::Macro.into(), "macro"),
        (TomlTrimPathsValue::Object.into(), "object"),
    ];
    for (expected, val) in test_cases {
        // env
        let gctx = GlobalContextBuilder::new()
            .env("CARGO_PROFILE_DEV_TRIM_PATHS", val)
            .build();
        let trim_paths: TomlTrimPaths = gctx.get("profile.dev.trim-paths").unwrap();
        assert_eq!(trim_paths, expected, "failed to parse {val}");

        // config.toml
        let gctx = GlobalContextBuilder::new()
            .config_arg(format!("profile.dev.trim-paths='{val}'"))
            .build();
        let trim_paths: TomlTrimPaths = gctx.get("profile.dev.trim-paths").unwrap();
        assert_eq!(trim_paths, expected, "failed to parse {val}");
    }

    let test_cases = [(TomlTrimPaths::none(), false), (TomlTrimPaths::All, true)];

    for (expected, val) in test_cases {
        // env
        let gctx = GlobalContextBuilder::new()
            .env("CARGO_PROFILE_DEV_TRIM_PATHS", format!("{val}"))
            .build();
        let trim_paths: TomlTrimPaths = gctx.get("profile.dev.trim-paths").unwrap();
        assert_eq!(trim_paths, expected, "failed to parse {val}");

        // config.toml
        let gctx = GlobalContextBuilder::new()
            .config_arg(format!("profile.dev.trim-paths={val}"))
            .build();
        let trim_paths: TomlTrimPaths = gctx.get("profile.dev.trim-paths").unwrap();
        assert_eq!(trim_paths, expected, "failed to parse {val}");
    }

    let expected = vec![
        TomlTrimPathsValue::Diagnostics,
        TomlTrimPathsValue::Macro,
        TomlTrimPathsValue::Object,
    ]
    .into();
    let val = r#"["diagnostics", "macro", "object"]"#;
    // config.toml
    let gctx = GlobalContextBuilder::new()
        .config_arg(format!("profile.dev.trim-paths={val}"))
        .build();
    let trim_paths: TomlTrimPaths = gctx.get("profile.dev.trim-paths").unwrap();
    assert_eq!(trim_paths, expected, "failed to parse {val}");
}

#[cargo_test]
fn missing_fields() {
    #[derive(Deserialize, Default, Debug)]
    struct Foo {
        bar: Bar,
    }

    #[derive(Deserialize, Default, Debug)]
    struct Bar {
        bax: bool,
        baz: bool,
    }

    let gctx = GlobalContextBuilder::new()
        .env("CARGO_FOO_BAR_BAZ", "true")
        .build();
    assert_error(
        gctx.get::<Foo>("foo").unwrap_err(),
        "\
could not load config key `foo.bar`

Caused by:
  missing field `bax`",
    );
    let gctx: GlobalContext = GlobalContextBuilder::new()
        .env("CARGO_FOO_BAR_BAZ", "true")
        .env("CARGO_FOO_BAR_BAX", "true")
        .build();
    let foo = gctx.get::<Foo>("foo").unwrap();
    assert_eq!(foo.bar.bax, true);
    assert_eq!(foo.bar.baz, true);

    let gctx: GlobalContext = GlobalContextBuilder::new()
        .config_arg("foo.bar.baz=true")
        .build();
    assert_error(
        gctx.get::<Foo>("foo").unwrap_err(),
        "\
error in --config cli option: could not load config key `foo.bar`

Caused by:
  missing field `bax`",
    );
}

#[cargo_test]
fn git_features() {
    let gctx = GlobalContextBuilder::new()
        .env("CARGO_UNSTABLE_GIT", "shallow-index")
        .build();
    assert!(do_check(
        gctx,
        Some(GitFeatures {
            shallow_index: true,
            ..GitFeatures::default()
        }),
    ));

    let gctx = GlobalContextBuilder::new()
        .env("CARGO_UNSTABLE_GIT", "shallow-index,abc")
        .build();
    assert_error(
        gctx.get::<Option<cargo::core::CliUnstable>>("unstable")
            .unwrap_err(),
        "\
error in environment variable `CARGO_UNSTABLE_GIT`: could not load config key `unstable.git`

Caused by:
[..]unstable 'git' only takes [..] as valid inputs",
    );

    let gctx = GlobalContextBuilder::new()
        .env("CARGO_UNSTABLE_GIT", "shallow-deps")
        .build();
    assert!(do_check(
        gctx,
        Some(GitFeatures {
            shallow_index: false,
            shallow_deps: true,
        }),
    ));

    let gctx = GlobalContextBuilder::new()
        .env("CARGO_UNSTABLE_GIT", "true")
        .build();
    assert!(do_check(gctx, Some(GitFeatures::all())));

    let gctx = GlobalContextBuilder::new()
        .env("CARGO_UNSTABLE_GIT_SHALLOW_INDEX", "true")
        .build();
    assert!(do_check(
        gctx,
        Some(GitFeatures {
            shallow_index: true,
            ..Default::default()
        }),
    ));

    let gctx = GlobalContextBuilder::new()
        .env("CARGO_UNSTABLE_GIT_SHALLOW_INDEX", "true")
        .env("CARGO_UNSTABLE_GIT_SHALLOW_DEPS", "true")
        .build();
    assert!(do_check(
        gctx,
        Some(GitFeatures {
            shallow_index: true,
            shallow_deps: true,
            ..Default::default()
        }),
    ));

    write_config_toml(
        "\
[unstable]
git = 'shallow-index'
",
    );
    let gctx = GlobalContextBuilder::new().build();
    assert!(do_check(
        gctx,
        Some(GitFeatures {
            shallow_index: true,
            shallow_deps: false,
        }),
    ));

    write_config_toml(
        "\
    [unstable.git]
    shallow_deps = false
    shallow_index = true
    ",
    );
    let gctx = GlobalContextBuilder::new().build();
    assert!(do_check(
        gctx,
        Some(GitFeatures {
            shallow_index: true,
            shallow_deps: false,
            ..Default::default()
        }),
    ));

    write_config_toml(
        "\
    [unstable.git]
    ",
    );
    let gctx = GlobalContextBuilder::new().build();
    assert!(do_check(gctx, Some(Default::default())));

    fn do_check(gctx: GlobalContext, expect: Option<GitFeatures>) -> bool {
        let unstable_flags = gctx
            .get::<Option<cargo::core::CliUnstable>>("unstable")
            .unwrap()
            .unwrap();
        unstable_flags.git == expect
    }
}

#[cargo_test]
fn gitoxide_features() {
    let gctx = GlobalContextBuilder::new()
        .env("CARGO_UNSTABLE_GITOXIDE", "fetch")
        .build();
    assert!(do_check(
        gctx,
        Some(GitoxideFeatures {
            fetch: true,
            ..GitoxideFeatures::default()
        }),
    ));

    let gctx = GlobalContextBuilder::new()
        .env("CARGO_UNSTABLE_GITOXIDE", "fetch,abc")
        .build();

    assert_error(
    gctx.get::<Option<cargo::core::CliUnstable>>("unstable")
        .unwrap_err(),
    "\
error in environment variable `CARGO_UNSTABLE_GITOXIDE`: could not load config key `unstable.gitoxide`

Caused by:
[..]unstable 'gitoxide' only takes [..] as valid inputs, for shallow fetches see `-Zgit=shallow-index,shallow-deps`",
);

    let gctx = GlobalContextBuilder::new()
        .env("CARGO_UNSTABLE_GITOXIDE", "true")
        .build();
    assert!(do_check(gctx, Some(GitoxideFeatures::all())));

    let gctx = GlobalContextBuilder::new()
        .env("CARGO_UNSTABLE_GITOXIDE_FETCH", "true")
        .build();
    assert!(do_check(
        gctx,
        Some(GitoxideFeatures {
            fetch: true,
            ..Default::default()
        }),
    ));

    write_config_toml(
        "\
[unstable]
gitoxide = \"fetch\"
",
    );
    let gctx = GlobalContextBuilder::new().build();
    assert!(do_check(
        gctx,
        Some(GitoxideFeatures {
            fetch: true,
            ..GitoxideFeatures::default()
        }),
    ));

    write_config_toml(
        "\
    [unstable.gitoxide]
    fetch = true
    checkout = false
    internal_use_git2 = false
    ",
    );
    let gctx = GlobalContextBuilder::new().build();
    assert!(do_check(
        gctx,
        Some(GitoxideFeatures {
            fetch: true,
            checkout: false,
            internal_use_git2: false,
        }),
    ));

    write_config_toml(
        "\
    [unstable.gitoxide]
    ",
    );
    let gctx = GlobalContextBuilder::new().build();
    assert!(do_check(gctx, Some(Default::default())));

    fn do_check(gctx: GlobalContext, expect: Option<GitoxideFeatures>) -> bool {
        let unstable_flags = gctx
            .get::<Option<cargo::core::CliUnstable>>("unstable")
            .unwrap()
            .unwrap();
        unstable_flags.gitoxide == expect
    }
}
