import ./make-test-python.nix (
  { pkgs, lib, ... }:
  let
    inherit (import ./ssh-keys.nix pkgs) snakeOilPrivateKey snakeOilPublicKey;

    mkNode = vlan: id: {
      virtualisation.vlans = [ vlan ];
      networking = {
        useDHCP = false;
        useNetworkd = true;
      };

      systemd.network = {
        enable = true;

        networks."10-eth${toString vlan}" = {
          matchConfig.Name = "eth${toString vlan}";
          linkConfig.RequiredForOnline = "no";
          networkConfig = {
            Address = "192.168.${toString vlan}.${toString id}/24";
            IPv4Forwarding = "yes";
            IPv6Forwarding = "yes";
          };
        };
      };
    };
  in
  {
    name = "systemd-networkd-vrf";
    meta.maintainers = with lib.maintainers; [ ma27 ];

    nodes = {
      client =
        { pkgs, ... }:
        {
          virtualisation.vlans = [
            1
            2
          ];

          networking = {
            useDHCP = false;
            useNetworkd = true;
            firewall.checkReversePath = "loose";
          };

          systemd.network = {
            enable = true;

            netdevs."10-vrf1" = {
              netdevConfig = {
                Kind = "vrf";
                Name = "vrf1";
                MTUBytes = "1300";
              };
              vrfConfig.Table = 23;
            };
            netdevs."10-vrf2" = {
              netdevConfig = {
                Kind = "vrf";
                Name = "vrf2";
                MTUBytes = "1300";
              };
              vrfConfig.Table = 42;
            };

            networks."10-vrf1" = {
              matchConfig.Name = "vrf1";
              networkConfig.IPv4Forwarding = "yes";
              networkConfig.IPv6Forwarding = "yes";
              routes = [
                {
                  Destination = "192.168.1.2";
                  Metric = 100;
                }
              ];
            };
            networks."10-vrf2" = {
              matchConfig.Name = "vrf2";
              networkConfig.IPv4Forwarding = "yes";
              networkConfig.IPv6Forwarding = "yes";
              routes = [
                {
                  Destination = "192.168.2.3";
                  Metric = 100;
                }
              ];
            };

            networks."10-eth1" = {
              matchConfig.Name = "eth1";
              linkConfig.RequiredForOnline = "no";
              networkConfig = {
                VRF = "vrf1";
                Address = "192.168.1.1/24";
                IPv4Forwarding = "yes";
                IPv6Forwarding = "yes";
              };
            };
            networks."10-eth2" = {
              matchConfig.Name = "eth2";
              linkConfig.RequiredForOnline = "no";
              networkConfig = {
                VRF = "vrf2";
                Address = "192.168.2.1/24";
                IPv4Forwarding = "yes";
                IPv6Forwarding = "yes";
              };
            };
          };
        };

      node1 = lib.mkMerge [
        (mkNode 1 2)
        {
          services.openssh.enable = true;
          users.users.root.openssh.authorizedKeys.keys = [ snakeOilPublicKey ];
        }
      ];

      node2 = mkNode 2 3;
      node3 = mkNode 2 4;
    };

    testScript = ''
      import json

      def compare(raw_json, to_compare):
          data = json.loads(raw_json)
          assert len(raw_json) >= len(to_compare)
          for i, row in enumerate(to_compare):
              actual = data[i]
              assert len(row.keys()) > 0
              for key, value in row.items():
                  assert value == actual[key], f"""
                    In entry {i}, value {key}: got: {actual[key]}, expected {value}
                  """


      start_all()

      client.wait_for_unit("network.target")
      node1.wait_for_unit("network.target")
      node2.wait_for_unit("network.target")
      node3.wait_for_unit("network.target")

      # Check that networkd properly configures the main routing table
      # and the routing tables for the VRF.
      with subtest("check vrf routing tables"):
          compare(
              client.succeed("ip --json -4 route list"),
              [
                  {"dst": "192.168.1.2", "dev": "vrf1", "metric": 100},
                  {"dst": "192.168.2.3", "dev": "vrf2", "metric": 100}
              ]
          )
          compare(
              client.succeed("ip --json -4 route list table 23"),
              [
                  {"dst": "192.168.1.0/24", "dev": "eth1", "prefsrc": "192.168.1.1"},
                  {"type": "local", "dst": "192.168.1.1", "dev": "eth1", "prefsrc": "192.168.1.1"},
                  {"type": "broadcast", "dev": "eth1", "prefsrc": "192.168.1.1", "dst": "192.168.1.255"}
              ]
          )
          compare(
              client.succeed("ip --json -4 route list table 42"),
              [
                  {"dst": "192.168.2.0/24", "dev": "eth2", "prefsrc": "192.168.2.1"},
                  {"type": "local", "dst": "192.168.2.1", "dev": "eth2", "prefsrc": "192.168.2.1"},
                  {"type": "broadcast", "dev": "eth2", "prefsrc": "192.168.2.1", "dst": "192.168.2.255"}
              ]
          )

      # Ensure that other nodes are reachable via ICMP through the VRF.
      with subtest("icmp through vrf works"):
          client.succeed("ping -c5 192.168.1.2")
          client.succeed("ping -c5 192.168.2.3")

      # Test whether TCP through a VRF IP is possible.
      with subtest("tcp traffic through vrf works"):
          node1.wait_for_open_port(22)
          client.succeed(
              "cat ${snakeOilPrivateKey} > privkey.snakeoil"
          )
          client.succeed("chmod 600 privkey.snakeoil")
          client.succeed(
              "ulimit -l 2048; ip vrf exec vrf1 ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i privkey.snakeoil root@192.168.1.2 true"
          )

      # Only configured routes through the VRF from the main routing table should
      # work. Additional IPs are only reachable when binding to the vrf interface.
      with subtest("only routes from main routing table work by default"):
          client.fail("ping -c5 192.168.2.4")
          client.succeed("ping -I vrf2 -c5 192.168.2.4")

      client.shutdown()
      node1.shutdown()
      node2.shutdown()
      node3.shutdown()
    '';
  }
)
