//! Tests for cargo-sbom precursor files.

use std::path::PathBuf;

use crate::prelude::*;
use cargo_test_support::basic_bin_manifest;
use cargo_test_support::cargo_test;
use cargo_test_support::compare::assert_e2e;
use cargo_test_support::project;
use cargo_test_support::registry::Package;
use snapbox::IntoData;

const SBOM_FILE_EXTENSION: &str = ".cargo-sbom.json";

fn append_sbom_suffix(link: &PathBuf) -> PathBuf {
    let mut link_buf = link.clone().into_os_string();
    link_buf.push(SBOM_FILE_EXTENSION);
    PathBuf::from(link_buf)
}

#[cargo_test]
fn warn_without_passing_unstable_flag() {
    let p = project()
        .file("Cargo.toml", &basic_bin_manifest("foo"))
        .file(
            "src/main.rs",
            r#"fn main() {
    eprintln!("{:?}", option_env!("CARGO_SBOM_PATH"));
}"#,
        )
        .build();

    p.cargo("run")
        .env("CARGO_BUILD_SBOM", "true")
        .masquerade_as_nightly_cargo(&["sbom"])
        .with_stderr_data(cargo_test_support::str![[r#"
[WARNING] ignoring 'sbom' config, pass `-Zsbom` to enable it
[COMPILING] foo v0.5.0 ([ROOT]/foo)
[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s
[RUNNING] `target/debug/foo[EXE]`
None

"#]])
        .run();

    let file = append_sbom_suffix(&p.bin("foo"));
    assert!(!file.exists());
}

#[cargo_test]
fn simple() {
    let p = project()
        .file("Cargo.toml", &basic_bin_manifest("foo"))
        .file("src/main.rs", r#"fn main() {}"#)
        .build();

    p.cargo("build -Zsbom")
        .env("CARGO_BUILD_SBOM", "true")
        .masquerade_as_nightly_cargo(&["sbom"])
        .run();

    let file = append_sbom_suffix(&p.bin("foo"));
    let output = std::fs::read_to_string(file).unwrap();
    // The expected test does contain the "rustc" section
    // but other tests omit them for brevity.
    assert_e2e().eq(
        output,
        cargo_test_support::str![[r#"
{
  "crates": [
    {
      "dependencies": [],
      "features": [],
      "id": "path+[ROOTURL]/foo#0.5.0",
      "kind": [
        "bin"
      ]
    }
  ],
  "root": 0,
  "rustc": {
    "commit_hash": "{...}",
    "host": "[HOST_TARGET]",
    "verbose_version": "{...}",
    "version": "{...}",
    "workspace_wrapper": null,
    "wrapper": null
  },
  "target": "[HOST_TARGET]",
  "version": 1
}
"#]]
        .is_json(),
    );
}

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

                [lib]
                crate-type = ["dylib", "rlib"]
            "#,
        )
        .file("src/main.rs", r#"fn main() { let _i = foo::give_five(); }"#)
        .file("src/lib.rs", r#"pub fn give_five() -> i32 { 5 }"#)
        .build();

    p.cargo("build -Zsbom")
        .env("CARGO_BUILD_SBOM", "true")
        .masquerade_as_nightly_cargo(&["sbom"])
        .run();

    assert_eq!(
        3,
        p.glob(p.target_debug_dir().join("*.cargo-sbom.json"))
            .count()
    );

    let sbom_path = append_sbom_suffix(&p.dylib("foo"));
    assert!(sbom_path.is_file());

    let output = std::fs::read_to_string(sbom_path).unwrap();
    assert_e2e().eq(
        output,
        cargo_test_support::str![[r#"
{
  "crates": [
    {
      "dependencies": [],
      "features": [],
      "id": "path+[ROOTURL]/foo#1.2.3",
      "kind": [
        "dylib",
        "rlib"
      ]
    }
  ],
  "root": 0,
  "rustc": "{...}",
  "target": "[HOST_TARGET]",
  "version": 1
}
"#]]
        .is_json(),
    );
}

#[cargo_test]
fn with_simple_build_script() {
    let p = project()
        .file(
            "Cargo.toml",
            r#"
                [package]
                name = "foo"
                version = "0.0.1"
                build = "build.rs"
            "#,
        )
        .file("src/main.rs", "#[cfg(foo)] fn main() {}")
        .file(
            "build.rs",
            r#"fn main() {
                println!("cargo::rustc-check-cfg=cfg(foo)");
                println!("cargo::rustc-cfg=foo");
            }"#,
        )
        .build();

    p.cargo("build -Zsbom")
        .env("CARGO_BUILD_SBOM", "true")
        .masquerade_as_nightly_cargo(&["sbom"])
        .run();

    let path = append_sbom_suffix(&p.bin("foo"));
    assert!(path.is_file());

    let output = std::fs::read_to_string(path).unwrap();
    assert_e2e().eq(
        output,
        cargo_test_support::str![[r#"
{
  "crates": [
    {
      "dependencies": [
        {
          "index": 1,
          "kind": "build"
        }
      ],
      "features": [],
      "id": "path+[ROOTURL]/foo#0.0.1",
      "kind": [
        "bin"
      ]
    },
    {
      "dependencies": [],
      "features": [],
      "id": "path+[ROOTURL]/foo#0.0.1",
      "kind": [
        "custom-build"
      ]
    }
  ],
  "root": 0,
  "rustc": "{...}",
  "target": "[HOST_TARGET]",
  "version": 1
}
"#]]
        .is_json(),
    );
}

#[cargo_test]
fn with_build_dependencies() {
    Package::new("baz", "0.1.0").publish();
    Package::new("bar", "0.1.0")
        .build_dep("baz", "0.1.0")
        .file(
            "Cargo.toml",
            r#"
                [package]
                name = "bar"
                version = "0.1.0"
                build = "build.rs"

                [build-dependencies]
                baz = "0.1.0"
            "#,
        )
        .file("src/lib.rs", "pub fn bar() -> i32 { 2 }")
        .file(
            "build.rs",
            r#"fn main() {
                println!("cargo::rustc-check-cfg=cfg(foo)");
                println!("cargo::rustc-cfg=foo");
            }"#,
        )
        .publish();

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

                [dependencies]
                bar = "0.1.0"
            "#,
        )
        .file("src/main.rs", "fn main() { let _i = bar::bar(); }")
        .build();

    p.cargo("build -Zsbom")
        .env("CARGO_BUILD_SBOM", "true")
        .masquerade_as_nightly_cargo(&["sbom"])
        .run();

    let path = append_sbom_suffix(&p.bin("foo"));
    let output = std::fs::read_to_string(path).unwrap();
    assert_e2e().eq(
        output,
        cargo_test_support::str![[r#"
{
  "crates": [
    {
      "dependencies": [
        {
          "index": 1,
          "kind": "build"
        }
      ],
      "features": [],
      "id": "registry+https://github.com/rust-lang/crates.io-index#bar@0.1.0",
      "kind": [
        "lib"
      ]
    },
    {
      "dependencies": [
        {
          "index": 2,
          "kind": "normal"
        }
      ],
      "features": [],
      "id": "registry+https://github.com/rust-lang/crates.io-index#bar@0.1.0",
      "kind": [
        "custom-build"
      ]
    },
    {
      "dependencies": [],
      "features": [],
      "id": "registry+https://github.com/rust-lang/crates.io-index#baz@0.1.0",
      "kind": [
        "lib"
      ]
    },
    {
      "dependencies": [
        {
          "index": 0,
          "kind": "normal"
        }
      ],
      "features": [],
      "id": "path+[ROOTURL]/foo#0.0.1",
      "kind": [
        "bin"
      ]
    }
  ],
  "root": 3,
  "rustc": "{...}",
  "target": "[HOST_TARGET]",
  "version": 1
}
"#]]
        .is_json(),
    );
}

#[cargo_test]
fn crate_uses_different_features_for_build_and_normal_dependencies() {
    let p = project()
        .file(
            "Cargo.toml",
            r#"
                [package]
                name = "a"
                version = "0.1.0"
                edition = "2021"

                [dependencies]
                b = { path = "b/", features = ["f1"] }

                [build-dependencies]
                b = { path = "b/", features = ["f2"] }
            "#,
        )
        .file(
            "src/main.rs",
            r#"
                fn main() { b::f1(); }
            "#,
        )
        .file(
            "build.rs",
            r#"
                fn main() { b::f2(); }
            "#,
        )
        .file(
            "b/Cargo.toml",
            r#"
                [package]
                name = "b"
                version = "0.0.1"
                edition = "2021"

                [features]
                f1 = []
                f2 = []
            "#,
        )
        .file(
            "b/src/lib.rs",
            r#"
                #[cfg(feature = "f1")]
                pub fn f1() {}

                #[cfg(feature = "f2")]
                pub fn f2() {}
            "#,
        )
        .build();

    p.cargo("build -Zsbom")
        .env("CARGO_BUILD_SBOM", "true")
        .masquerade_as_nightly_cargo(&["sbom"])
        .run();

    let path = append_sbom_suffix(&p.bin("a"));
    assert!(path.is_file());
    let output = std::fs::read_to_string(path).unwrap();
    assert_e2e().eq(
        output,
        cargo_test_support::str![[r#"
{
  "crates": [
    {
      "dependencies": [
        {
          "index": 1,
          "kind": "build"
        },
        {
          "index": 3,
          "kind": "normal"
        }
      ],
      "features": [],
      "id": "path+[ROOTURL]/foo#a@0.1.0",
      "kind": [
        "bin"
      ]
    },
    {
      "dependencies": [
        {
          "index": 2,
          "kind": "normal"
        }
      ],
      "features": [],
      "id": "path+[ROOTURL]/foo#a@0.1.0",
      "kind": [
        "custom-build"
      ]
    },
    {
      "dependencies": [],
      "features": [
        "f2"
      ],
      "id": "path+[ROOTURL]/foo/b#0.0.1",
      "kind": [
        "lib"
      ]
    },
    {
      "dependencies": [],
      "features": [
        "f1"
      ],
      "id": "path+[ROOTURL]/foo/b#0.0.1",
      "kind": [
        "lib"
      ]
    }
  ],
  "root": 0,
  "rustc": "{...}",
  "target": "[HOST_TARGET]",
  "version": 1
}
"#]]
        .is_json(),
    );
}

#[cargo_test]
fn artifact_dep() {
    Package::new("bar", "0.5.0")
        .file("src/main.rs", "fn main() {}")
        .file("Cargo.toml", &basic_bin_manifest("bar"))
        .publish();

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

                [lib]
                crate-type = ["dylib"] 

                [dependencies]
                bar = { version = "0.5.0", artifact = "bin" }

                [build-dependencies]
                bar = { version = "0.5.0", artifact = "bin" }
            "#,
        )
        .file("src/lib.rs", "")
        .file("build.rs", r#"
                fn main() {
                    let bar: std::path::PathBuf = std::env::var("CARGO_BIN_FILE_BAR").expect("CARGO_BIN_FILE_BAR").into();
                    assert!(&bar.is_file());
                }"#)
        .build();
    p.cargo("build -Z bindeps -Z sbom")
        .env("CARGO_BUILD_SBOM", "true")
        .masquerade_as_nightly_cargo(&["bindeps", "sbom"])
        .run();

    let output = std::fs::read_to_string(append_sbom_suffix(&p.dylib("foo"))).unwrap();
    assert_e2e().eq(
        output,
        cargo_test_support::str![[r#"
{
  "crates": [
    {
      "dependencies": [],
      "features": [],
      "id": "registry+https://github.com/rust-lang/crates.io-index#bar@0.5.0",
      "kind": [
        "bin"
      ]
    },
    {
      "dependencies": [
        {
          "index": 0,
          "kind": "normal"
        },
        {
          "index": 0,
          "kind": "build"
        },
        {
          "index": 2,
          "kind": "build"
        }
      ],
      "features": [],
      "id": "path+[ROOTURL]/foo#0.0.0",
      "kind": [
        "dylib"
      ]
    },
    {
      "dependencies": [],
      "features": [],
      "id": "path+[ROOTURL]/foo#0.0.0",
      "kind": [
        "custom-build"
      ]
    }
  ],
  "root": 1,
  "rustc": "{...}",
  "target": "[HOST_TARGET]",
  "version": 1
}
"#]]
        .is_json(),
    );
}

#[cargo_test]
fn proc_macro() {
    Package::new("noop", "0.0.1")
        .file(
            "Cargo.toml",
            r#"
            [package]
            name = "noop"
            version = "0.0.1"
            edition = "2021"

            [lib]
            proc-macro = true
        "#,
        )
        .file(
            "src/lib.rs",
            r#"
            extern crate proc_macro;
            use proc_macro::TokenStream;

            #[proc_macro_derive(Noop)]
            pub fn noop(_input: TokenStream) -> TokenStream {
                "".parse().unwrap()
            }
        "#,
        )
        .publish();

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

                [dependencies]
                noop = "0.0.1"
            "#,
        )
        .file(
            "src/main.rs",
            r#"
                #[macro_use]
                extern crate noop;

                #[derive(Noop)]
                struct X;

                fn main() {}
            "#,
        )
        .build();

    p.cargo("build -Z sbom")
        .env("CARGO_BUILD_SBOM", "true")
        .masquerade_as_nightly_cargo(&["sbom"])
        .run();

    let output = std::fs::read_to_string(append_sbom_suffix(&p.bin("foo"))).unwrap();
    assert_e2e().eq(
        output,
        cargo_test_support::str![[r#"
{
  "crates": [
    {
      "dependencies": [
        {
          "index": 1,
          "kind": "build"
        }
      ],
      "features": [],
      "id": "path+[ROOTURL]/foo#0.0.1",
      "kind": [
        "bin"
      ]
    },
    {
      "dependencies": [],
      "features": [],
      "id": "registry+https://github.com/rust-lang/crates.io-index#noop@0.0.1",
      "kind": [
        "proc-macro"
      ]
    }
  ],
  "root": 0,
  "rustc": "{...}",
  "target": "[HOST_TARGET]",
  "version": 1
}
"#]]
        .is_json(),
    );
}

#[cargo_test]
fn workspace_wrapper() {
    let wrapper = project()
        .at("wrapper")
        .file("Cargo.toml", &basic_bin_manifest("wrapper"))
        .file(
            "src/main.rs",
            r#"
            fn main() {
                let mut args = std::env::args().skip(1);
                if let Some(sbom) = std::env::var_os("CARGO_SBOM_PATH") {
                    for sbom in std::env::split_paths(&sbom) {
                        eprintln!("found sbom");
                        assert!(sbom.exists());
                    }
                }
                let status = std::process::Command::new(&args.next().unwrap())
                    .args(args).status().unwrap();
                std::process::exit(status.code().unwrap_or(1));
          }
          "#,
        )
        .build();
    wrapper.cargo("build").run();

    let p = project()
        .file("Cargo.toml", &basic_bin_manifest("foo"))
        .file("src/main.rs", r#"fn main() {}"#)
        .build();
    p.cargo("build -Zsbom")
        .env("CARGO_BUILD_SBOM", "true")
        .env("RUSTC_WRAPPER", wrapper.bin("wrapper"))
        .masquerade_as_nightly_cargo(&["sbom"])
        .with_stderr_data(cargo_test_support::str![[r#"
[COMPILING] foo v0.5.0 ([ROOT]/foo)
found sbom
[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s

"#]])
        .run();

    let file = append_sbom_suffix(&p.bin("foo"));
    let output = std::fs::read_to_string(file).unwrap();
    assert_e2e().eq(
        output,
        cargo_test_support::str![[r#"
{
  "crates": [
    {
      "dependencies": [],
      "features": [],
      "id": "path+[ROOTURL]/foo#0.5.0",
      "kind": [
        "bin"
      ]
    }
  ],
  "root": 0,
  "rustc": "{...}",
  "target": "[HOST_TARGET]",
  "version": 1
}
"#]]
        .is_json(),
    );
}
