{ kanidmPackage, pkgs, ... }:
let
  certs = import ./common/acme/server/snakeoil-certs.nix;
  serverDomain = certs.domain;

  # copy certs to store to work around mount namespacing
  certsPath = pkgs.runCommand "snakeoil-certs" { } ''
    mkdir $out
    cp ${certs."${serverDomain}".cert} $out/snakeoil.crt
    cp ${certs."${serverDomain}".key} $out/snakeoil.key
  '';

  provisionAdminPassword = "very-strong-password-for-admin";
  provisionIdmAdminPassword = "very-strong-password-for-idm-admin";
  provisionIdmAdminPassword2 = "very-strong-alternative-password-for-idm-admin";
in
{
  name = "kanidm-provisioning-${kanidmPackage.version}";
  meta.maintainers = with pkgs.lib.maintainers; [ oddlama ];

  _module.args.kanidmPackage = pkgs.lib.mkDefault pkgs.kanidmWithSecretProvisioning;

  nodes.provision =
    { pkgs, lib, ... }:
    {
      services.kanidm = {
        package = kanidmPackage;
        enableServer = true;
        serverSettings = {
          origin = "https://${serverDomain}";
          domain = serverDomain;
          bindaddress = "[::]:443";
          ldapbindaddress = "[::1]:636";
          tls_chain = "${certsPath}/snakeoil.crt";
          tls_key = "${certsPath}/snakeoil.key";
        };
        # So we can check whether provisioning did what we wanted
        enableClient = true;
        clientSettings = {
          uri = "https://${serverDomain}";
          verify_ca = true;
          verify_hostnames = true;
        };
      };

      specialisation.credentialProvision.configuration =
        { ... }:
        {
          services.kanidm.provision = lib.mkForce {
            enable = true;
            adminPasswordFile = pkgs.writeText "admin-pw" provisionAdminPassword;
            idmAdminPasswordFile = pkgs.writeText "idm-admin-pw" provisionIdmAdminPassword;
          };
        };

      specialisation.changedCredential.configuration =
        { ... }:
        {
          services.kanidm.provision = lib.mkForce {
            enable = true;
            idmAdminPasswordFile = pkgs.writeText "idm-admin-pw" provisionIdmAdminPassword2;
          };
        };

      specialisation.addEntities.configuration =
        { ... }:
        {
          services.kanidm.provision = lib.mkForce {
            enable = true;
            # Test whether credential recovery works without specific idmAdmin password
            #idmAdminPasswordFile =

            groups.supergroup1 = {
              members = [ "testgroup1" ];
            };

            groups.testgroup1 = { };
            groups.imperative = {
              overwriteMembers = false;
              members = [ "testuser1" ];
            };

            persons.testuser1 = {
              displayName = "Test User";
              legalName = "Jane Doe";
              mailAddresses = [ "jane.doe@example.com" ];
              groups = [
                "testgroup1"
                "service1-access"
              ];
            };

            persons.testuser2 = {
              displayName = "Powerful Test User";
              legalName = "Ryouiki Tenkai";
              groups = [ "service1-admin" ];
            };

            groups.service1-access = { };
            groups.service1-admin = { };
            systems.oauth2.service1 = {
              displayName = "Service One";
              originUrl = "https://one.example.com/";
              originLanding = "https://one.example.com/landing";
              basicSecretFile = pkgs.writeText "bs-service1" "very-strong-secret-for-service1";
              scopeMaps.service1-access = [
                "openid"
                "email"
                "profile"
              ];
              supplementaryScopeMaps.service1-admin = [ "admin" ];
              claimMaps.groups = {
                valuesByGroup.service1-admin = [ "admin" ];
              };
            };

            systems.oauth2.service2 = {
              displayName = "Service Two";
              originUrl = "https://two.example.com/";
              originLanding = "https://landing2.example.com/";
              # Test not setting secret
              # basicSecretFile =
              allowInsecureClientDisablePkce = true;
              preferShortUsername = true;
            };
          };
        };

      specialisation.changeAttributes.configuration =
        { ... }:
        {
          services.kanidm.provision = lib.mkForce {
            enable = true;
            # Changing admin credentials at any time should not be a problem:
            idmAdminPasswordFile = pkgs.writeText "idm-admin-pw" provisionIdmAdminPassword;

            groups.supergroup1 = {
              #members = ["testgroup1"];
            };

            groups.testgroup1 = { };
            groups.imperative = {
              overwriteMembers = false;
              # Will be retained:
              # members = [ "testuser1" ];
            };

            persons.testuser1 = {
              displayName = "Test User (changed)";
              legalName = "Jane Doe (changed)";
              mailAddresses = [
                "jane.doe@example.com"
                "second.doe@example.com"
              ];
              groups = [
                #"testgroup1"
                "service1-access"
              ];
            };

            persons.testuser2 = {
              displayName = "Powerful Test User (changed)";
              legalName = "Ryouiki Tenkai (changed)";
              groups = [ "service1-admin" ];
            };

            groups.service1-access = { };
            groups.service1-admin = { };
            systems.oauth2.service1 = {
              displayName = "Service One (changed)";
              # multiple origin urls
              originUrl = [
                "https://changed-one.example.com/"
                "https://changed-one.example.org/"
              ];
              originLanding = "https://changed-one.example.com/landing-changed";
              basicSecretFile = pkgs.writeText "bs-service1" "changed-very-strong-secret-for-service1";
              scopeMaps.service1-access = [
                "openid"
                "email"
                #"profile"
              ];
              supplementaryScopeMaps.service1-admin = [ "adminchanged" ];
              claimMaps.groups = {
                valuesByGroup.service1-admin = [ "adminchanged" ];
              };
            };

            systems.oauth2.service2 = {
              displayName = "Service Two (changed)";
              originUrl = "https://changed-two.example.com/";
              originLanding = "https://changed-landing2.example.com/";
              # Test not setting secret
              # basicSecretFile =
              allowInsecureClientDisablePkce = false;
              preferShortUsername = false;
            };
          };
        };

      specialisation.removeAttributes.configuration =
        { ... }:
        {
          services.kanidm.provision = lib.mkForce {
            enable = true;
            idmAdminPasswordFile = pkgs.writeText "idm-admin-pw" provisionIdmAdminPassword;

            groups.supergroup1 = { };

            persons.testuser1 = {
              displayName = "Test User (changed)";
            };

            persons.testuser2 = {
              displayName = "Powerful Test User (changed)";
              groups = [ "service1-admin" ];
            };

            groups.service1-access = { };
            groups.service1-admin = { };
            systems.oauth2.service1 = {
              displayName = "Service One (changed)";
              originUrl = "https://changed-one.example.com/";
              originLanding = "https://changed-one.example.com/landing-changed";
              basicSecretFile = pkgs.writeText "bs-service1" "changed-very-strong-secret-for-service1";
              # Removing maps requires setting them to the empty list
              scopeMaps.service1-access = [ ];
              supplementaryScopeMaps.service1-admin = [ ];
            };

            systems.oauth2.service2 = {
              displayName = "Service Two (changed)";
              originUrl = "https://changed-two.example.com/";
              originLanding = "https://changed-landing2.example.com/";
            };
          };
        };

      specialisation.removeEntities.configuration =
        { ... }:
        {
          services.kanidm.provision = lib.mkForce {
            enable = true;
            idmAdminPasswordFile = pkgs.writeText "idm-admin-pw" provisionIdmAdminPassword;
          };
        };

      specialisation.extraJsonFile.configuration =
        { ... }:
        {
          services.kanidm.provision = lib.mkForce {
            enable = true;
            idmAdminPasswordFile = pkgs.writeText "idm-admin-pw" provisionIdmAdminPassword;

            extraJsonFile = pkgs.writeText "extra-json.json" (
              builtins.toJSON {
                persons.testuser2.displayName = "Test User 2";
                groups.testgroup1.members = [ "testuser2" ];
              }
            );

            groups.testgroup1 = { };

            persons.testuser1 = {
              displayName = "Test User 1";
              groups = [ "testgroup1" ];
            };
          };
        };

      security.pki.certificateFiles = [ certs.ca.cert ];

      networking.hosts."::1" = [ serverDomain ];
      networking.firewall.allowedTCPPorts = [ 443 ];

      users.users.kanidm.shell = pkgs.bashInteractive;

      environment.systemPackages = [
        kanidmPackage
        pkgs.openldap
        pkgs.ripgrep
        pkgs.jq
      ];
    };

  testScript =
    { nodes, ... }:
    let
      # We need access to the config file in the test script.
      filteredConfig = pkgs.lib.converge (pkgs.lib.filterAttrsRecursive (
        _: v: v != null
      )) nodes.provision.services.kanidm.serverSettings;
      serverConfigFile = (pkgs.formats.toml { }).generate "server.toml" filteredConfig;

      specialisations = "${nodes.provision.system.build.toplevel}/specialisation";
    in
    ''
      import re

      def assert_contains(haystack, needle):
          if needle not in haystack:
              print("The haystack that will cause the following exception is:")
              print("---")
              print(haystack)
              print("---")
              raise Exception(f"Expected string '{needle}' was not found")

      def assert_matches(haystack, expr):
          if not re.search(expr, haystack):
              print("The haystack that will cause the following exception is:")
              print("---")
              print(haystack)
              print("---")
              raise Exception(f"Expected regex '{expr}' did not match")

      def assert_lacks(haystack, needle):
          if needle in haystack:
              print("The haystack that will cause the following exception is:")
              print("---")
              print(haystack, end="")
              print("---")
              raise Exception(f"Unexpected string '{needle}' was found")

      provision.start()

      def provision_login(pw):
          provision.wait_for_unit("kanidm.service")
          provision.wait_until_succeeds("curl -Lsf https://${serverDomain} | grep Kanidm")
          if pw is None:
              pw = provision.succeed("su - kanidm -c 'kanidmd recover-account -c ${serverConfigFile} idm_admin 2>&1 | rg -o \'[A-Za-z0-9]{48}\' '").strip().removeprefix("'").removesuffix("'")
          out = provision.succeed(f"KANIDM_PASSWORD={pw} kanidm login -D idm_admin")
          assert_contains(out, "Login Success for idm_admin")

      with subtest("Test Provisioning - setup"):
          provision_login(None)
          provision.succeed("kanidm logout -D idm_admin")

      with subtest("Test Provisioning - credentialProvision"):
          provision.succeed('${specialisations}/credentialProvision/bin/switch-to-configuration test')
          provision_login("${provisionIdmAdminPassword}")

          # Make sure neither password is logged
          provision.fail("journalctl --since -10m --unit kanidm.service --grep '${provisionAdminPassword}'")
          provision.fail("journalctl --since -10m --unit kanidm.service --grep '${provisionIdmAdminPassword}'")

          # Test provisioned admin pw
          out = provision.succeed("KANIDM_PASSWORD=${provisionAdminPassword} kanidm login -D admin")
          assert_contains(out, "Login Success for admin")
          provision.succeed("kanidm logout -D admin")
          provision.succeed("kanidm logout -D idm_admin")

      with subtest("Test Provisioning - changedCredential"):
          provision.succeed('${specialisations}/changedCredential/bin/switch-to-configuration test')
          provision_login("${provisionIdmAdminPassword2}")
          provision.succeed("kanidm logout -D idm_admin")

      with subtest("Test Provisioning - addEntities"):
          provision.succeed('${specialisations}/addEntities/bin/switch-to-configuration test')
          # Unspecified idm admin password
          provision_login(None)

          out = provision.succeed("kanidm group get testgroup1")
          assert_contains(out, "name: testgroup1")

          out = provision.succeed("kanidm group get imperative")
          assert_contains(out, "name: imperative")
          assert_contains(out, "member: testuser1")

          out = provision.succeed("kanidm group get supergroup1")
          assert_contains(out, "name: supergroup1")
          assert_contains(out, "member: testgroup1")

          out = provision.succeed("kanidm person get testuser1")
          assert_contains(out, "name: testuser1")
          assert_contains(out, "displayname: Test User")
          assert_contains(out, "legalname: Jane Doe")
          assert_contains(out, "mail: jane.doe@example.com")
          assert_contains(out, "memberof: testgroup1")
          assert_contains(out, "memberof: imperative")
          assert_contains(out, "memberof: service1-access")

          out = provision.succeed("kanidm person get testuser2")
          assert_contains(out, "name: testuser2")
          assert_contains(out, "displayname: Powerful Test User")
          assert_contains(out, "legalname: Ryouiki Tenkai")
          assert_contains(out, "memberof: service1-admin")
          assert_lacks(out, "mail:")

          out = provision.succeed("kanidm group get service1-access")
          assert_contains(out, "name: service1-access")

          out = provision.succeed("kanidm group get service1-admin")
          assert_contains(out, "name: service1-admin")

          out = provision.succeed("kanidm system oauth2 get service1")
          assert_contains(out, "name: service1")
          assert_contains(out, "displayname: Service One")
          assert_contains(out, "oauth2_rs_origin: https://one.example.com/")
          assert_contains(out, "oauth2_rs_origin_landing: https://one.example.com/landing")
          assert_matches(out, 'oauth2_rs_scope_map: service1-access.*{"email", "openid", "profile"}')
          assert_matches(out, 'oauth2_rs_sup_scope_map: service1-admin.*{"admin"}')
          assert_matches(out, 'oauth2_rs_claim_map: groups:.*"admin"')

          out = provision.succeed("kanidm system oauth2 show-basic-secret service1")
          assert_contains(out, "very-strong-secret-for-service1")

          out = provision.succeed("kanidm system oauth2 get service2")
          assert_contains(out, "name: service2")
          assert_contains(out, "displayname: Service Two")
          assert_contains(out, "oauth2_rs_origin: https://two.example.com/")
          assert_contains(out, "oauth2_rs_origin_landing: https://landing2.example.com/")
          assert_contains(out, "oauth2_allow_insecure_client_disable_pkce: true")
          assert_contains(out, "oauth2_prefer_short_username: true")

          provision.succeed("kanidm logout -D idm_admin")

      with subtest("Test Provisioning - changeAttributes"):
          provision.succeed('${specialisations}/changeAttributes/bin/switch-to-configuration test')
          provision_login("${provisionIdmAdminPassword}")

          out = provision.succeed("kanidm group get testgroup1")
          assert_contains(out, "name: testgroup1")

          out = provision.succeed("kanidm group get imperative")
          assert_contains(out, "name: imperative")
          assert_contains(out, "member: testuser1")

          out = provision.succeed("kanidm group get supergroup1")
          assert_contains(out, "name: supergroup1")
          assert_lacks(out, "member: testgroup1")

          out = provision.succeed("kanidm person get testuser1")
          assert_contains(out, "name: testuser1")
          assert_contains(out, "displayname: Test User (changed)")
          assert_contains(out, "legalname: Jane Doe (changed)")
          assert_contains(out, "mail: jane.doe@example.com")
          assert_contains(out, "mail: second.doe@example.com")
          assert_lacks(out, "memberof: testgroup1")
          assert_contains(out, "memberof: imperative")
          assert_contains(out, "memberof: service1-access")

          out = provision.succeed("kanidm person get testuser2")
          assert_contains(out, "name: testuser2")
          assert_contains(out, "displayname: Powerful Test User (changed)")
          assert_contains(out, "legalname: Ryouiki Tenkai (changed)")
          assert_contains(out, "memberof: service1-admin")
          assert_lacks(out, "mail:")

          out = provision.succeed("kanidm group get service1-access")
          assert_contains(out, "name: service1-access")

          out = provision.succeed("kanidm group get service1-admin")
          assert_contains(out, "name: service1-admin")

          out = provision.succeed("kanidm system oauth2 get service1")
          assert_contains(out, "name: service1")
          assert_contains(out, "displayname: Service One (changed)")
          assert_contains(out, "oauth2_rs_origin: https://changed-one.example.com/")
          assert_contains(out, "oauth2_rs_origin: https://changed-one.example.org/")
          assert_contains(out, "oauth2_rs_origin_landing: https://changed-one.example.com/landing")
          assert_matches(out, 'oauth2_rs_scope_map: service1-access.*{"email", "openid"}')
          assert_matches(out, 'oauth2_rs_sup_scope_map: service1-admin.*{"adminchanged"}')
          assert_matches(out, 'oauth2_rs_claim_map: groups:.*"adminchanged"')

          out = provision.succeed("kanidm system oauth2 show-basic-secret service1")
          assert_contains(out, "changed-very-strong-secret-for-service1")

          out = provision.succeed("kanidm system oauth2 get service2")
          assert_contains(out, "name: service2")
          assert_contains(out, "displayname: Service Two (changed)")
          assert_contains(out, "oauth2_rs_origin: https://changed-two.example.com/")
          assert_contains(out, "oauth2_rs_origin_landing: https://changed-landing2.example.com/")
          assert_lacks(out, "oauth2_allow_insecure_client_disable_pkce: true")
          assert_lacks(out, "oauth2_prefer_short_username: true")

          provision.succeed("kanidm logout -D idm_admin")

      with subtest("Test Provisioning - removeAttributes"):
          provision.succeed('${specialisations}/removeAttributes/bin/switch-to-configuration test')
          provision_login("${provisionIdmAdminPassword}")

          out = provision.succeed("kanidm group get testgroup1")
          assert_lacks(out, "name: testgroup1")

          out = provision.succeed("kanidm group get supergroup1")
          assert_contains(out, "name: supergroup1")
          assert_lacks(out, "member: testgroup1")

          out = provision.succeed("kanidm person get testuser1")
          assert_contains(out, "name: testuser1")
          assert_contains(out, "displayname: Test User (changed)")
          assert_lacks(out, "legalname: Jane Doe (changed)")
          assert_lacks(out, "mail: jane.doe@example.com")
          assert_lacks(out, "mail: second.doe@example.com")
          assert_lacks(out, "memberof: testgroup1")
          assert_lacks(out, "memberof: service1-access")

          out = provision.succeed("kanidm person get testuser2")
          assert_contains(out, "name: testuser2")
          assert_contains(out, "displayname: Powerful Test User (changed)")
          assert_lacks(out, "legalname: Ryouiki Tenkai (changed)")
          assert_contains(out, "memberof: service1-admin")
          assert_lacks(out, "mail:")

          out = provision.succeed("kanidm group get service1-access")
          assert_contains(out, "name: service1-access")

          out = provision.succeed("kanidm group get service1-admin")
          assert_contains(out, "name: service1-admin")

          out = provision.succeed("kanidm system oauth2 get service1")
          assert_contains(out, "name: service1")
          assert_contains(out, "displayname: Service One (changed)")
          assert_contains(out, "oauth2_rs_origin: https://changed-one.example.com/")
          assert_lacks(out, "oauth2_rs_origin: https://changed-one.example.org/")
          assert_contains(out, "oauth2_rs_origin_landing: https://changed-one.example.com/landing")
          assert_lacks(out, "oauth2_rs_scope_map")
          assert_lacks(out, "oauth2_rs_sup_scope_map")
          assert_lacks(out, "oauth2_rs_claim_map")

          out = provision.succeed("kanidm system oauth2 show-basic-secret service1")
          assert_contains(out, "changed-very-strong-secret-for-service1")

          out = provision.succeed("kanidm system oauth2 get service2")
          assert_contains(out, "name: service2")
          assert_contains(out, "displayname: Service Two (changed)")
          assert_contains(out, "oauth2_rs_origin: https://changed-two.example.com/")
          assert_contains(out, "oauth2_rs_origin_landing: https://changed-landing2.example.com/")
          assert_lacks(out, "oauth2_allow_insecure_client_disable_pkce: true")
          assert_lacks(out, "oauth2_prefer_short_username: true")

          provision.succeed("kanidm logout -D idm_admin")

      with subtest("Test Provisioning - removeEntities"):
          provision.succeed('${specialisations}/removeEntities/bin/switch-to-configuration test')
          provision_login("${provisionIdmAdminPassword}")

          out = provision.succeed("kanidm group get testgroup1")
          assert_lacks(out, "name: testgroup1")

          out = provision.succeed("kanidm group get supergroup1")
          assert_lacks(out, "name: supergroup1")

          out = provision.succeed("kanidm person get testuser1")
          assert_lacks(out, "name: testuser1")

          out = provision.succeed("kanidm person get testuser2")
          assert_lacks(out, "name: testuser2")

          out = provision.succeed("kanidm group get service1-access")
          assert_lacks(out, "name: service1-access")

          out = provision.succeed("kanidm group get service1-admin")
          assert_lacks(out, "name: service1-admin")

          out = provision.succeed("kanidm system oauth2 get service1")
          assert_lacks(out, "name: service1")

          out = provision.succeed("kanidm system oauth2 get service2")
          assert_lacks(out, "name: service2")

          provision.succeed("kanidm logout -D idm_admin")

      with subtest("Test Provisioning - extraJsonFile"):
          provision.succeed('${specialisations}/extraJsonFile/bin/switch-to-configuration test')
          provision_login("${provisionIdmAdminPassword}")

          out = provision.succeed("kanidm group get testgroup1")
          assert_contains(out, "name: testgroup1")

          out = provision.succeed("kanidm person get testuser1")
          assert_contains(out, "name: testuser1")

          out = provision.succeed("kanidm person get testuser2")
          assert_contains(out, "name: testuser2")

          out = provision.succeed("kanidm group get testgroup1")
          assert_contains(out, "member: testuser1")
          assert_contains(out, "member: testuser2")

          provision.succeed("kanidm logout -D idm_admin")
    '';
}
