{ config, lib, pkgs, ... }:

with lib;

let

  stubType = types.submodule ({ name, ... }: {
    options = {
      name = mkOption {
        type = types.str;
        default = "dummy";
        description = "The stub package name.";
      };

      outPath = mkOption {
        type = types.nullOr types.str;
        default = "@${name}@";
        defaultText = literalExpression ''"@''${name}@"'';
      };

      version = mkOption {
        type = types.nullOr types.str;
        default = null;
        defaultText = literalExpression "pkgs.\${name}.version or null";
      };

      buildScript = mkOption {
        type = types.str;
        default = defaultBuildScript;
      };
    };
  });

  defaultBuildScript = "mkdir $out";

  dummyPackage = pkgs.runCommandLocal "dummy" { meta.mainProgram = "dummy"; }
    defaultBuildScript;

  mkStubPackage = { name ? "dummy", outPath ? null, version ? null
    , buildScript ? defaultBuildScript }:
    let
      pkg = if name == "dummy" && buildScript == defaultBuildScript then
        dummyPackage
      else
        pkgs.runCommandLocal name {
          pname = name;
          meta.mainProgram = name;
        } buildScript;
    in pkg // optionalAttrs (outPath != null) {
      inherit outPath;

      # Prevent getOutput from descending into outputs
      outputSpecified = true;

      # Allow the original package to be used in derivation inputs
      __spliced = {
        buildHost = pkg;
        hostTarget = pkg;
      };
    } // optionalAttrs (version != null) { inherit version; };

in {
  options.test.stubs = mkOption {
    type = types.attrsOf stubType;
    default = { };
    description =
      "Package attributes that should be replaced by a stub package.";
  };

  config = {
    lib.test.mkStubPackage = mkStubPackage;

    nixpkgs.overlays = [ (self: super: { inherit mkStubPackage; }) ]
      ++ optional (config.test.stubs != { }) (self: super:
        mapAttrs (n: v:
          mkStubPackage (v // optionalAttrs (v.version == null) {
            version = super.${n}.version or null;
          })) config.test.stubs);
  };
}