//! Tests for checking behavior of old cargos.
//!
//! These tests are ignored because it is intended to be run on a developer
//! system with a bunch of toolchains installed. This requires `rustup` to be
//! installed. It will iterate over installed toolchains, and run some tests
//! over each one, producing a report at the end. As of this writing, I have
//! tested 1.0 to 1.51. Run this with:
//!
//! ```console
//! cargo test --test testsuite -- old_cargos --no-capture --ignored
//! ```

use std::fs;

use crate::prelude::*;
use crate::utils::cargo_exe;
use cargo::CargoResult;
use cargo_test_support::registry::{self, Dependency, Package};
use cargo_test_support::{execs, paths, process, project, rustc_host, str};
use cargo_util::{ProcessBuilder, ProcessError};
use semver::Version;

fn tc_process(cmd: &str, toolchain: &str) -> ProcessBuilder {
    let mut p = if toolchain == "this" {
        if cmd == "cargo" {
            process(&cargo_exe())
        } else {
            process(cmd)
        }
    } else {
        let mut cmd = process(cmd);
        cmd.arg(format!("+{}", toolchain));
        cmd
    };
    // Reset PATH since `process` modifies it to remove rustup.
    p.env("PATH", std::env::var_os("PATH").unwrap());
    p
}

/// Returns a sorted list of all toolchains.
///
/// The returned value includes the parsed version, and the rustup toolchain
/// name as a string.
fn collect_all_toolchains() -> Vec<(Version, String)> {
    let rustc_version = |tc| {
        let mut cmd = tc_process("rustc", tc);
        cmd.arg("-V");
        let output = cmd.exec_with_output().expect("rustc installed");
        let version = std::str::from_utf8(&output.stdout).unwrap();
        let parts: Vec<_> = version.split_whitespace().collect();
        assert_eq!(parts[0], "rustc");
        assert!(parts[1].starts_with("1."));
        Version::parse(parts[1]).expect("valid version")
    };

    // Provide a way to override the list.
    if let Ok(tcs) = std::env::var("OLD_CARGO") {
        return tcs
            .split(',')
            .map(|tc| (rustc_version(tc), tc.to_string()))
            .collect();
    }

    let host = rustc_host();
    // I tend to have lots of toolchains installed, but I don't want to test
    // all of them (like dated nightlies, or toolchains for non-host targets).
    let valid_names = &[
        format!("stable-{}", host),
        format!("beta-{}", host),
        format!("nightly-{}", host),
    ];

    let output = ProcessBuilder::new("rustup")
        .args(&["toolchain", "list"])
        .exec_with_output()
        .expect("rustup should be installed");
    let stdout = std::str::from_utf8(&output.stdout).unwrap();
    let mut toolchains: Vec<_> = stdout
        .lines()
        .map(|line| {
            // Some lines say things like (default), just get the version.
            line.split_whitespace().next().expect("non-empty line")
        })
        .filter(|line| {
            line.ends_with(&host)
                && (line.starts_with("1.") || valid_names.iter().any(|name| name == line))
        })
        .map(|line| (rustc_version(line), line.to_string()))
        .collect();

    toolchains.sort_by(|a, b| a.0.cmp(&b.0));
    toolchains
}

/// Returns whether the default toolchain is the stable version.
fn default_toolchain_is_stable() -> bool {
    let default = tc_process("rustc", "this").arg("-V").exec_with_output();
    let stable = tc_process("rustc", "stable").arg("-V").exec_with_output();
    match (default, stable) {
        (Ok(d), Ok(s)) => d.stdout == s.stdout,
        _ => false,
    }
}

// This is a test for exercising the behavior of older versions of cargo with
// the new feature syntax.
//
// The test involves a few dependencies with different feature requirements:
//
// * `bar` 1.0.0 is the base version that does not use the new syntax.
// * `bar` 1.0.1 has a feature with the new syntax, but the feature is unused.
//   The optional dependency `new-baz-dep` should not be activated.
// * `bar` 1.0.2 has a dependency on `baz` that *requires* the new feature
//   syntax.
#[ignore = "must be run manually, requires old cargo installations"]
#[cargo_test]
fn new_features() {
    let registry = registry::init();
    if std::process::Command::new("rustup").output().is_err() {
        panic!("old_cargos requires rustup to be installed");
    }
    Package::new("new-baz-dep", "1.0.0").publish();

    Package::new("baz", "1.0.0").publish();
    let baz101_cksum = Package::new("baz", "1.0.1")
        .add_dep(Dependency::new("new-baz-dep", "1.0").optional(true))
        .feature("new-feat", &["dep:new-baz-dep"])
        .publish();

    let bar100_cksum = Package::new("bar", "1.0.0")
        .add_dep(Dependency::new("baz", "1.0").optional(true))
        .feature("feat", &["baz"])
        .publish();
    let bar101_cksum = Package::new("bar", "1.0.1")
        .add_dep(Dependency::new("baz", "1.0").optional(true))
        .feature("feat", &["dep:baz"])
        .publish();
    let bar102_cksum = Package::new("bar", "1.0.2")
        .add_dep(Dependency::new("baz", "1.0").enable_features(&["new-feat"]))
        .publish();

    let p = project()
        .file(
            "Cargo.toml",
            r#"
                [package]
                name = "foo"
                version = "0.1.0"

                [dependencies]
                bar = "1.0"
            "#,
        )
        .file("src/lib.rs", "")
        .build();

    let lock_bar_to = |toolchain_version: &Version, bar_version| {
        let lock = if toolchain_version < &Version::new(1, 12, 0) {
            let url = registry.index_url();
            match bar_version {
                100 => format!(
                    r#"
                        [root]
                        name = "foo"
                        version = "0.1.0"
                        dependencies = [
                         "bar 1.0.0 (registry+{url})",
                        ]

                        [[package]]
                        name = "bar"
                        version = "1.0.0"
                        source = "registry+{url}"
                    "#,
                    url = url
                ),
                101 => format!(
                    r#"
                        [root]
                        name = "foo"
                        version = "0.1.0"
                        dependencies = [
                         "bar 1.0.1 (registry+{url})",
                        ]

                        [[package]]
                        name = "bar"
                        version = "1.0.1"
                        source = "registry+{url}"
                    "#,
                    url = url
                ),
                102 => format!(
                    r#"
                        [root]
                        name = "foo"
                        version = "0.1.0"
                        dependencies = [
                         "bar 1.0.2 (registry+{url})",
                        ]

                        [[package]]
                        name = "bar"
                        version = "1.0.2"
                        source = "registry+{url}"
                        dependencies = [
                         "baz 1.0.1 (registry+{url})",
                        ]

                        [[package]]
                        name = "baz"
                        version = "1.0.1"
                        source = "registry+{url}"
                    "#,
                    url = url
                ),
                _ => panic!("unexpected version"),
            }
        } else {
            match bar_version {
                100 => format!(
                    r#"
                        [root]
                        name = "foo"
                        version = "0.1.0"
                        dependencies = [
                         "bar 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
                        ]

                        [[package]]
                        name = "bar"
                        version = "1.0.0"
                        source = "registry+https://github.com/rust-lang/crates.io-index"

                        [metadata]
                        "checksum bar 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "{}"
                    "#,
                    bar100_cksum
                ),
                101 => format!(
                    r#"
                        [root]
                        name = "foo"
                        version = "0.1.0"
                        dependencies = [
                         "bar 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
                        ]

                        [[package]]
                        name = "bar"
                        version = "1.0.1"
                        source = "registry+https://github.com/rust-lang/crates.io-index"

                        [metadata]
                        "checksum bar 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "{}"
                    "#,
                    bar101_cksum
                ),
                102 => format!(
                    r#"
                        [root]
                        name = "foo"
                        version = "0.1.0"
                        dependencies = [
                         "bar 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
                        ]

                        [[package]]
                        name = "bar"
                        version = "1.0.2"
                        source = "registry+https://github.com/rust-lang/crates.io-index"
                        dependencies = [
                         "baz 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
                        ]

                        [[package]]
                        name = "baz"
                        version = "1.0.1"
                        source = "registry+https://github.com/rust-lang/crates.io-index"

                        [metadata]
                        "checksum bar 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "{bar102_cksum}"
                        "checksum baz 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "{baz101_cksum}"
                    "#,
                    bar102_cksum = bar102_cksum,
                    baz101_cksum = baz101_cksum
                ),
                _ => panic!("unexpected version"),
            }
        };
        p.change_file("Cargo.lock", &lock);
    };

    let toolchains = collect_all_toolchains();

    let config_path = paths::home().join(".cargo/config");
    let lock_path = p.root().join("Cargo.lock");

    struct ToolchainBehavior {
        bar: Option<Version>,
        baz: Option<Version>,
        new_baz_dep: Option<Version>,
    }

    // Collect errors to print at the end. One entry per toolchain, a list of
    // strings to print.
    let mut unexpected_results: Vec<Vec<String>> = Vec::new();

    for (version, toolchain) in &toolchains {
        if version >= &Version::new(1, 15, 0) && version < &Version::new(1, 18, 0) {
            // These versions do not stay within the sandbox, and chokes on
            // Cargo's own `Cargo.toml`.
            continue;
        }
        let mut tc_result = Vec::new();
        // Write a config appropriate for this version.
        if version < &Version::new(1, 12, 0) {
            fs::write(
                &config_path,
                format!(
                    r#"
                        [registry]
                        index = "{}"
                    "#,
                    registry.index_url()
                ),
            )
            .unwrap();
        } else {
            fs::write(
                &config_path,
                format!(
                    "
                        [source.crates-io]
                        registry = 'https://wut'  # only needed by 1.12
                        replace-with = 'dummy-registry'

                        [source.dummy-registry]
                        registry = '{}'
                    ",
                    registry.index_url()
                ),
            )
            .unwrap();
        }

        // Fetches the version of a package in the lock file.
        let pkg_version = |pkg| -> Option<Version> {
            let output = tc_process("cargo", toolchain)
                .args(&["pkgid", pkg])
                .cwd(p.root())
                .exec_with_output()
                .ok()?;
            let stdout = std::str::from_utf8(&output.stdout).unwrap();
            let version = stdout
                .trim()
                .rsplitn(2, ['@', ':'])
                .next()
                .expect("version after colon");
            Some(Version::parse(version).expect("parseable version"))
        };

        // Runs `cargo build` and returns the versions selected in the lock.
        let run_cargo = || -> CargoResult<ToolchainBehavior> {
            match tc_process("cargo", toolchain)
                .args(&["build", "--verbose"])
                .cwd(p.root())
                .exec_with_output()
            {
                Ok(_output) => {
                    eprintln!("{} ok", toolchain);
                    let bar = pkg_version("bar");
                    let baz = pkg_version("baz");
                    let new_baz_dep = pkg_version("new-baz-dep");
                    Ok(ToolchainBehavior {
                        bar,
                        baz,
                        new_baz_dep,
                    })
                }
                Err(e) => {
                    eprintln!("{} err {}", toolchain, e);
                    Err(e)
                }
            }
        };

        macro_rules! check_lock {
            ($tc_result:ident, $pkg:expr, $which:expr, $actual:expr, None) => {
                check_lock!(= $tc_result, $pkg, $which, $actual, None);
            };
            ($tc_result:ident, $pkg:expr, $which:expr, $actual:expr, $expected:expr) => {
                check_lock!(= $tc_result, $pkg, $which, $actual, Some(Version::parse($expected).unwrap()));
            };
            (= $tc_result:ident, $pkg:expr, $which:expr, $actual:expr, $expected:expr) => {
                let exp: Option<Version> = $expected;
                if $actual != $expected {
                    $tc_result.push(format!(
                        "{} for {} saw {:?} but expected {:?}",
                        $which, $pkg, $actual, exp
                    ));
                }
            };
        }

        let check_err_contains = |tc_result: &mut Vec<_>, err: anyhow::Error, contents| {
            if let Some(ProcessError {
                stderr: Some(stderr),
                ..
            }) = err.downcast_ref::<ProcessError>()
            {
                let stderr = std::str::from_utf8(stderr).unwrap();
                if !stderr.contains(contents) {
                    tc_result.push(format!(
                        "{} expected to see error contents:\n{}\nbut saw:\n{}",
                        toolchain, contents, stderr
                    ));
                }
            } else {
                panic!("{} unexpected error {}", toolchain, err);
            }
        };

        // Unlocked behavior.
        let which = "unlocked";
        lock_path.rm_rf();
        p.build_dir().rm_rf();
        match run_cargo() {
            Ok(behavior) => {
                if version < &Version::new(1, 51, 0) {
                    check_lock!(tc_result, "bar", which, behavior.bar, "1.0.2");
                    check_lock!(tc_result, "baz", which, behavior.baz, "1.0.1");
                    check_lock!(tc_result, "new-baz-dep", which, behavior.new_baz_dep, None);
                } else if version >= &Version::new(1, 51, 0) && version <= &Version::new(1, 59, 0) {
                    check_lock!(tc_result, "bar", which, behavior.bar, "1.0.0");
                    check_lock!(tc_result, "baz", which, behavior.baz, None);
                    check_lock!(tc_result, "new-baz-dep", which, behavior.new_baz_dep, None);
                }
                // Starting with 1.60, namespaced-features has been stabilized.
                else {
                    check_lock!(tc_result, "bar", which, behavior.bar, "1.0.2");
                    check_lock!(tc_result, "baz", which, behavior.baz, "1.0.1");
                    check_lock!(
                        tc_result,
                        "new-baz-dep",
                        which,
                        behavior.new_baz_dep,
                        "1.0.0"
                    );
                }
            }
            Err(e) => {
                if version < &Version::new(1, 49, 0) {
                    // Old versions don't like the dep: syntax.
                    check_err_contains(
                        &mut tc_result,
                        e,
                        "which is neither a dependency nor another feature",
                    );
                } else if version >= &Version::new(1, 49, 0) && version < &Version::new(1, 51, 0) {
                    check_err_contains(
                        &mut tc_result,
                        e,
                        "requires the `-Z namespaced-features` flag",
                    );
                } else {
                    tc_result.push(format!("unlocked build failed: {}", e));
                }
            }
        }

        let which = "locked bar 1.0.0";
        lock_bar_to(version, 100);
        match run_cargo() {
            Ok(behavior) => {
                check_lock!(tc_result, "bar", which, behavior.bar, "1.0.0");
                check_lock!(tc_result, "baz", which, behavior.baz, None);
                check_lock!(tc_result, "new-baz-dep", which, behavior.new_baz_dep, None);
            }
            Err(e) => {
                tc_result.push(format!("bar 1.0.0 locked build failed: {}", e));
            }
        }

        let which = "locked bar 1.0.1";
        lock_bar_to(version, 101);
        match run_cargo() {
            Ok(behavior) => {
                check_lock!(tc_result, "bar", which, behavior.bar, "1.0.1");
                check_lock!(tc_result, "baz", which, behavior.baz, None);
                check_lock!(tc_result, "new-baz-dep", which, behavior.new_baz_dep, None);
            }
            Err(e) => {
                if version < &Version::new(1, 49, 0) {
                    // Old versions don't like the dep: syntax.
                    check_err_contains(
                        &mut tc_result,
                        e,
                        "which is neither a dependency nor another feature",
                    );
                } else if version >= &Version::new(1, 49, 0) && version < &Version::new(1, 51, 0) {
                    check_err_contains(
                        &mut tc_result,
                        e,
                        "requires the `-Z namespaced-features` flag",
                    );
                } else {
                    // When version >= 1.51 and <= 1.59,
                    // 1.0.1 can't be used without -Znamespaced-features
                    // It gets filtered out of the index.
                    check_err_contains(
                        &mut tc_result,
                        e,
                        "candidate versions found which didn't match: 1.0.2, 1.0.0",
                    );
                }
            }
        }

        let which = "locked bar 1.0.2";
        lock_bar_to(version, 102);
        match run_cargo() {
            Ok(behavior) => {
                if version <= &Version::new(1, 59, 0) {
                    check_lock!(tc_result, "bar", which, behavior.bar, "1.0.2");
                    check_lock!(tc_result, "baz", which, behavior.baz, "1.0.1");
                    check_lock!(tc_result, "new-baz-dep", which, behavior.new_baz_dep, None);
                }
                // Starting with 1.60, namespaced-features has been stabilized.
                else {
                    check_lock!(tc_result, "bar", which, behavior.bar, "1.0.2");
                    check_lock!(tc_result, "baz", which, behavior.baz, "1.0.1");
                    check_lock!(
                        tc_result,
                        "new-baz-dep",
                        which,
                        behavior.new_baz_dep,
                        "1.0.0"
                    );
                }
            }
            Err(e) => {
                if version < &Version::new(1, 49, 0) {
                    // Old versions don't like the dep: syntax.
                    check_err_contains(
                        &mut tc_result,
                        e,
                        "which is neither a dependency nor another feature",
                    );
                } else if version >= &Version::new(1, 49, 0) && version < &Version::new(1, 51, 0) {
                    check_err_contains(
                        &mut tc_result,
                        e,
                        "requires the `-Z namespaced-features` flag",
                    );
                } else {
                    // When version >= 1.51 and <= 1.59,
                    // baz can't lock to 1.0.1, it requires -Znamespaced-features
                    check_err_contains(
                        &mut tc_result,
                        e,
                        "candidate versions found which didn't match: 1.0.0",
                    );
                }
            }
        }

        unexpected_results.push(tc_result);
    }

    // Generate a report.
    let mut has_err = false;
    for ((tc_vers, tc_name), errs) in toolchains.iter().zip(unexpected_results) {
        if errs.is_empty() {
            continue;
        }
        eprintln!("error: toolchain {} (version {}):", tc_name, tc_vers);
        for err in errs {
            eprintln!("  {}", err);
        }
        has_err = true;
    }
    if has_err {
        panic!("at least one toolchain did not run as expected");
    }
}

#[cargo_test]
#[ignore = "must be run manually, requires old cargo installations"]
fn index_cache_rebuild() {
    // Checks that the index cache gets rebuilt.
    //
    // 1.48 will not cache entries with features with the same name as a
    // dependency. If the cache does not get rebuilt, then running with
    // `-Znamespaced-features` would prevent the new cargo from seeing those
    // entries. The index cache version was changed to prevent this from
    // happening, and switching between versions should work correctly
    // (although it will thrash the cash, that's better than not working
    // correctly.
    let registry = registry::init();
    Package::new("baz", "1.0.0").publish();
    Package::new("bar", "1.0.0").publish();
    Package::new("bar", "1.0.1")
        .add_dep(Dependency::new("baz", "1.0").optional(true))
        .feature("baz", &["dep:baz"])
        .publish();

    let p = project()
        .file(
            "Cargo.toml",
            r#"
                [package]
                name = "foo"
                version = "0.1.0"

                [dependencies]
                bar = "1.0"
            "#,
        )
        .file("src/lib.rs", "")
        .file(
            ".cargo/config.toml",
            &format!(
                r#"
                    [source.crates-io]
                    replace-with = 'dummy-registry'

                    [source.dummy-registry]
                    registry = '{}'
                "#,
                registry.index_url()
            ),
        )
        .build();

    // This version of Cargo errors on index entries that have overlapping
    // feature names, so 1.0.1 will be missing.
    execs()
        .with_process_builder(tc_process("cargo", "1.48.0"))
        .arg("check")
        .cwd(p.root())
        .with_stderr_data(str![[r#"
[UPDATING] `[ROOT]/registry` index
[DOWNLOADING] crates ...
[DOWNLOADED] bar v1.0.0 (registry `[ROOT]/registry`)
[CHECKING] bar v1.0.0
[CHECKING] foo v0.1.0 ([ROOT]/foo)
[FINISHED] dev [unoptimized + debuginfo] target(s) in [ELAPSED]s

"#]])
        .run();

    fs::remove_file(p.root().join("Cargo.lock")).unwrap();

    // This should rebuild the cache and use 1.0.1.
    p.cargo("check")
        .with_stderr_data(str![[r#"
[WARNING] no edition set: defaulting to the 2015 edition while the latest is [..]
[UPDATING] `dummy-registry` index
[LOCKING] 2 packages to latest compatible versions
[DOWNLOADING] crates ...
[DOWNLOADED] bar v1.0.1 (registry `dummy-registry`)
[CHECKING] bar v1.0.1
[CHECKING] foo v0.1.0 ([ROOT]/foo)
[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s

"#]])
        .run();

    fs::remove_file(p.root().join("Cargo.lock")).unwrap();

    // Verify 1.48 can still resolve, and is at 1.0.0.
    execs()
        .with_process_builder(tc_process("cargo", "1.48.0"))
        .arg("tree")
        .cwd(p.root())
        .with_stdout_data(str![[r#"
foo v0.1.0 ([ROOT]/foo)
└── bar v1.0.0

"#]])
        .run();
}

#[cargo_test]
#[ignore = "must be run manually, requires old cargo installations"]
fn avoids_split_debuginfo_collision() {
    // Test needs two different toolchains.
    // If the default toolchain is stable, then it won't work.
    if default_toolchain_is_stable() {
        return;
    }
    // Checks for a bug where .o files were being incorrectly shared between
    // different toolchains using incremental and split-debuginfo on macOS.
    let p = project()
        .file(
            "Cargo.toml",
            r#"
                [package]
                name = "foo"
                version = "0.1.0"

                [profile.dev]
                split-debuginfo = "unpacked"
            "#,
        )
        .file("src/main.rs", "fn main() {}")
        .build();

    execs()
        .with_process_builder(tc_process("cargo", "stable"))
        .arg("build")
        .env("CARGO_INCREMENTAL", "1")
        .cwd(p.root())
        .with_stderr_data(str![[r#"
[COMPILING] foo v0.1.0 ([ROOT]/foo)
[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s

"#]])
        .run();

    p.cargo("build")
        .env("CARGO_INCREMENTAL", "1")
        .with_stderr_data(str![[r#"
[WARNING] no edition set: defaulting to the 2015 edition while the latest is [..]
[COMPILING] foo v0.1.0 ([ROOT]/foo)
[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s

"#]])
        .run();

    execs()
        .with_process_builder(tc_process("cargo", "stable"))
        .arg("build")
        .env("CARGO_INCREMENTAL", "1")
        .cwd(p.root())
        .with_stderr_data(str![[r#"
[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s

"#]])
        .run();
}
