{ lib
, buildPackages
, buildRustCrate
, callPackage
, releaseTools
, runCommand
, runCommandCC
, stdenv
, symlinkJoin
, writeTextFile
}:

let
  mkCrate = buildRustCrate: args: let
    p = {
      crateName = "nixtestcrate";
      version = "0.1.0";
      authors = [ "Test <test@example.com>" ];
    } // args;
  in buildRustCrate p;
  mkHostCrate = mkCrate buildRustCrate;

  mkCargoToml =
    { name, crateVersion ? "0.1.0", path ? "Cargo.toml" }:
      mkFile path ''
        [package]
        name = ${builtins.toJSON name}
        version = ${builtins.toJSON crateVersion}
      '';

  mkFile = destination: text: writeTextFile {
    name = "src";
    destination = "/${destination}";
    inherit text;
  };

  mkBin = name: mkFile name ''
    use std::env;
    fn main() {
      let name: String = env::args().nth(0).unwrap();
      println!("executed {}", name);
    }
  '';

  mkBinExtern = name: extern: mkFile name ''
    extern crate ${extern};
    fn main() {
      assert_eq!(${extern}::test(), 23);
    }
  '';

  mkTestFile = name: functionName: mkFile name ''
    #[cfg(test)]
    #[test]
    fn ${functionName}() {
      assert!(true);
    }
  '';
  mkTestFileWithMain = name: functionName: mkFile name ''
    #[cfg(test)]
    #[test]
    fn ${functionName}() {
      assert!(true);
    }

    fn main() {}
  '';


  mkLib = name: mkFile name "pub fn test() -> i32 { return 23; }";

  mkTest = crateArgs: let
    crate = mkHostCrate (builtins.removeAttrs crateArgs ["expectedTestOutput"]);
    hasTests = crateArgs.buildTests or false;
    expectedTestOutputs = crateArgs.expectedTestOutputs or null;
    binaries = map (v: lib.escapeShellArg v.name) (crateArgs.crateBin or []);
    isLib = crateArgs ? libName || crateArgs ? libPath;
    crateName = crateArgs.crateName or "nixtestcrate";
    libName = crateArgs.libName or crateName;

    libTestBinary = if !isLib then null else mkHostCrate {
      crateName = "run-test-${crateName}";
      dependencies = [ crate ];
      src = mkBinExtern "src/main.rs" libName;
    };

    in
      assert expectedTestOutputs != null -> hasTests;
      assert hasTests -> expectedTestOutputs != null;

      runCommand "run-buildRustCrate-${crateName}-test" {
        nativeBuildInputs = [ crate ];
      } (if !hasTests then ''
          ${lib.concatMapStringsSep "\n" (binary:
            # Can't actually run the binary when cross-compiling
            (lib.optionalString (stdenv.hostPlatform != stdenv.buildPlatform) "type ") + binary
          ) binaries}
          ${lib.optionalString isLib ''
              test -e ${crate}/lib/*.rlib || exit 1
              ${lib.optionalString (stdenv.hostPlatform != stdenv.buildPlatform) "test -x "} \
                ${libTestBinary}/bin/run-test-${crateName}
          ''}
          touch $out
        '' else if stdenv.hostPlatform == stdenv.buildPlatform then ''
          for file in ${crate}/tests/*; do
            $file 2>&1 >> $out
          done
          set -e
          ${lib.concatMapStringsSep "\n" (o: "grep '${o}' $out || {  echo 'output \"${o}\" not found in:'; cat $out; exit 23; }") expectedTestOutputs}
        '' else ''
          for file in ${crate}/tests/*; do
            test -x "$file"
          done
          touch "$out"
        ''
      );

    /* Returns a derivation that asserts that the crate specified by `crateArgs`
       has the specified files as output.

       `name` is used as part of the derivation name that performs the checking.

       `crateArgs` is passed to `mkHostCrate` to build the crate with `buildRustCrate`.

       `expectedFiles` contains a list of expected file paths in the output. E.g.
       `[ "./bin/my_binary" ]`.

       `output` specifies the name of the output to use. By default, the default
       output is used but e.g. `output = "lib";` will cause the lib output
       to be checked instead. You do not need to specify any directories.
     */
    assertOutputs = { name, crateArgs, expectedFiles, output? null }:
      assert (builtins.isString name);
      assert (builtins.isAttrs crateArgs);
      assert (builtins.isList expectedFiles);

      let
        crate = mkHostCrate (builtins.removeAttrs crateArgs ["expectedTestOutput"]);
        crateOutput = if output == null then crate else crate."${output}";
        expectedFilesFile = writeTextFile {
          name = "expected-files-${name}";
          text =
            let sorted = builtins.sort (a: b: a<b) expectedFiles;
                concatenated = builtins.concatStringsSep "\n" sorted;
            in "${concatenated}\n";
        };
      in
      runCommand "assert-outputs-${name}" {
      } (''
      local actualFiles=$(mktemp)

      cd "${crateOutput}"
      find . -type f \
        | sort \
      ''
      # sed out the hash because it differs per platform
      + ''
        | sed -E -e 's/-[0-9a-fA-F]{10}\.rlib/-HASH.rlib/g' \
        > "$actualFiles"
      diff -q ${expectedFilesFile} "$actualFiles" > /dev/null || {
        echo -e "\033[0;1;31mERROR: Difference in expected output files in ${crateOutput} \033[0m" >&2
        echo === Got:
        sed -e 's/^/  /' $actualFiles
        echo === Expected:
        sed -e 's/^/  /' ${expectedFilesFile}
        echo === Diff:
        diff -u ${expectedFilesFile} $actualFiles |\
          tail -n +3 |\
          sed -e 's/^/  /'
        exit 1
      }
      touch $out
      '')
      ;

  in rec {

  tests = let
    cases = rec {
      libPath =  { libPath = "src/my_lib.rs"; src = mkLib "src/my_lib.rs"; };
      srcLib =  { src = mkLib "src/lib.rs"; };

      # This used to be supported by cargo but as of 1.40.0 I can't make it work like that with just cargo anymore.
      # This might be a regression or deprecated thing they finally removed…
      # customLibName =  { libName = "test_lib"; src = mkLib "src/test_lib.rs"; };
      # rustLibTestsCustomLibName = {
      #   libName = "test_lib";
      #   src = mkTestFile "src/test_lib.rs" "foo";
      #   buildTests = true;
      #   expectedTestOutputs = [ "test foo ... ok" ];
      # };

      customLibNameAndLibPath =  { libName = "test_lib"; libPath = "src/best-lib.rs"; src = mkLib "src/best-lib.rs"; };
      crateBinWithPath =  { crateBin = [{ name = "test_binary1"; path = "src/foobar.rs"; }]; src = mkBin "src/foobar.rs"; };
      crateBinNoPath1 =  { crateBin = [{ name = "my-binary2"; }]; src = mkBin "src/my_binary2.rs"; };
      crateBinNoPath2 =  {
        crateBin = [{ name = "my-binary3"; } { name = "my-binary4"; }];
        src = symlinkJoin {
          name = "buildRustCrateMultipleBinariesCase";
          paths = [ (mkBin "src/bin/my_binary3.rs") (mkBin "src/bin/my_binary4.rs") ];
        };
      };
      crateBinNoPath3 =  { crateBin = [{ name = "my-binary5"; }]; src = mkBin "src/bin/main.rs"; };
      crateBinNoPath4 =  { crateBin = [{ name = "my-binary6"; }]; src = mkBin "src/main.rs";};
      crateBinRename1 = {
        crateBin = [{ name = "my-binary-rename1"; }];
        src = mkBinExtern "src/main.rs" "foo_renamed";
        dependencies = [ (mkHostCrate { crateName = "foo"; src = mkLib "src/lib.rs"; }) ];
        crateRenames = { "foo" = "foo_renamed"; };
      };
      crateBinRename2 = {
        crateBin = [{ name = "my-binary-rename2"; }];
        src = mkBinExtern "src/main.rs" "foo_renamed";
        dependencies = [ (mkHostCrate { crateName = "foo"; libName = "foolib"; src = mkLib "src/lib.rs"; }) ];
        crateRenames = { "foo" = "foo_renamed"; };
      };
      crateBinRenameMultiVersion = let
        crateWithVersion = version: mkHostCrate {
          crateName = "my_lib";
          inherit version;
          src = mkFile "src/lib.rs" ''
            pub const version: &str = "${version}";
          '';
        };
        depCrate01 = crateWithVersion "0.1.2";
        depCrate02 = crateWithVersion "0.2.1";
      in {
        crateName = "my_bin";
        src = symlinkJoin {
          name = "my_bin_src";
          paths = [
            (mkFile  "src/main.rs" ''
              #[test]
              fn my_lib_01() { assert_eq!(lib01::version, "0.1.2"); }

              #[test]
              fn my_lib_02() { assert_eq!(lib02::version, "0.2.1"); }

              fn main() { }
            '')
          ];
        };
        dependencies = [ depCrate01 depCrate02 ];
        crateRenames = {
          "my_lib" = [
            {
              version = "0.1.2";
              rename = "lib01";
            }
            {
              version = "0.2.1";
              rename = "lib02";
            }
          ];
        };
        buildTests = true;
        expectedTestOutputs = [
          "test my_lib_01 ... ok"
          "test my_lib_02 ... ok"
        ];
      };
      rustLibTestsDefault = {
        src = mkTestFile "src/lib.rs" "baz";
        buildTests = true;
        expectedTestOutputs = [ "test baz ... ok" ];
      };
      rustLibTestsCustomLibPath = {
        libPath = "src/test_path.rs";
        src = mkTestFile "src/test_path.rs" "bar";
        buildTests = true;
        expectedTestOutputs = [ "test bar ... ok" ];
      };
      rustLibTestsCustomLibPathWithTests = {
        libPath = "src/test_path.rs";
        src = symlinkJoin {
          name = "rust-lib-tests-custom-lib-path-with-tests-dir";
          paths = [
            (mkTestFile "src/test_path.rs" "bar")
            (mkTestFile "tests/something.rs" "something")
          ];
        };
        buildTests = true;
        expectedTestOutputs = [
          "test bar ... ok"
          "test something ... ok"
        ];
      };
      rustBinTestsCombined = {
        src = symlinkJoin {
          name = "rust-bin-tests-combined";
          paths = [
            (mkTestFileWithMain "src/main.rs" "src_main")
            (mkTestFile "tests/foo.rs" "tests_foo")
            (mkTestFile "tests/bar.rs" "tests_bar")
          ];
        };
        buildTests = true;
        expectedTestOutputs = [
          "test src_main ... ok"
          "test tests_foo ... ok"
          "test tests_bar ... ok"
        ];
      };
      rustBinTestsSubdirCombined = {
        src = symlinkJoin {
          name = "rust-bin-tests-subdir-combined";
          paths = [
            (mkTestFileWithMain "src/main.rs" "src_main")
            (mkTestFile "tests/foo/main.rs" "tests_foo")
            (mkTestFile "tests/bar/main.rs" "tests_bar")
          ];
        };
        buildTests = true;
        expectedTestOutputs = [
          "test src_main ... ok"
          "test tests_foo ... ok"
          "test tests_bar ... ok"
        ];
      };
      linkAgainstRlibCrate = {
        crateName = "foo";
        src = mkFile  "src/main.rs" ''
          extern crate somerlib;
          fn main() {}
        '';
        dependencies = [
          (mkHostCrate {
            crateName = "somerlib";
            type = [ "rlib" ];
            src = mkLib "src/lib.rs";
          })
        ];
      };
      buildScriptDeps = let
        depCrate = buildRustCrate: boolVal: mkCrate buildRustCrate {
          crateName = "bar";
          src = mkFile "src/lib.rs" ''
            pub const baz: bool = ${boolVal};
          '';
        };
      in {
        crateName = "foo";
        src = symlinkJoin {
          name = "build-script-and-main";
          paths = [
            (mkFile  "src/main.rs" ''
              extern crate bar;
              #[cfg(test)]
              #[test]
              fn baz_false() { assert!(!bar::baz); }
              fn main() { }
            '')
            (mkFile  "build.rs" ''
              extern crate bar;
              fn main() { assert!(bar::baz); }
            '')
          ];
        };
        buildDependencies = [ (depCrate buildPackages.buildRustCrate "true") ];
        dependencies = [ (depCrate buildRustCrate "false") ];
        buildTests = true;
        expectedTestOutputs = [ "test baz_false ... ok" ];
      };
      buildScriptFeatureEnv = {
        crateName = "build-script-feature-env";
        features = [ "some-feature" "crate/another_feature" ];
        src = symlinkJoin {
          name = "build-script-feature-env";
          paths = [
            (mkFile  "src/main.rs" ''
              #[cfg(test)]
              #[test]
              fn feature_not_visible() {
                assert!(std::env::var("CARGO_FEATURE_SOME_FEATURE").is_err());
                assert!(option_env!("CARGO_FEATURE_SOME_FEATURE").is_none());
              }
              fn main() {}
            '')
            (mkFile  "build.rs" ''
              fn main() {
                assert!(std::env::var("CARGO_FEATURE_SOME_FEATURE").is_ok());
                assert!(option_env!("CARGO_FEATURE_SOME_FEATURE").is_none());
              }
            '')
          ];
        };
        buildTests = true;
        expectedTestOutputs = [ "test feature_not_visible ... ok" ];
      };
      # Regression test for https://github.com/NixOS/nixpkgs/pull/88054
      # Build script output should be rewritten as valid env vars.
      buildScriptIncludeDirDeps = let
        depCrate = mkHostCrate {
          crateName = "bar";
          src = symlinkJoin {
            name = "build-script-and-include-dir-bar";
            paths = [
              (mkFile  "src/lib.rs" ''
                fn main() { }
              '')
              (mkFile  "build.rs" ''
                use std::path::PathBuf;
                fn main() { println!("cargo:include-dir={}/src", std::env::current_dir().unwrap_or(PathBuf::from(".")).to_str().unwrap()); }
              '')
            ];
          };
        };
      in {
        crateName = "foo";
        src = symlinkJoin {
          name = "build-script-and-include-dir-foo";
          paths = [
            (mkFile  "src/main.rs" ''
              fn main() { }
            '')
            (mkFile  "build.rs" ''
              fn main() { assert!(std::env::var_os("DEP_BAR_INCLUDE_DIR").is_some()); }
            '')
          ];
        };
        buildDependencies = [ depCrate ];
        dependencies = [ depCrate ];
      };
      # Regression test for https://github.com/NixOS/nixpkgs/issues/74071
      # Whenevever a build.rs file is generating files those should not be overlayed onto the actual source dir
      buildRsOutDirOverlay = {
        src = symlinkJoin {
          name = "buildrs-out-dir-overlay";
          paths = [
            (mkLib "src/lib.rs")
            (mkFile "build.rs" ''
              use std::env;
              use std::ffi::OsString;
              use std::fs;
              use std::path::Path;
              fn main() {
                let out_dir = env::var_os("OUT_DIR").expect("OUT_DIR not set");
                let out_file = Path::new(&out_dir).join("lib.rs");
                fs::write(out_file, "invalid rust code!").expect("failed to write lib.rs");
              }
            '')
          ];
        };
      };
      # Regression test for https://github.com/NixOS/nixpkgs/pull/83379
      # link flag order should be preserved
      linkOrder = {
        src = symlinkJoin {
          name = "buildrs-out-dir-overlay";
          paths = [
            (mkFile "build.rs" ''
              fn main() {
                // in the other order, linkage will fail
                println!("cargo:rustc-link-lib=b");
                println!("cargo:rustc-link-lib=a");
              }
            '')
            (mkFile "src/main.rs" ''
              extern "C" {
                fn hello_world();
              }
              fn main() {
                unsafe {
                  hello_world();
                }
              }
            '')
          ];
        };
        buildInputs = let
          compile = name: text: let
            src = writeTextFile {
              name = "${name}-src.c";
              inherit text;
            };
          in runCommandCC name {} ''
            mkdir -p $out/lib
            # Note: On darwin (which defaults to clang) we have to add
            # `-undefined dynamic_lookup` as otherwise the compilation fails.
            $CC -shared \
              ${lib.optionalString stdenv.isDarwin "-undefined dynamic_lookup"} \
              -o $out/lib/${name}${stdenv.hostPlatform.extensions.sharedLibrary} ${src}
          '';
          b = compile "libb" ''
            #include <stdio.h>

            void hello();

            void hello_world() {
              hello();
              printf(" world!\n");
            }
          '';
          a = compile "liba" ''
            #include <stdio.h>

            void hello() {
              printf("hello");
            }
          '';
        in [ a b ];
      };
      rustCargoTomlInSubDir = {
        # The "workspace_member" can be set to the sub directory with the crate to build.
        # By default ".", meaning the top level directory is assumed.
        # Using null will trigger a search.
        workspace_member = null;
        src = symlinkJoin rec {
          name = "find-cargo-toml";
          paths = [
            (mkCargoToml { name = "ignoreMe"; })
            (mkTestFileWithMain "src/main.rs" "ignore_main")

            (mkCargoToml { name = "rustCargoTomlInSubDir"; path = "subdir/Cargo.toml"; })
            (mkTestFileWithMain "subdir/src/main.rs" "src_main")
            (mkTestFile "subdir/tests/foo/main.rs" "tests_foo")
            (mkTestFile "subdir/tests/bar/main.rs" "tests_bar")
          ];
        };
        buildTests = true;
        expectedTestOutputs = [
          "test src_main ... ok"
          "test tests_foo ... ok"
          "test tests_bar ... ok"
        ];
      };

      rustCargoTomlInTopDir =
        let
          withoutCargoTomlSearch = builtins.removeAttrs rustCargoTomlInSubDir [ "workspace_member" ];
        in
          withoutCargoTomlSearch // {
            expectedTestOutputs = [
              "test ignore_main ... ok"
            ];
          };
      procMacroInPrelude = {
        procMacro = true;
        edition = "2018";
        src = symlinkJoin {
          name = "proc-macro-in-prelude";
          paths = [
            (mkFile "src/lib.rs" ''
              use proc_macro::TokenTree;
            '')
          ];
        };
      };
    };
    brotliCrates = (callPackage ./brotli-crates.nix {});
    rcgenCrates = callPackage ./rcgen-crates.nix {
      # Suppress deprecation warning
      buildRustCrate = null;
    };
    tests = lib.mapAttrs (key: value: mkTest (value // lib.optionalAttrs (!value?crateName) { crateName = key; })) cases;
  in tests // rec {

    crateBinWithPathOutputs = assertOutputs {
      name="crateBinWithPath";
      crateArgs = {
        crateBin = [{ name = "test_binary1"; path = "src/foobar.rs"; }];
        src = mkBin "src/foobar.rs";
      };
      expectedFiles = [
        "./bin/test_binary1"
      ];
    };

    crateBinWithPathOutputsDebug = assertOutputs {
      name="crateBinWithPath";
      crateArgs = {
        release = false;
        crateBin = [{ name = "test_binary1"; path = "src/foobar.rs"; }];
        src = mkBin "src/foobar.rs";
      };
      expectedFiles = [
        "./bin/test_binary1"
      ] ++ lib.optionals stdenv.isDarwin [
        # On Darwin, the debug symbols are in a separate directory.
        "./bin/test_binary1.dSYM/Contents/Info.plist"
        "./bin/test_binary1.dSYM/Contents/Resources/DWARF/test_binary1"
      ];
    };

    crateBinNoPath1Outputs = assertOutputs {
      name="crateBinNoPath1";
      crateArgs = {
        crateBin = [{ name = "my-binary2"; }];
        src = mkBin "src/my_binary2.rs";
      };
      expectedFiles = [
        "./bin/my-binary2"
      ];
    };

    crateLibOutputs = assertOutputs {
      name="crateLib";
      output="lib";
      crateArgs = {
        libName = "test_lib";
        type = [ "rlib" ];
        libPath = "src/lib.rs";
        src = mkLib "src/lib.rs";
      };
      expectedFiles = [
        "./nix-support/propagated-build-inputs"
        "./lib/libtest_lib-HASH.rlib"
        "./lib/link"
      ];
    };

    crateLibOutputsDebug = assertOutputs {
      name="crateLib";
      output="lib";
      crateArgs = {
        release = false;
        libName = "test_lib";
        type = [ "rlib" ];
        libPath = "src/lib.rs";
        src = mkLib "src/lib.rs";
      };
      expectedFiles = [
        "./nix-support/propagated-build-inputs"
        "./lib/libtest_lib-HASH.rlib"
        "./lib/link"
      ];
    };

    brotliTest = let
      pkg = brotliCrates.brotli_2_5_0 {};
    in runCommand "run-brotli-test-cmd" {
      nativeBuildInputs = [ pkg ];
    } (if stdenv.hostPlatform == stdenv.buildPlatform then ''
      ${pkg}/bin/brotli -c ${pkg}/bin/brotli > /dev/null && touch $out
    '' else ''
      test -x '${pkg}/bin/brotli' && touch $out
    '');
    allocNoStdLibTest = let
      pkg = brotliCrates.alloc_no_stdlib_1_3_0 {};
    in runCommand "run-alloc-no-stdlib-test-cmd" {
      nativeBuildInputs = [ pkg ];
    } ''
      test -e ${pkg}/bin/example && touch $out
    '';
    brotliDecompressorTest = let
      pkg = brotliCrates.brotli_decompressor_1_3_1 {};
    in runCommand "run-brotli-decompressor-test-cmd" {
      nativeBuildInputs = [ pkg ];
    } ''
      test -e ${pkg}/bin/brotli-decompressor && touch $out
    '';

    rcgenTest = let
      pkg = rcgenCrates.rootCrate.build;
    in runCommand "run-rcgen-test-cmd" {
      nativeBuildInputs = [ pkg ];
    } (if stdenv.hostPlatform == stdenv.buildPlatform then ''
      ${pkg}/bin/rcgen && touch $out
    '' else ''
      test -x '${pkg}/bin/rcgen' && touch $out
    '');
  };
  test = releaseTools.aggregate {
    name = "buildRustCrate-tests";
    meta = {
      description = "Test cases for buildRustCrate";
      maintainers = [ ];
    };
    constituents = builtins.attrValues tests;
  };
}
