{
  system ? builtins.currentSystem,
  config ? { },
  pkgs ? import ../.. { inherit system config; },
}:

with import ../lib/testing-python.nix { inherit system pkgs; };

let
  readyFile = "/tmp/readerReady";
  resultFile = "/tmp/readerResult";

  testReader = pkgs.writeScript "test-input-reader" ''
    rm -f ${resultFile} ${resultFile}.tmp
    logger "testReader: START: expecting '$1'."
    touch ${readyFile}
    read -r -N ''${#1} -t 60 chars
    rm -f ${readyFile}

    if [ "$chars" == "$1" ]; then
      logger -s "testReader: PASS: Got '$1' as expected." 2>${resultFile}.tmp
    else
      logger -s "testReader: FAIL: Expected '$1' but got '$chars'." 2>${resultFile}.tmp
    fi
    # rename after the file is written to prevent a race condition
    mv  ${resultFile}.tmp ${resultFile}
  '';

  mkKeyboardTest =
    layout:
    {
      extraConfig ? { },
      tests,
    }:
    with pkgs.lib;
    makeTest {
      name = "keymap-${layout}";

      nodes.machine.console.keyMap = mkOverride 900 layout;
      nodes.machine.services.xserver.desktopManager.xterm.enable = false;
      nodes.machine.services.xserver.xkb.layout = mkOverride 900 layout;
      nodes.machine.imports = [
        ./common/x11.nix
        extraConfig
      ];

      testScript = ''
        import json
        import shlex


        def run_test_case(cmd, inputs, expected):
            assert len(inputs) == len(expected)
            machine.execute("rm -f ${readyFile} ${resultFile}")

            # set up process that expects all the keys to be entered
            machine.succeed(
                "${pkgs.systemd}/bin/systemd-cat -t input-test-reader -- {} {} {} &".format(
                    cmd,
                    "${testReader}",
                    shlex.quote("".join(expected)),
                )
            )

            # wait for reader to be ready
            machine.wait_for_file("${readyFile}")

            # send all keys
            for key in inputs:
                machine.send_key(key)

            # wait for result and check
            machine.wait_for_file("${resultFile}")
            machine.succeed("grep -q 'PASS:' ${resultFile}")


        with open("${pkgs.writeText "tests.json" (builtins.toJSON tests)}") as json_file:
            tests = json.load(json_file)

        # These environments used to run in the opposite order, causing the
        # following error at openvt startup.
        #
        # openvt: Couldn't deallocate console 1
        #
        # This error did not appear in successful runs.
        # I don't know the exact cause, but I it seems that openvt and X are
        # fighting over the virtual terminal. This does not appear to be a problem
        # when the X test runs first.
        keymap_environments = {
            "Xorg Keymap": "env DISPLAY=:0 xterm -title testterm -class testterm -fullscreen -e",
            "VT Keymap": "openvt -c 2 -sw --",
        }

        machine.wait_for_x()

        for test_case_name, test_data in tests.items():
            for keymap_env_name, command in keymap_environments.items():
                with subtest(f"{test_case_name} - {keymap_env_name}"):
                    run_test_case(
                        command,
                        test_data["qwerty"],
                        test_data["expect"],
                    )
      '';
    };

in
pkgs.lib.mapAttrs mkKeyboardTest {
  azerty = {
    tests = {
      azqw.qwerty = [
        "q"
        "w"
      ];
      azqw.expect = [
        "a"
        "z"
      ];
      altgr.qwerty = [
        "alt_r-2"
        "alt_r-3"
        "alt_r-4"
        "alt_r-5"
        "alt_r-6"
      ];
      altgr.expect = [
        "~"
        "#"
        "{"
        "["
        "|"
      ];
    };

    extraConfig.console.keyMap = "fr";
    extraConfig.services.xserver.xkb.layout = "fr";
  };

  bone = {
    tests = {
      layer1.qwerty = [
        "f"
        "j"
      ];
      layer1.expect = [
        "e"
        "n"
      ];
      layer2.qwerty = [
        "shift-f"
        "shift-j"
        "shift-6"
      ];
      layer2.expect = [
        "E"
        "N"
        "$"
      ];
      layer3.qwerty = [
        "caps_lock-d"
        "caps_lock-f"
      ];
      layer3.expect = [
        "{"
        "}"
      ];
    };

    extraConfig.console.keyMap = "bone";
    extraConfig.services.xserver.xkb.layout = "de";
    extraConfig.services.xserver.xkb.variant = "bone";
  };

  colemak = {
    tests = {
      homerow.qwerty = [
        "a"
        "s"
        "d"
        "f"
        "j"
        "k"
        "l"
        "semicolon"
      ];
      homerow.expect = [
        "a"
        "r"
        "s"
        "t"
        "n"
        "e"
        "i"
        "o"
      ];
    };

    extraConfig.console.keyMap = "colemak";
    extraConfig.services.xserver.xkb.layout = "us";
    extraConfig.services.xserver.xkb.variant = "colemak";
  };

  dvorak = {
    tests = {
      homerow.qwerty = [
        "a"
        "s"
        "d"
        "f"
        "j"
        "k"
        "l"
        "semicolon"
      ];
      homerow.expect = [
        "a"
        "o"
        "e"
        "u"
        "h"
        "t"
        "n"
        "s"
      ];
      symbols.qwerty = [
        "q"
        "w"
        "e"
        "minus"
        "equal"
      ];
      symbols.expect = [
        "'"
        ","
        "."
        "["
        "]"
      ];
    };

    extraConfig.console.keyMap = "dvorak";
    extraConfig.services.xserver.xkb.layout = "us";
    extraConfig.services.xserver.xkb.variant = "dvorak";
  };

  dvorak-programmer = {
    tests = {
      homerow.qwerty = [
        "a"
        "s"
        "d"
        "f"
        "j"
        "k"
        "l"
        "semicolon"
      ];
      homerow.expect = [
        "a"
        "o"
        "e"
        "u"
        "h"
        "t"
        "n"
        "s"
      ];
      numbers.qwerty = map (x: "shift-${x}") [
        "1"
        "2"
        "3"
        "4"
        "5"
        "6"
        "7"
        "8"
        "9"
        "0"
        "minus"
      ];
      numbers.expect = [
        "%"
        "7"
        "5"
        "3"
        "1"
        "9"
        "0"
        "2"
        "4"
        "6"
        "8"
      ];
      symbols.qwerty = [
        "1"
        "2"
        "3"
        "4"
        "5"
        "6"
        "7"
        "8"
        "9"
        "0"
        "minus"
      ];
      symbols.expect = [
        "&"
        "["
        "{"
        "}"
        "("
        "="
        "*"
        ")"
        "+"
        "]"
        "!"
      ];
    };

    extraConfig.console.keyMap = "dvorak-programmer";
    extraConfig.services.xserver.xkb.layout = "us";
    extraConfig.services.xserver.xkb.variant = "dvp";
  };

  neo = {
    tests = {
      layer1.qwerty = [
        "f"
        "j"
      ];
      layer1.expect = [
        "e"
        "n"
      ];
      layer2.qwerty = [
        "shift-f"
        "shift-j"
        "shift-6"
      ];
      layer2.expect = [
        "E"
        "N"
        "$"
      ];
      layer3.qwerty = [
        "caps_lock-d"
        "caps_lock-f"
      ];
      layer3.expect = [
        "{"
        "}"
      ];
    };

    extraConfig.console.keyMap = "neo";
    extraConfig.services.xserver.xkb.layout = "de";
    extraConfig.services.xserver.xkb.variant = "neo";
  };

  qwertz = {
    tests = {
      zy.qwerty = [
        "z"
        "y"
      ];
      zy.expect = [
        "y"
        "z"
      ];
      altgr.qwerty = map (x: "alt_r-${x}") [
        "q"
        "less"
        "7"
        "8"
        "9"
        "0"
      ];
      altgr.expect = [
        "@"
        "|"
        "{"
        "["
        "]"
        "}"
      ];
    };

    extraConfig.console.keyMap = "de";
    extraConfig.services.xserver.xkb.layout = "de";
  };

  custom = {
    tests = {
      us.qwerty = [
        "a"
        "b"
        "g"
        "d"
        "z"
        "shift-2"
        "shift-3"
      ];
      us.expect = [
        "a"
        "b"
        "g"
        "d"
        "z"
        "@"
        "#"
      ];
      greek.qwerty = map (x: "alt_r-${x}") [
        "a"
        "b"
        "g"
        "d"
        "z"
      ];
      greek.expect = [
        "α"
        "β"
        "γ"
        "δ"
        "ζ"
      ];
    };

    extraConfig.console.useXkbConfig = true;
    extraConfig.services.xserver.xkb.layout = "us-greek";
    extraConfig.services.xserver.xkb.extraLayouts.us-greek = {
      description = "US layout with alt-gr greek";
      languages = [ "eng" ];
      symbolsFile = pkgs.writeText "us-greek" ''
        xkb_symbols "us-greek"
        {
          include "us(basic)"
          include "level3(ralt_switch)"
          key <LatA> { [ a, A, Greek_alpha ] };
          key <LatB> { [ b, B, Greek_beta  ] };
          key <LatG> { [ g, G, Greek_gamma ] };
          key <LatD> { [ d, D, Greek_delta ] };
          key <LatZ> { [ z, Z, Greek_zeta  ] };
        };
      '';
    };
  };
}
