//! Tests for `include` config field.

use crate::prelude::*;
use cargo_test_support::str;

use super::config::GlobalContextBuilder;
use super::config::assert_error;
use super::config::write_config_at;
use super::config::write_config_toml;

#[cargo_test]
fn simple() {
    // Simple test.
    write_config_at(
        ".cargo/config.toml",
        "
        include = ['other.toml']
        key1 = 1
        key2 = 2
        ",
    );
    write_config_at(
        ".cargo/other.toml",
        "
        key2 = 3
        key3 = 4
        ",
    );
    let gctx = GlobalContextBuilder::new().build();
    assert_eq!(gctx.get::<i32>("key1").unwrap(), 1);
    assert_eq!(gctx.get::<i32>("key2").unwrap(), 2);
    assert_eq!(gctx.get::<i32>("key3").unwrap(), 4);
}

#[cargo_test]
fn mix_of_hierarchy_and_include() {
    write_config_at(
        "foo/.cargo/config.toml",
        "
        include = ['other.toml']
        key1 = 1

        # also make sure unstable flags merge in the correct order
        [unstable]
        features = ['1']
        ",
    );
    write_config_at(
        "foo/.cargo/other.toml",
        "
        key1 = 2
        key2 = 2

        [unstable]
        features = ['2']
        ",
    );
    write_config_at(
        ".cargo/config.toml",
        "
        include = ['other.toml']
        key1 = 3
        key2 = 3
        key3 = 3

        [unstable]
        features = ['3']
        ",
    );
    write_config_at(
        ".cargo/other.toml",
        "
        key1 = 4
        key2 = 4
        key3 = 4
        key4 = 4

        [unstable]
        features = ['4']
        ",
    );
    let gctx = GlobalContextBuilder::new()
        .cwd("foo")
        .nightly_features_allowed(true)
        .build();
    assert_eq!(gctx.get::<i32>("key1").unwrap(), 1);
    assert_eq!(gctx.get::<i32>("key2").unwrap(), 2);
    assert_eq!(gctx.get::<i32>("key3").unwrap(), 3);
    assert_eq!(gctx.get::<i32>("key4").unwrap(), 4);
    assert_eq!(
        gctx.get::<Vec<String>>("unstable.features").unwrap(),
        vec![
            "4".to_string(),
            "3".to_string(),
            "2".to_string(),
            "1".to_string()
        ]
    );
}

#[cargo_test]
fn left_to_right_bottom_to_top() {
    // How it merges multiple nested includes.
    write_config_at(
        ".cargo/config.toml",
        "
        include = ['left-middle.toml', 'right-middle.toml']
        top = 1
        ",
    );
    write_config_at(
        ".cargo/right-middle.toml",
        "
        include = ['right-bottom.toml']
        top = 0
        right-middle = 0
        ",
    );
    write_config_at(
        ".cargo/right-bottom.toml",
        "
        top = -1
        right-middle = -1
        right-bottom = -1
        ",
    );
    write_config_at(
        ".cargo/left-middle.toml",
        "
        include = ['left-bottom.toml']
        top = -2
        right-middle = -2
        right-bottom = -2
        left-middle = -2
        ",
    );
    write_config_at(
        ".cargo/left-bottom.toml",
        "
        top = -3
        right-middle = -3
        right-bottom = -3
        left-middle = -3
        left-bottom = -3
        ",
    );
    let gctx = GlobalContextBuilder::new().build();
    assert_eq!(gctx.get::<i32>("top").unwrap(), 1);
    assert_eq!(gctx.get::<i32>("right-middle").unwrap(), 0);
    assert_eq!(gctx.get::<i32>("right-bottom").unwrap(), -1);
    assert_eq!(gctx.get::<i32>("left-middle").unwrap(), -2);
    assert_eq!(gctx.get::<i32>("left-bottom").unwrap(), -3);
}

#[cargo_test]
fn nested_include_resolves_relative_to_including_file() {
    write_config_at(
        ".cargo/config.toml",
        "
        include = ['../config/cargo.toml']
        ",
    );
    write_config_at(
        "config/cargo.toml",
        "
        include = ['other.toml']
        middle = 10
        ",
    );
    write_config_at(
        "config/other.toml",
        "
        nested = 42
        ",
    );

    // This should not be included,
    // because `include` path is resolved relative to the including config
    // (where the `include` is defined)
    write_config_at(
        ".cargo/other.toml",
        "
        INVALID = SYNTAX
        ",
    );

    let gctx = GlobalContextBuilder::new().build();

    assert_eq!(gctx.get::<i32>("nested").unwrap(), 42);
    assert_eq!(gctx.get::<i32>("middle").unwrap(), 10);
}

#[cargo_test]
fn missing_file() {
    // Error when there's a missing file.
    write_config_toml("include=['missing.toml']");
    // Config values are lazy loaded and fail at first retrieval.
    // This ensures simple commands like `cargo --version` working even when
    let gctx = GlobalContextBuilder::new().build();
    assert_error(
        gctx.get::<bool>("foo").unwrap_err(),
        str![[r#"
could not load Cargo configuration

Caused by:
  failed to load config include `missing.toml` from `[ROOT]/.cargo/config.toml`

Caused by:
  failed to read configuration file `[ROOT]/.cargo/missing.toml`

Caused by:
  [NOT_FOUND]
"#]],
    );
}

#[cargo_test]
fn wrong_file_extension() {
    // Error when it doesn't end with `.toml`.
    write_config_toml("include=['config.png']");
    // Config values are lazy loaded and fail at first retrieval.
    // This ensures simple commands like `cargo --version` working even when
    let gctx = GlobalContextBuilder::new().build();
    assert_error(
        gctx.get::<bool>("foo").unwrap_err(),
        str![[r#"
could not load Cargo configuration

Caused by:
  expected a config include path ending with `.toml`, but found `config.png` from `[ROOT]/.cargo/config.toml`
"#]],
    );
}

#[cargo_test]
fn cycle() {
    // Detects a cycle.
    write_config_at(".cargo/config.toml", "include=['one.toml']");
    write_config_at(".cargo/one.toml", "include=['two.toml']");
    write_config_at(".cargo/two.toml", "include=['config.toml']");
    let gctx = GlobalContextBuilder::new().build();
    assert_error(
        gctx.get::<bool>("foo").unwrap_err(),
        str![[r#"
could not load Cargo configuration

Caused by:
  failed to load config include `one.toml` from `[ROOT]/.cargo/config.toml`

Caused by:
  failed to load config include `two.toml` from `[ROOT]/.cargo/one.toml`

Caused by:
  failed to load config include `config.toml` from `[ROOT]/.cargo/two.toml`

Caused by:
  config `include` cycle detected with path `[ROOT]/.cargo/config.toml`
"#]],
    );
}

#[cargo_test]
fn cli_include() {
    // Using --config with include.
    // CLI takes priority over files.
    write_config_at(
        ".cargo/config.toml",
        "
        foo = 1
        bar = 2
        ",
    );
    write_config_at(".cargo/config-foo.toml", "foo = 2");
    let gctx = GlobalContextBuilder::new()
        .config_arg("include=['.cargo/config-foo.toml']")
        .build();
    assert_eq!(gctx.get::<i32>("foo").unwrap(), 2);
    assert_eq!(gctx.get::<i32>("bar").unwrap(), 2);
}

#[cargo_test]
fn bad_format() {
    // Not a valid format.
    write_config_toml("include = 1");
    // Config values are lazy loaded and fail at first retrieval.
    // This ensures simple commands like `cargo --version` working even when
    let gctx = GlobalContextBuilder::new().build();
    assert_error(
        gctx.get::<bool>("foo").unwrap_err(),
        str![[r#"
could not load Cargo configuration

Caused by:
  expected a list of strings or a list of tables, but found integer at `include` in `[ROOT]/.cargo/config.toml
"#]],
    );
}

#[cargo_test]
fn cli_include_failed() {
    // Error message when CLI include fails to load.
    let gctx = GlobalContextBuilder::new()
        .config_arg("include=['foobar.toml']")
        .build_err();
    assert_error(
        gctx.unwrap_err(),
        str![[r#"
failed to load --config include

Caused by:
  failed to load config include `foobar.toml` from `--config cli option`

Caused by:
  failed to read configuration file `[ROOT]/foobar.toml`

Caused by:
  [NOT_FOUND]
"#]],
    );
}

#[cargo_test]
fn cli_merge_failed() {
    // Error message when CLI include merge fails.
    write_config_toml("foo = ['a']");
    write_config_at(
        ".cargo/other.toml",
        "
        foo = 'b'
        ",
    );
    let gctx = GlobalContextBuilder::new()
        .config_arg("include=['.cargo/other.toml']")
        .build_err();
    // Maybe this error message should mention it was from an include file?
    assert_error(
        gctx.unwrap_err(),
        str![[r#"
failed to merge key `foo` between [ROOT]/.cargo/config.toml and [ROOT]/.cargo/other.toml

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

#[cargo_test]
fn cli_include_take_priority_over_env() {
    write_config_at(".cargo/include.toml", "k='include'");

    // k=env
    let gctx = GlobalContextBuilder::new().env("CARGO_K", "env").build();
    assert_eq!(gctx.get::<String>("k").unwrap(), "env");

    // k=env
    // --config 'include=".cargo/include.toml"'
    let gctx = GlobalContextBuilder::new()
        .env("CARGO_K", "env")
        .config_arg("include=['.cargo/include.toml']")
        .build();
    assert_eq!(gctx.get::<String>("k").unwrap(), "include");

    // k=env
    // --config '.cargo/foo.toml'
    write_config_at(".cargo/foo.toml", "include=['include.toml']");
    let gctx = GlobalContextBuilder::new()
        .env("CARGO_K", "env")
        .config_arg(".cargo/foo.toml")
        .build();
    assert_eq!(gctx.get::<String>("k").unwrap(), "include");
}

#[cargo_test]
fn inline_table_style() {
    write_config_at(
        ".cargo/config.toml",
        "
        include = ['simple.toml', { path = 'other.toml' }]
        key1 = 1
        key2 = 2
        ",
    );
    write_config_at(
        ".cargo/simple.toml",
        "
        key2 = 3
        key3 = 4
        ",
    );
    write_config_at(
        ".cargo/other.toml",
        "
        key3 = 5
        key4 = 6
        ",
    );

    let gctx = GlobalContextBuilder::new().build();
    assert_eq!(gctx.get::<i32>("key1").unwrap(), 1);
    assert_eq!(gctx.get::<i32>("key2").unwrap(), 2);
    assert_eq!(gctx.get::<i32>("key3").unwrap(), 5);
    assert_eq!(gctx.get::<i32>("key4").unwrap(), 6);
}

#[cargo_test]
fn array_of_tables_style() {
    write_config_at(
        ".cargo/config.toml",
        "
        key1 = 1
        key2 = 2

        [[include]]
        path = 'other1.toml'

        [[include]]
        path = 'other2.toml'
        ",
    );
    write_config_at(
        ".cargo/other1.toml",
        "
        key2 = 3
        key3 = 4
        ",
    );
    write_config_at(
        ".cargo/other2.toml",
        "
        key3 = 5
        key4 = 6
        ",
    );

    let gctx = GlobalContextBuilder::new().build();
    assert_eq!(gctx.get::<i32>("key1").unwrap(), 1);
    assert_eq!(gctx.get::<i32>("key2").unwrap(), 2);
    assert_eq!(gctx.get::<i32>("key3").unwrap(), 5);
    assert_eq!(gctx.get::<i32>("key4").unwrap(), 6);
}

#[cargo_test]
fn table_with_unknown_fields() {
    // Unknown fields should be ignored for forward compatibility
    write_config_at(
        ".cargo/config.toml",
        "
        key1 = 1

        [[include]]
        path = 'other.toml'
        unknown_foo = true
        unknown_bar = 123
        ",
    );
    write_config_at(
        ".cargo/other.toml",
        "
        key2 = 2
        ",
    );

    let gctx = GlobalContextBuilder::new().build();
    assert_eq!(gctx.get::<i32>("key1").unwrap(), 1);
    assert_eq!(gctx.get::<i32>("key2").unwrap(), 2);
}

#[cargo_test]
fn table_missing_required_field() {
    // Missing required field should fail
    write_config_at(
        ".cargo/config.toml",
        "
        key1 = 1
        [[include]]
        random_field = true
        ",
    );

    // Config values are lazy loaded and fail at first retrieval.
    // This ensures simple commands like `cargo --version` working even when
    let gctx = GlobalContextBuilder::new().build();
    assert_error(
        gctx.get::<bool>("foo").unwrap_err(),
        str![[r#"
could not load Cargo configuration

Caused by:
  missing field `path` at `include[0]` in `[ROOT]/.cargo/config.toml`
"#]],
    );
}

#[cargo_test]
fn optional_include_missing_and_existing() {
    write_config_at(
        ".cargo/config.toml",
        "
        key1 = 1

        [[include]]
        path = 'missing.toml'
        optional = true

        [[include]]
        path = 'other.toml'
        optional = true
        ",
    );
    write_config_at(
        ".cargo/other.toml",
        "
        key2 = 2
        ",
    );

    let gctx = GlobalContextBuilder::new().build();
    assert_eq!(gctx.get::<i32>("key1").unwrap(), 1);
    assert_eq!(gctx.get::<i32>("key2").unwrap(), 2);
}

#[cargo_test]
fn optional_false_missing_file() {
    write_config_at(
        ".cargo/config.toml",
        "
        key1 = 1

        [[include]]
        path = 'missing.toml'
        optional = false
        ",
    );

    // Config values are lazy loaded and fail at first retrieval.
    // This ensures simple commands like `cargo --version` working even when
    let gctx = GlobalContextBuilder::new().build();
    assert_error(
        gctx.get::<bool>("foo").unwrap_err(),
        str![[r#"
could not load Cargo configuration

Caused by:
  failed to load config include `missing.toml` from `[ROOT]/.cargo/config.toml`

Caused by:
  failed to read configuration file `[ROOT]/.cargo/missing.toml`

Caused by:
  [NOT_FOUND]
"#]],
    );
}

#[cargo_test]
fn disallow_glob_syntax() {
    // Reserved for future extension
    write_config_toml("include = ['config-*.toml']");
    // Config values are lazy loaded and fail at first retrieval.
    // This ensures simple commands like `cargo --version` working even when
    let gctx = GlobalContextBuilder::new().build();
    assert_error(
        gctx.get::<bool>("foo").unwrap_err(),
        str![[r#"
could not load Cargo configuration

Caused by:
  expected a config include path without glob patterns, but found `config-*.toml` from `[ROOT]/.cargo/config.toml`
"#]],
    );
}

#[cargo_test]
fn disallow_template_syntax() {
    // Reserved for future extension
    write_config_toml("include = ['{workspace-root}/config.toml']");
    // Config values are lazy loaded and fail at first retrieval.
    // This ensures simple commands like `cargo --version` working even when
    let gctx = GlobalContextBuilder::new().build();
    assert_error(
        gctx.get::<bool>("foo").unwrap_err(),
        str![[r#"
could not load Cargo configuration

Caused by:
  expected a config include path without template braces, but found `{workspace-root}/config.toml` from `[ROOT]/.cargo/config.toml`
"#]],
    );
}
