{ pkgs, ... }:
let
  homeserverUrl = "http://homeserver:8008";
in
{
  name = "matrix-appservice-irc";
  meta = {
    maintainers = pkgs.matrix-appservice-irc.meta.maintainers;
  };

  nodes = {
    homeserver = {
      # We'll switch to this once the config is copied into place
      specialisation.running.configuration = {
        services.matrix-synapse = {
          enable = true;
          settings = {
            database.name = "sqlite3";
            app_service_config_files = [ "/registration.yml" ];

            enable_registration = true;

            # don't use this in production, always use some form of verification
            enable_registration_without_verification = true;

            listeners = [
              {
                # The default but tls=false
                bind_addresses = [
                  "0.0.0.0"
                ];
                port = 8008;
                resources = [
                  {
                    "compress" = true;
                    "names" = [ "client" ];
                  }
                  {
                    "compress" = false;
                    "names" = [ "federation" ];
                  }
                ];
                tls = false;
                type = "http";
              }
            ];
          };
        };

        networking.firewall.allowedTCPPorts = [ 8008 ];
      };
    };

    ircd = {
      services.ngircd = {
        enable = true;
        config = ''
          [Global]
            Name = ircd.ircd
            Info = Server Info Text
            AdminInfo1 = _

          [Channel]
            Name = #test
            Topic = a cool place

          [Options]
            PAM = no
        '';
      };
      networking.firewall.allowedTCPPorts = [ 6667 ];
    };

    appservice =
      { pkgs, ... }:
      {
        services.matrix-appservice-irc = {
          enable = true;
          registrationUrl = "http://appservice:8009";

          settings = {
            homeserver.url = homeserverUrl;
            homeserver.domain = "homeserver";

            ircService = {
              servers."ircd" = {
                name = "IRCd";
                port = 6667;
                dynamicChannels = {
                  enabled = true;
                  aliasTemplate = "#irc_$CHANNEL";
                };
              };
              mediaProxy = {
                publicUrl = "http://localhost:11111/media";
                ttl = 0;
              };
            };
          };
        };

        networking.firewall.allowedTCPPorts = [ 8009 ];
      };

    client =
      { pkgs, ... }:
      {
        environment.systemPackages = [
          (pkgs.writers.writePython3Bin "do_test"
            {
              libraries = [ pkgs.python3Packages.matrix-nio ];
              flakeIgnore = [
                # We don't live in the dark ages anymore.
                # Languages like Python that are whitespace heavy will overrun
                # 79 characters..
                "E501"
              ];
            }
            ''
              import sys
              import socket
              import functools
              from time import sleep
              import asyncio

              from nio import AsyncClient, RoomMessageText, JoinResponse


              async def matrix_room_message_text_callback(matrix: AsyncClient, msg: str, _r, e):
                  print("Received matrix text message: ", e)
                  if msg in e.body:
                      print("Received hi from IRC")
                      await matrix.close()
                      exit(0)  # Actual exit point


              class IRC:
                  def __init__(self):
                      sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                      sock.connect(("ircd", 6667))
                      sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
                      sock.send(b"USER bob bob bob :bob\n")
                      sock.send(b"NICK bob\n")
                      self.sock = sock

                  def join(self, room: str):
                      self.sock.send(f"JOIN {room}\n".encode())

                  def privmsg(self, room: str, msg: str):
                      self.sock.send(f"PRIVMSG {room} :{msg}\n".encode())

                  def expect_msg(self, body: str):
                      buffer = ""
                      while True:
                          buf = self.sock.recv(1024).decode()
                          buffer += buf
                          if body in buffer:
                              return


              async def run(homeserver: str):
                  irc = IRC()

                  matrix = AsyncClient(homeserver)
                  response = await matrix.register("alice", "foobar")
                  print("Matrix register response: ", response)

                  response = await matrix.join("#irc_#test:homeserver")
                  print("Matrix join room response:", response)
                  assert isinstance(response, JoinResponse)
                  room_id = response.room_id

                  irc.join("#test")
                  # FIXME: what are we waiting on here? Matrix? IRC? Both?
                  # 10s seem bad for busy hydra machines.
                  sleep(10)

                  # Exchange messages
                  print("Sending text message to matrix room")
                  response = await matrix.room_send(
                      room_id=room_id,
                      message_type="m.room.message",
                      content={"msgtype": "m.text", "body": "hi from matrix"},
                  )
                  print("Matrix room send response: ", response)
                  irc.privmsg("#test", "hi from irc")

                  print("Waiting for the matrix message to appear on the IRC side...")
                  irc.expect_msg("hi from matrix")

                  callback = functools.partial(
                      matrix_room_message_text_callback, matrix, "hi from irc"
                  )
                  matrix.add_event_callback(callback, RoomMessageText)

                  print("Waiting for matrix message...")
                  await matrix.sync_forever()

                  exit(1)  # Unreachable


              if __name__ == "__main__":
                  asyncio.run(run(sys.argv[1]))
            ''
          )
        ];
      };
  };

  testScript = ''
    import pathlib
    import os

    start_all()

    ircd.wait_for_unit("ngircd.service")
    ircd.wait_for_open_port(6667)

    with subtest("start the appservice"):
        appservice.wait_for_unit("matrix-appservice-irc.service")
        appservice.wait_for_open_port(8009)
        appservice.wait_for_file("/var/lib/matrix-appservice-irc/media-signingkey.jwk")
        appservice.wait_for_open_port(11111)

    with subtest("copy the registration file"):
        appservice.copy_from_vm("/var/lib/matrix-appservice-irc/registration.yml")
        homeserver.copy_from_host(
            str(pathlib.Path(os.environ.get("out", os.getcwd())) / "registration.yml"), "/"
        )
        homeserver.succeed("chmod 444 /registration.yml")

    with subtest("start the homeserver"):
        homeserver.succeed(
            "/run/current-system/specialisation/running/bin/switch-to-configuration test >&2"
        )

        homeserver.wait_for_unit("matrix-synapse.service")
        homeserver.wait_for_open_port(8008)

    with subtest("ensure messages can be exchanged"):
        client.succeed("do_test ${homeserverUrl} >&2")
  '';
}
