pkgs: test:
let
  inherit (pkgs.lib) mapAttrsToList concatStringsSep genAttrs mkIf;
  inherit (builtins) attrNames;

  interactiveConfig = ({ config, ... }: {
    # so we can run `nix shell nixpkgs#foo` on the machines
    nix.extraOptions = ''
      extra-experimental-features = nix-command flakes
    '';

    # so we can ssh in and rebuild them
    services.openssh = {
      enable = true;
      settings = {
        PermitRootLogin = "yes";
        PermitEmptyPasswords = "yes";
        UsePAM = false;
      };
    };

    virtualisation = mkIf (config.networking.hostName == "jumphost") {
      forwardPorts = [{
        from = "host";
        host.port = 2222;
        guest.port = 22;
      }];
    };
  });

  sshConfig = pkgs.writeText "ssh-config" ''
    Host *
      User root
      StrictHostKeyChecking no
      BatchMode yes
      ConnectTimeout 20
      UserKnownHostsFile=/dev/null
      LogLevel Error # no "added to known hosts"
    Host jumphost
      Port 2222
      HostName localhost
    Host * !jumphost
      ProxyJump jumphost
  '';

  # one should first start up the interactive test driver, then start the
  # machines, then update the config, and then redeploy with the `rebuildScript`
  # associated with the new config.
  rebuildScript = pkgs.writeShellScriptBin "rebuild" ''
    # create an association array from machine names to the path to their
    # configuration in the nix store
    declare -A configPaths=(${
      concatStringsSep " "
        (mapAttrsToList
          (n: v: ''["${n}"]="${v.system.build.toplevel}"'')
          rebuildableTest.driverInteractive.nodes)
    })

    rebuild_one() {
      machine="$1"
      echo "pushing new config to $machine"

      if [ -z ''${configPaths[$machine]+x} ]; then
        echo 'No machine '"$machine"' in this test.'
        exit 1
      fi

      if ! ssh -F ${sshConfig} $machine true; then
        echo 'Couldn'"'"'t connect to '"$machine"'. Make sure you'"'"'ve started it with `'"$machine"'.start()` in the test interactive driver.'
        exit 1
      fi

      # taken from nixos-rebuild (we only want to do the activate part)
       cmd=(
          "systemd-run"
          "-E" "LOCALE_ARCHIVE"
          "--collect"
          "--no-ask-password"
          "--pty"
          "--quiet"
          "--same-dir"
          "--service-type=exec"
          "--unit=nixos-rebuild-switch-to-configuration"
          "--wait"
          "''${configPaths[$machine]}/bin/switch-to-configuration"
          "test"
      )


      if ! ssh -F ${sshConfig} $machine "''${cmd[@]}"; then
          echo "warning: error(s) occurred while switching to the new configuration"
          exit 1
      fi
    }

    if ! ssh -F ${sshConfig} jumphost true; then
      echo 'Couldn'"'"'t connect to jump host. Make sure you are running driverInteractive, and that you'"'"'ve run `jumphost.start()` and `jumphost.forward_port(2222,22)`'
      exit 1
    fi

    if [ -n "$1" ]; then
      rebuild_one "$1"
    else
      for machine in ${concatStringsSep " " (attrNames rebuildableTest.driverInteractive.nodes)}; do
        rebuild_one $machine
      done
    fi
  '';

  # NOTE: This is awkward because NixOS does not expose the module interface
  # that is used to build tests. When we upstream this, we can build it into the
  # system more naturally (and expose more of the interface to end users while
  # we're at it)
  rebuildableTest =
    let
      preOverride = pkgs.nixosTest (test // {
        interactive = (test.interactive or { }) // {
          # no need to // with test.interactive.nodes here, since we are iterating
          # over all of them, and adding back in the config via `imports`
          nodes = genAttrs
            (
              attrNames test.nodes or { } ++
                attrNames test.interactive.nodes or { } ++
                [ "jumphost" ]
            )
            (n: {
              imports = [
                (test.interactive.${n} or { })
                interactiveConfig
              ];
            });
        };
        # override with test.passthru in case someone wants to overwrite us.
        passthru = { inherit rebuildScript sshConfig; } // (test.passthru or { });
      });
    in
    preOverride // {
      driverInteractive = preOverride.driverInteractive.overrideAttrs (old: {
        # this comes from runCommand, not mkDerivation, so this is the only
        # hook we have to override
        buildCommand = old.buildCommand + ''
          ln -s ${sshConfig} $out/ssh-config
          ln -s ${rebuildScript}/bin/rebuild $out/bin/rebuild
        '';
      });
    };
in
rebuildableTest