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

    # copy certs to store to work around mount namespacing
    certsPath = pkgs.runCommandNoCC "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";
    meta.maintainers = with pkgs.lib.maintainers; [ oddlama ];

    nodes.provision =
      { pkgs, lib, ... }:
      {
        services.kanidm = {
          package = pkgs.kanidm.withSecretProvisioning;
          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 = { };

              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 = { };

              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;
            };
          };

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

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

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

        environment.systemPackages = with pkgs; [
          kanidm
          openldap
          ripgrep
          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}")

            # 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 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: 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 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: 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")
      '';
  }
)