//! Tests for the `cargo generate-lockfile` command.

use std::fs;

use crate::prelude::*;
use cargo_test_support::compare::assert_e2e;
use cargo_test_support::registry::{Package, RegistryBuilder};
use cargo_test_support::{ProjectBuilder, basic_manifest, paths, project, str};

#[cargo_test]
fn adding_and_removing_packages() {
    let p = project()
        .file("src/main.rs", "fn main() {}")
        .file("bar/Cargo.toml", &basic_manifest("bar", "0.0.1"))
        .file("bar/src/lib.rs", "")
        .build();

    p.cargo("generate-lockfile").run();

    let lock1 = p.read_lockfile();

    // add a dep
    p.change_file(
        "Cargo.toml",
        r#"
            [package]
            name = "foo"
            authors = []
            version = "0.0.1"

            [dependencies.bar]
            path = "bar"
        "#,
    );
    p.cargo("generate-lockfile").run();
    let lock2 = p.read_lockfile();
    assert_ne!(lock1, lock2);

    // change the dep
    p.change_file("bar/Cargo.toml", &basic_manifest("bar", "0.0.2"));
    p.cargo("generate-lockfile").run();
    let lock3 = p.read_lockfile();
    assert_ne!(lock1, lock3);
    assert_ne!(lock2, lock3);

    // remove the dep
    println!("lock4");
    p.change_file(
        "Cargo.toml",
        r#"
            [package]
            name = "foo"
            authors = []
            version = "0.0.1"
        "#,
    );
    p.cargo("generate-lockfile").run();
    let lock4 = p.read_lockfile();
    assert_eq!(lock1, lock4);
}

#[cargo_test]
fn no_index_update_sparse() {
    let _registry = RegistryBuilder::new().http_index().build();
    no_index_update(
        str![[r#"
[UPDATING] `dummy-registry` index
[LOCKING] 1 package to latest compatible version

"#]],
        str![[r#"
[LOCKING] 1 package to latest compatible version

"#]],
    );
}

#[cargo_test]
fn no_index_update_git() {
    no_index_update(
        str![[r#"
[UPDATING] `dummy-registry` index
[LOCKING] 1 package to latest compatible version

"#]],
        str![[r#"
[LOCKING] 1 package to latest compatible version

"#]],
    );
}

fn no_index_update(expected: impl IntoData, expected_unstable_option: impl IntoData) {
    Package::new("serde", "1.0.0").publish();

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

                [dependencies]
                serde = "1.0"
            "#,
        )
        .file("src/main.rs", "fn main() {}")
        .build();

    p.cargo("generate-lockfile")
        .with_stderr_data(expected)
        .run();

    p.cargo("generate-lockfile -Zno-index-update")
        .masquerade_as_nightly_cargo(&["no-index-update"])
        .with_stdout_data("")
        .with_stderr_data(expected_unstable_option)
        .run();
}

#[cargo_test]
fn preserve_metadata() {
    let p = project()
        .file("src/main.rs", "fn main() {}")
        .file("bar/Cargo.toml", &basic_manifest("bar", "0.0.1"))
        .file("bar/src/lib.rs", "")
        .build();

    p.cargo("generate-lockfile").run();

    let metadata = r#"
[metadata]
bar = "baz"
foo = "bar"
"#;
    let lock = p.read_lockfile();
    let data = lock + metadata;
    p.change_file("Cargo.lock", &data);

    // Build and make sure the metadata is still there
    p.cargo("build").run();
    let lock = p.read_lockfile();
    assert!(lock.contains(metadata.trim()), "{}", lock);

    // Update and make sure the metadata is still there
    p.cargo("update").run();
    let lock = p.read_lockfile();
    assert!(lock.contains(metadata.trim()), "{}", lock);
}

#[cargo_test]
fn preserve_line_endings_issue_2076() {
    let p = project()
        .file("src/main.rs", "fn main() {}")
        .file("bar/Cargo.toml", &basic_manifest("bar", "0.0.1"))
        .file("bar/src/lib.rs", "")
        .build();

    let lockfile = p.root().join("Cargo.lock");
    p.cargo("generate-lockfile").run();
    assert!(lockfile.is_file());
    p.cargo("generate-lockfile").run();

    let lock0 = p.read_lockfile();

    assert!(lock0.starts_with("# This file is automatically @generated by Cargo.\n# It is not intended for manual editing.\n"));

    let lock1 = lock0.replace("\n", "\r\n");
    p.change_file("Cargo.lock", &lock1);

    p.cargo("generate-lockfile").run();

    let lock2 = p.read_lockfile();

    assert!(lock2.starts_with("# This file is automatically @generated by Cargo.\r\n# It is not intended for manual editing.\r\n"));
    assert_eq!(lock1, lock2);
}

#[cargo_test]
fn cargo_update_generate_lockfile() {
    let p = project().file("src/main.rs", "fn main() {}").build();

    let lockfile = p.root().join("Cargo.lock");
    assert!(!lockfile.is_file());
    p.cargo("update").with_stderr_data("").run();
    assert!(lockfile.is_file());

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

    assert!(!lockfile.is_file());
    p.cargo("update").with_stderr_data("").run();
    assert!(lockfile.is_file());
}

#[cargo_test]
fn duplicate_entries_in_lockfile() {
    let _a = ProjectBuilder::new(paths::root().join("a"))
        .file(
            "Cargo.toml",
            r#"
            [package]
            name = "a"
            authors = []
            version = "0.0.1"

            [dependencies]
            common = {path="common"}
            "#,
        )
        .file("src/lib.rs", "")
        .build();

    let common_toml = &basic_manifest("common", "0.0.1");

    let _common_in_a = ProjectBuilder::new(paths::root().join("a/common"))
        .file("Cargo.toml", common_toml)
        .file("src/lib.rs", "")
        .build();

    let b = ProjectBuilder::new(paths::root().join("b"))
        .file(
            "Cargo.toml",
            r#"
            [package]
            name = "b"
            authors = []
            version = "0.0.1"
            edition = "2015"

            [dependencies]
            common = {path="common"}
            a = {path="../a"}
            "#,
        )
        .file("src/lib.rs", "")
        .build();

    let _common_in_b = ProjectBuilder::new(paths::root().join("b/common"))
        .file("Cargo.toml", common_toml)
        .file("src/lib.rs", "")
        .build();

    // should fail due to a duplicate package `common` in the lock file
    b.cargo("build")
        .with_status(101)
        .with_stderr_data(str![[r#"
[ERROR] package collision in the lockfile: packages common v0.0.1 ([ROOT]/a/common) and common v0.0.1 ([ROOT]/b/common) are different, but only one can be written to lockfile unambiguously

"#]])
        .run();
}

#[cargo_test]
fn generate_lockfile_holds_lock_and_offline() {
    Package::new("syn", "1.0.0").publish();

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

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

    p.cargo("generate-lockfile")
        .with_stderr_data(str![[r#"
[UPDATING] `dummy-registry` index
[LOCKING] 1 package to latest compatible version

"#]])
        .run();

    p.cargo("generate-lockfile --offline")
        .with_stderr_data(str![[r#"
[LOCKING] 1 package to latest compatible version

"#]])
        .run();
}

#[cargo_test]
fn publish_time_feature_gated() {
    let p = project()
        .file(
            "Cargo.toml",
            r#"
                [package]
                name = "foo"

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

    p.cargo("generate-lockfile --publish-time 2025-03-01T06:00:00Z")
        .masquerade_as_nightly_cargo(&["publish-time"])
        .with_status(101)
        .with_stderr_data(str![[r#"
[ERROR] the `--publish-time` flag is unstable, pass `-Z unstable-options` to enable it
See https://github.com/rust-lang/cargo/issues/5221 for more information about the `--publish-time` flag.

"#]])
        .run();
}

#[cargo_test]
fn publish_time() {
    Package::new("has_time", "2025.1.1")
        .pubtime("2025-01-01T06:00:00Z")
        .publish();
    Package::new("has_time", "2025.6.1")
        .pubtime("2025-06-01T06:00:00Z")
        .publish();
    Package::new("no_time", "2025.1.1").publish();
    Package::new("no_time", "2025.6.1").publish();

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

                [dependencies]
                has_time = "2025.0"
                no_time = "2025.0"
            "#,
        )
        .file("src/lib.rs", "")
        .build();

    p.cargo("generate-lockfile --publish-time 2025-03-01T06:00:00Z -Zunstable-options")
        .masquerade_as_nightly_cargo(&["publish-time"])
        .with_stderr_data(str![[r#"
[UPDATING] `dummy-registry` index
[LOCKING] 2 packages to latest compatible versions as of 2025-03-01T06:00:00Z
[ADDING] has_time v2025.1.1 (available: v2025.6.1)

"#]])
        .run();

    let lock = p.read_lockfile();
    assert_e2e().eq(
        lock,
        str![[r##"
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4

[[package]]
name = "foo"
version = "0.0.0"
dependencies = [
 "has_time",
 "no_time",
]

[[package]]
name = "has_time"
version = "2025.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "105ca3acbc796da3e728ff310cafc6961cebc48d0106285edb8341015b5cc2d7"

[[package]]
name = "no_time"
version = "2025.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01e688c07975f1e85f526c033322273181a4d8fe97800543d813d0a0adc134e3"

"##]],
    );
}

#[cargo_test]
fn publish_time_no_candidates() {
    Package::new("has_time", "2025.6.1")
        .pubtime("2025-06-01T06:00:00Z")
        .publish();

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

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

    p.cargo("generate-lockfile --publish-time 2025-03-01T06:00:00Z -Zunstable-options")
        .masquerade_as_nightly_cargo(&["publish-time"])
        .with_status(101)
        .with_stderr_data(str![[r#"
[UPDATING] `dummy-registry` index
[ERROR] failed to select a version for the requirement `has_time = "^2025.0"`
  version 2025.6.1 is unavailable
location searched: `dummy-registry` index (which is replacing registry `crates-io`)
required by package `foo v0.0.0 ([ROOT]/foo)`

"#]])
        .run();
}

#[cargo_test]
fn publish_time_invalid() {
    Package::new("has_time", "2025.6.1")
        .pubtime("2025-06-01T06:00:00")
        .publish();

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

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

    p.cargo("generate-lockfile --publish-time 2025-03-01T06:00:00Z -Zunstable-options")
        .masquerade_as_nightly_cargo(&["publish-time"])
        .with_status(101)
        .with_stderr_data(str![[r#"
[UPDATING] `dummy-registry` index
[ERROR] failed to select a version for the requirement `has_time = "^2025.0"`
  version 2025.6.1's index entry is invalid
location searched: `dummy-registry` index (which is replacing registry `crates-io`)
required by package `foo v0.0.0 ([ROOT]/foo)`

"#]])
        .run();
}
