import ../make-test-python.nix (
  { lib, ... }:
  let
    snakeoil-keys = import ./snakeoil-keys.nix;

    hosts = lib.attrNames snakeoil-keys;

    subnetOf =
      name: config:
      let
        subnets = config.services.tinc.networks.myNetwork.hostSettings.${name}.subnets;
      in
      (builtins.head subnets).address;

    makeTincHost =
      name:
      {
        subnet,
        extraConfig ? { },
      }:
      lib.mkMerge [
        {
          subnets = [ { address = subnet; } ];
          settings = {
            Ed25519PublicKey = snakeoil-keys.${name}.ed25519Public;
          };
          rsaPublicKey = snakeoil-keys.${name}.rsaPublic;
        }
        extraConfig
      ];

    makeTincNode =
      { config, ... }:
      name: extraConfig:
      lib.mkMerge [
        {
          services.tinc.networks.myNetwork = {
            inherit name;
            rsaPrivateKeyFile = builtins.toFile "rsa.priv" snakeoil-keys.${name}.rsaPrivate;
            ed25519PrivateKeyFile = builtins.toFile "ed25519.priv" snakeoil-keys.${name}.ed25519Private;

            hostSettings = lib.mapAttrs makeTincHost {
              static = {
                subnet = "10.0.0.11";
                # Only specify the addresses in the node's vlans, Tinc does not
                # seem to try each one, unlike the documentation suggests...
                extraConfig.addresses = map (vlan: {
                  address = "192.168.${toString vlan}.11";
                  port = 655;
                }) config.virtualisation.vlans;
              };
              dynamic1 = {
                subnet = "10.0.0.21";
              };
              dynamic2 = {
                subnet = "10.0.0.22";
              };
            };
          };

          networking.useDHCP = false;

          networking.interfaces."tinc.myNetwork" = {
            virtual = true;
            virtualType = "tun";
            ipv4.addresses = [
              {
                address = subnetOf name config;
                prefixLength = 24;
              }
            ];
          };

          # Prevents race condition between NixOS service and tinc creating the
          # interface.
          # See: https://github.com/NixOS/nixpkgs/issues/27070
          systemd.services."tinc.myNetwork" = {
            after = [ "network-addresses-tinc.myNetwork.service" ];
            requires = [ "network-addresses-tinc.myNetwork.service" ];
          };

          networking.firewall.allowedTCPPorts = [ 655 ];
          networking.firewall.allowedUDPPorts = [ 655 ];
        }
        extraConfig
      ];

  in
  {
    name = "tinc";
    meta.maintainers = with lib.maintainers; [ minijackson ];

    nodes = {

      static =
        { ... }@args:
        makeTincNode args "static" {
          virtualisation.vlans = [
            1
            2
          ];

          networking.interfaces.eth1.ipv4.addresses = [
            {
              address = "192.168.1.11";
              prefixLength = 24;
            }
          ];

          networking.interfaces.eth2.ipv4.addresses = [
            {
              address = "192.168.2.11";
              prefixLength = 24;
            }
          ];
        };

      dynamic1 =
        { ... }@args:
        makeTincNode args "dynamic1" {
          virtualisation.vlans = [ 1 ];
        };

      dynamic2 =
        { ... }@args:
        makeTincNode args "dynamic2" {
          virtualisation.vlans = [ 2 ];
        };

    };

    testScript = ''
      start_all()

      static.wait_for_unit("tinc.myNetwork.service")
      dynamic1.wait_for_unit("tinc.myNetwork.service")
      dynamic2.wait_for_unit("tinc.myNetwork.service")

      # Static is accessible by the other hosts
      dynamic1.succeed("ping -c5 192.168.1.11")
      dynamic2.succeed("ping -c5 192.168.2.11")

      # The other hosts are in separate vlans
      dynamic1.fail("ping -c5 192.168.2.11")
      dynamic2.fail("ping -c5 192.168.1.11")

      # Each host can ping themselves through Tinc
      static.succeed("ping -c5 10.0.0.11")
      dynamic1.succeed("ping -c5 10.0.0.21")
      dynamic2.succeed("ping -c5 10.0.0.22")

      # Static is accessible by the other hosts through Tinc
      dynamic1.succeed("ping -c5 10.0.0.11")
      dynamic2.succeed("ping -c5 10.0.0.11")

      # Static can access the other hosts through Tinc
      static.succeed("ping -c5 10.0.0.21")
      static.succeed("ping -c5 10.0.0.22")

      # The other hosts in separate vlans can access each other through Tinc
      dynamic1.succeed("ping -c5 10.0.0.22")
      dynamic2.succeed("ping -c5 10.0.0.21")
    '';
  }
)
