/*
 *
 *  * Copyright 2021  Red Hat, Inc. and/or its affiliates
 *  * and other contributors as indicated by the @author tags.
 *  *
 *  * Licensed under the Apache License, Version 2.0 (the "License");
 *  * you may not use this file except in compliance with the License.
 *  * You may obtain a copy of the License at
 *  *
 *  * http://www.apache.org/licenses/LICENSE-2.0
 *  *
 *  * Unless required by applicable law or agreed to in writing, software
 *  * distributed under the License is distributed on an "AS IS" BASIS,
 *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  * See the License for the specific language governing permissions and
 *  * limitations under the License.
 *
 */

package org.keycloak.testsuite.user.profile;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.keycloak.userprofile.config.UPConfigUtils.ROLE_ADMIN;
import static org.keycloak.userprofile.config.UPConfigUtils.ROLE_USER;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;

import org.junit.Assert;
import org.junit.Test;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.messages.Messages;
import org.keycloak.testsuite.runonserver.RunOnServer;
import org.keycloak.userprofile.AttributeGroupMetadata;
import org.keycloak.userprofile.DeclarativeUserProfileProvider;
import org.keycloak.userprofile.config.UPAttribute;
import org.keycloak.userprofile.config.UPAttributePermissions;
import org.keycloak.userprofile.config.UPAttributeRequired;
import org.keycloak.userprofile.config.UPAttributeSelector;
import org.keycloak.userprofile.config.UPConfig;
import org.keycloak.testsuite.util.ClientScopeBuilder;
import org.keycloak.testsuite.util.KeycloakModelUtils;
import org.keycloak.userprofile.Attributes;
import org.keycloak.userprofile.UserProfile;
import org.keycloak.userprofile.UserProfileContext;
import org.keycloak.userprofile.UserProfileProvider;
import org.keycloak.userprofile.ValidationException;
import org.keycloak.userprofile.config.UPConfigUtils;
import org.keycloak.userprofile.validator.UsernameIDNHomographValidator;
import org.keycloak.util.JsonSerialization;
import org.keycloak.validate.ValidationError;
import org.keycloak.validate.validators.EmailValidator;
import org.keycloak.validate.validators.LengthValidator;

/**
 * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
 */
public class UserProfileTest extends AbstractUserProfileTest {

    protected static final String ATT_ADDRESS = "address";

    @Override
    public void configureTestRealm(RealmRepresentation testRealm) {
        super.configureTestRealm(testRealm);
        testRealm.setClientScopes(new ArrayList<>());
        testRealm.getClientScopes().add(ClientScopeBuilder.create().name("customer").protocol("openid-connect").build());
        testRealm.getClientScopes().add(ClientScopeBuilder.create().name("client-a").protocol("openid-connect").build());
        ClientRepresentation client = KeycloakModelUtils.createClient(testRealm, "client-a");
        client.setDefaultClientScopes(Collections.singletonList("customer"));
        KeycloakModelUtils.createClient(testRealm, "client-b");
    }

    @Test
    public void testIdempotentProfile() {
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testIdempotentProfile);
    }

    private static void testIdempotentProfile(KeycloakSession session) {
        Map<String, Object> attributes = new HashMap<>();
        UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
        UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);

        attributes.put(UserModel.USERNAME, "profiled-user");

        // once created, profile attributes can not be changed
        assertTrue(profile.getAttributes().contains(UserModel.USERNAME));
        assertNull(profile.getAttributes().getFirstValue(UserModel.USERNAME));
    }

    @Test
    public void testCustomAttributeInAnyContext() {
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testCustomAttributeInAnyContext);
    }

    private static void testCustomAttributeInAnyContext(KeycloakSession session) {
        Map<String, Object> attributes = new HashMap<>();

        attributes.put(UserModel.USERNAME, "profiled-user");
        attributes.put(UserModel.FIRST_NAME, "John");
        attributes.put(UserModel.LAST_NAME, "Doe");
        attributes.put(UserModel.EMAIL, org.keycloak.models.utils.KeycloakModelUtils.generateId() + "@keycloak.org");

        UserProfileProvider provider = getUserProfileProvider(session);

        provider.setConfiguration("{\"attributes\": [{\"name\": \"address\", \"required\": {}, \"permissions\": {\"edit\": [\"user\"]}}]}");

        UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);

        try {
            profile.validate();
            Assert.fail("Should fail validation");
        } catch (ValidationException ve) {
            // address is mandatory
            assertTrue(ve.isAttributeOnError("address"));
        }

        containsInAnyOrder(UserModel.USERNAME, UserModel.EMAIL, UserModel.FIRST_NAME, UserModel.LAST_NAME, "address");

        attributes.put("address", "myaddress");

        profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);

        profile.validate();
    }

    @Test
    public void testResolveProfile() {
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testResolveProfile);
    }

    private static void testResolveProfile(KeycloakSession session) {
        configureAuthenticationSession(session);

        Map<String, Object> attributes = new HashMap<>();

        attributes.put(UserModel.USERNAME, "profiled-user");
        attributes.put(UserModel.FIRST_NAME, "John");
        attributes.put(UserModel.LAST_NAME, "Doe");
        attributes.put(UserModel.EMAIL, org.keycloak.models.utils.KeycloakModelUtils.generateId() + "@keycloak.org");

        UserProfileProvider provider = getUserProfileProvider(session);

        provider.setConfiguration("{\"attributes\": [{\"name\": \"business.address\", \"required\": {\"scopes\": [\"customer\"]}, \"permissions\": {\"edit\": [\"user\"]}}]}");

        UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);

        profile.getAttributes();

        try {
            profile.validate();
            Assert.fail("Should fail validation");
        } catch (ValidationException ve) {
            // address is mandatory
            assertTrue(ve.isAttributeOnError("business.address"));
        }

        attributes.put("business.address", "valid-address");
        profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
        profile.validate();

        profile = provider.create(UserProfileContext.ACCOUNT, attributes);
        profile.validate();
    }

    @Test
    public void testValidation() {
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::failValidationWhenEmptyAttributes);
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testAttributeValidation);
    }

    private static void failValidationWhenEmptyAttributes(KeycloakSession session) throws IOException {
        Map<String, Object> attributes = new HashMap<>();
        UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
        provider.setConfiguration(null);
        UserProfile profile;

        try {
            profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
            profile.validate();
            Assert.fail("Should fail validation");
        } catch (ValidationException ve) {
            // username is mandatory
            assertTrue(ve.isAttributeOnError(UserModel.USERNAME));
        }

        RealmModel realm = session.getContext().getRealm();

        try {
            attributes.clear();
            attributes.put(UserModel.EMAIL, "profile-user@keycloak.org");
            profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
            profile.validate();
            Assert.fail("Should fail validation");
        } catch (ValidationException ve) {
            // username is mandatory
            assertTrue(ve.isAttributeOnError(UserModel.USERNAME));
        }

        try {
            realm.setRegistrationEmailAsUsername(true);
            attributes.clear();
            attributes.put(UserModel.FIRST_NAME, "Joe");
            attributes.put(UserModel.LAST_NAME, "Doe");
            attributes.put(UserModel.EMAIL, "profile-user@keycloak.org");
            profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
            profile.validate();
        } catch (ValidationException ve) {
            Assert.fail("Should be OK email as username");
        } finally {
            // we should probably avoid this kind of logic and make the test reset the realm to original state
            realm.setRegistrationEmailAsUsername(false);
        }

        UPConfig config = JsonSerialization.readValue(provider.getConfiguration(), UPConfig.class);

        UPAttribute email = config.getAttribute("email");

        email.setRequired(null);

        provider.setConfiguration(JsonSerialization.writeValueAsString(config));

        attributes.clear();
        attributes.put(UserModel.USERNAME, "profile-user");
        attributes.put(UserModel.FIRST_NAME, "Joe");
        attributes.put(UserModel.LAST_NAME, "Doe");
        provider.create(UserProfileContext.UPDATE_PROFILE, attributes).validate();
    }

    private static void testAttributeValidation(KeycloakSession session) {
        Map<String, Object> attributes = new HashMap<>();
        UserProfileProvider provider = session.getProvider(UserProfileProvider.class);

        UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
        List<ValidationError> errors = new ArrayList<>();

        assertFalse(profile.getAttributes().validate(UserModel.USERNAME, (Consumer<ValidationError>) errors::add));
        assertTrue(containsErrorMessage(errors, Messages.MISSING_USERNAME));

        errors.clear();
        attributes.clear();
        attributes.put(UserModel.EMAIL, "invalid");
        profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
        assertFalse(profile.getAttributes().validate(UserModel.EMAIL, (Consumer<ValidationError>) errors::add));
        assertTrue(containsErrorMessage(errors, EmailValidator.MESSAGE_INVALID_EMAIL));
    }
    
    private static boolean containsErrorMessage(List<ValidationError> errors, String message){
    	for(ValidationError err : errors) {
    		if(err.getMessage().equals(message)) {
    			return true;
    		}
    	}
    	return false;
    }
    

    @Test
    public void testValidateComplianceWithUserProfile() {
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testValidateComplianceWithUserProfile);
    }

    private static void testValidateComplianceWithUserProfile(KeycloakSession session) throws IOException {
        RealmModel realm = session.getContext().getRealm();
        UserModel user = session.users().addUser(realm, "profiled-user");
        UserProfileProvider provider = getUserProfileProvider(session);

        UPConfig config = new UPConfig();
        UPAttribute attribute = new UPAttribute();

        attribute.setName("address");

        UPAttributeRequired requirements = new UPAttributeRequired();

        attribute.setRequired(requirements);

        UPAttributePermissions permissions = new UPAttributePermissions();
        permissions.setEdit(Collections.singleton(ROLE_USER));
        attribute.setPermissions(permissions);

        config.addAttribute(attribute);

        provider.setConfiguration(JsonSerialization.writeValueAsString(config));

        UserProfile profile = provider.create(UserProfileContext.ACCOUNT, user);

        try {
            profile.validate();
            Assert.fail("Should fail validation");
        } catch (ValidationException ve) {
            // username is mandatory
            assertTrue(ve.isAttributeOnError("address"));
        }

        user.setSingleAttribute(UserModel.FIRST_NAME, "john");
        user.setSingleAttribute(UserModel.LAST_NAME, "doe");
        user.setSingleAttribute(UserModel.EMAIL, "jd@keycloak.org");
        user.setAttribute("address", Arrays.asList("fixed-address"));

        profile = provider.create(UserProfileContext.ACCOUNT, user);

        profile.validate();
    }

    @Test
    public void testGetProfileAttributes() {
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testGetProfileAttributes);
    }

    private static void testGetProfileAttributes(KeycloakSession session) {
        RealmModel realm = session.getContext().getRealm();
        UserModel user = session.users().addUser(realm, org.keycloak.models.utils.KeycloakModelUtils.generateId());
        user.setFirstName("John");
        user.setLastName("John");
        user.setEmail(org.keycloak.models.utils.KeycloakModelUtils.generateId() + "@keycloak.org");
        UserProfileProvider provider = getUserProfileProvider(session);

        provider.setConfiguration("{\"attributes\": [{\"name\": \"address\", \"required\": {}, \"permissions\": {\"edit\": [\"user\"]}}]}");

        UserProfile profile = provider.create(UserProfileContext.ACCOUNT, user);
        Attributes attributes = profile.getAttributes();

        assertThat(attributes.nameSet(),
                containsInAnyOrder(UserModel.USERNAME, UserModel.EMAIL, UserModel.FIRST_NAME, UserModel.LAST_NAME, UserModel.LOCALE, "address"));

        try {
            profile.validate();
            Assert.fail("Should fail validation");
        } catch (ValidationException ve) {
            // username is mandatory
            assertTrue(ve.isAttributeOnError("address"));
        }

        assertNotNull(attributes.getFirstValue(UserModel.USERNAME));
        assertNotNull(attributes.getFirstValue(UserModel.EMAIL));
        assertNotNull(attributes.getFirstValue(UserModel.FIRST_NAME));
        assertNotNull(attributes.getFirstValue(UserModel.LAST_NAME));
        assertNull(attributes.getFirstValue("address"));

        user.setAttribute("address", Arrays.asList("fixed-address"));

        profile = provider.create(UserProfileContext.ACCOUNT, user);
        attributes = profile.getAttributes();

        profile.validate();

        assertNotNull(attributes.getFirstValue("address"));
    }

    @Test
    public void testGetProfileAttributeGroups() {
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testGetProfileAttributeGroups);
    }

    private static void testGetProfileAttributeGroups(KeycloakSession session) {
        RealmModel realm = session.getContext().getRealm();
        UserModel user = session.users().addUser(realm, org.keycloak.models.utils.KeycloakModelUtils.generateId());
        UserProfileProvider provider = getUserProfileProvider(session);

        String configuration = "{\n" +
                "  \"attributes\": [\n" +
                "    {\n" +
                "      \"name\": \"address\",\n" +
                "      \"group\": \"companyaddress\"\n" +
                "    },\n" +
                "    {\n" +
                "      \"name\": \"second\",\n" +
                "      \"group\": \"groupwithanno" + "\"\n" +
                "    }\n" +
                "  ],\n" +
                "  \"groups\": [\n" +
                "    {\n" +
                "      \"name\": \"companyaddress\",\n" +
                "      \"displayHeader\": \"header\",\n" +
                "      \"displayDescription\": \"description\"\n" +
                "    },\n" +
                "    {\n" +
                "      \"name\": \"groupwithanno\",\n" +
                "      \"annotations\": {\n" +
                "        \"anno1\": \"value1\",\n" +
                "        \"anno2\": \"value2\"\n" +
                "      }\n" +
                "    }\n" +
                "  ]\n" +
                "}\n";
        provider.setConfiguration(configuration);

        UserProfile profile = provider.create(UserProfileContext.ACCOUNT, user);
        Attributes attributes = profile.getAttributes();

        assertThat(attributes.nameSet(),
                containsInAnyOrder(UserModel.USERNAME, UserModel.EMAIL, UserModel.FIRST_NAME, UserModel.LAST_NAME, UserModel.LOCALE, "address", "second"));
        
        
        AttributeGroupMetadata companyAddressGroup = attributes.getMetadata("address").getAttributeGroupMetadata();
        assertEquals("companyaddress", companyAddressGroup.getName());
        assertEquals("header", companyAddressGroup.getDisplayHeader());
        assertEquals("description", companyAddressGroup.getDisplayDescription());
        assertNull(companyAddressGroup.getAnnotations());
        
        AttributeGroupMetadata groupwithannoGroup = attributes.getMetadata("second").getAttributeGroupMetadata();
        assertEquals("groupwithanno", groupwithannoGroup.getName());
        assertNull(groupwithannoGroup.getDisplayHeader());
        assertNull(groupwithannoGroup.getDisplayDescription());
        Map<String, Object> annotations = groupwithannoGroup.getAnnotations();
        assertEquals(2, annotations.size());
        assertEquals("value1", annotations.get("anno1"));
        assertEquals("value2", annotations.get("anno2"));
    }

    @Test
    public void testCreateAndUpdateUser() {
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testCreateAndUpdateUser);
    }

    private static void testCreateAndUpdateUser(KeycloakSession session) throws IOException {
        UserProfileProvider provider = getUserProfileProvider(session);

        UPConfig config = JsonSerialization.readValue(provider.getConfiguration(), UPConfig.class);
        UPAttribute attribute = new UPAttribute();
        attribute.setName("address");
        UPAttributePermissions permissions = new UPAttributePermissions();
        permissions.setEdit(new HashSet<>(Arrays.asList("admin", "user")));
        attribute.setPermissions(permissions);
        config.addAttribute(attribute);

        attribute = new UPAttribute();
        attribute.setName("business.address");
        permissions = new UPAttributePermissions();
        permissions.setEdit(new HashSet<>(Arrays.asList("admin", "user")));
        attribute.setPermissions(permissions);
        config.addAttribute(attribute);

        provider.setConfiguration(JsonSerialization.writeValueAsString(config));

        Map<String, Object> attributes = new HashMap<>();
        String userName = org.keycloak.models.utils.KeycloakModelUtils.generateId();

        attributes.put(UserModel.USERNAME, userName);
        attributes.put(UserModel.EMAIL, "user@keycloak.org");
        attributes.put(UserModel.FIRST_NAME, "Joe");
        attributes.put(UserModel.LAST_NAME, "Doe");
        attributes.put("address", "fixed-address");

        UserProfile profile = provider.create(UserProfileContext.ACCOUNT, attributes);
        UserModel user = profile.create();

        assertEquals(userName, user.getUsername());
        assertEquals("fixed-address", user.getFirstAttribute("address"));

        attributes.put(UserModel.FIRST_NAME, "Alice");
        attributes.put(UserModel.LAST_NAME, "In Chains");
        attributes.put(UserModel.EMAIL, "alice@keycloak.org");

        profile = provider.create(UserProfileContext.ACCOUNT, attributes, user);
        Set<String> attributesUpdated = new HashSet<>();
        Map<String, String> attributesUpdatedOldValues = new HashMap<>();
        attributesUpdatedOldValues.put(UserModel.FIRST_NAME, "Joe");
        attributesUpdatedOldValues.put(UserModel.LAST_NAME, "Doe");
        attributesUpdatedOldValues.put(UserModel.EMAIL, "user@keycloak.org");
        
        profile.update((attributeName, userModel, oldValue) -> {
            assertTrue(attributesUpdated.add(attributeName)); 
            assertEquals(attributesUpdatedOldValues.get(attributeName), getSingleValue(oldValue));
            assertEquals(attributes.get(attributeName), userModel.getFirstAttribute(attributeName));
            });

        assertThat(attributesUpdated, containsInAnyOrder(UserModel.FIRST_NAME, UserModel.LAST_NAME, UserModel.EMAIL));

        configureAuthenticationSession(session);

        attributes.put("business.address", "fixed-business-address");
        profile = provider.create(UserProfileContext.ACCOUNT, attributes, user);

        attributesUpdated.clear();
        profile.update((attributeName, userModel, oldValue) -> assertTrue(attributesUpdated.add(attributeName)));

        assertThat(attributesUpdated, containsInAnyOrder("business.address"));

        assertEquals("fixed-business-address", user.getFirstAttribute("business.address"));
    }
    
    private static String getSingleValue(List<String> vals) {
        if(vals==null || vals.isEmpty())
            return null;
        return vals.get(0);
    }
    
    @Test
    public void testReadonlyUpdates() {
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testReadonlyUpdates);
    }

    private static void testReadonlyUpdates(KeycloakSession session) {
        Map<String, Object> attributes = new HashMap<>();

        attributes.put(UserModel.USERNAME, org.keycloak.models.utils.KeycloakModelUtils.generateId());
        attributes.put(UserModel.FIRST_NAME, "John");
        attributes.put(UserModel.LAST_NAME, "Doe");
        attributes.put(UserModel.EMAIL, org.keycloak.models.utils.KeycloakModelUtils.generateId() + "@keycloak.org");
        attributes.put("address", Arrays.asList("fixed-address"));
        attributes.put("department", Arrays.asList("sales"));

        UserProfileProvider provider = getUserProfileProvider(session);

        provider.setConfiguration("{\"attributes\": [{\"name\": \"department\", \"permissions\": {\"edit\": [\"admin\"]}}]}");

        UserProfile profile = provider.create(UserProfileContext.ACCOUNT, attributes);
        UserModel user = profile.create();

        assertThat(profile.getAttributes().nameSet(),
                containsInAnyOrder(UserModel.USERNAME, UserModel.EMAIL, UserModel.FIRST_NAME, UserModel.LAST_NAME, UserModel.LOCALE, "department"));

        assertNull(user.getFirstAttribute("department"));

        profile = provider.create(UserProfileContext.USER_API, attributes, user);

        Set<String> attributesUpdated = new HashSet<>();

        profile.update((attributeName, userModel, oldValue) -> assertTrue(attributesUpdated.add(attributeName)));

        assertThat(attributesUpdated, containsInAnyOrder("department"));

        assertEquals("sales", user.getFirstAttribute("department"));

        attributes.put("department", "cannot-change");

        profile = provider.create(UserProfileContext.ACCOUNT, attributes, user);

        try {
            profile.update();
            fail("Should fail due to read only attribute");
        } catch (ValidationException ve) {
            assertTrue(ve.isAttributeOnError("department"));
        }

        assertEquals("sales", user.getFirstAttribute("department"));

        assertTrue(profile.getAttributes().isReadOnly("department"));
    }

    @Test
    public void testReadonlyEmailCannotBeUpdated() {
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testReadonlyEmailCannotBeUpdated);
    }

    private static void testReadonlyEmailCannotBeUpdated(KeycloakSession session) {
        Map<String, Object> attributes = new HashMap<>();

        attributes.put(UserModel.USERNAME, org.keycloak.models.utils.KeycloakModelUtils.generateId());
        attributes.put(UserModel.FIRST_NAME, "John");
        attributes.put(UserModel.LAST_NAME, "Doe");
        attributes.put(UserModel.EMAIL, "readonly@foo.bar");

        UserProfileProvider provider = getUserProfileProvider(session);

        // configure email r/o for user
        provider.setConfiguration("{\"attributes\": [{\"name\": \"email\", \"permissions\": {\"edit\": [ \"admin\"]}}]}");

        UserProfile profile = provider.create(UserProfileContext.ACCOUNT, attributes);
        UserModel user = profile.create();

        assertThat(profile.getAttributes().nameSet(),
                containsInAnyOrder(UserModel.USERNAME, UserModel.EMAIL, UserModel.FIRST_NAME, UserModel.LAST_NAME, UserModel.LOCALE));

        profile = provider.create(UserProfileContext.USER_API, attributes, user);

        Set<String> attributesUpdated = new HashSet<>();

        profile.update((attributeName, userModel, oldValue) -> assertTrue(attributesUpdated.add(attributeName)));

        attributes.put(UserModel.EMAIL, "cannot-change@foo.bar");

        profile = provider.create(UserProfileContext.ACCOUNT, attributes, user);

        try {
            profile.update();
            fail("Should fail since email is read only");
        } catch (ValidationException ve) {
            assertTrue(ve.isAttributeOnError("email"));
        }

        assertEquals("E-Mail address shouldn't be changed", "readonly@foo.bar", user.getEmail());
    }

    @Test
    public void testUpdateEmail() {
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testUpdateEmail);
    }

    private static void testUpdateEmail(KeycloakSession session) {
        Map<String, Object> attributes = new HashMap<>();

        attributes.put(UserModel.USERNAME, org.keycloak.models.utils.KeycloakModelUtils.generateId());
        attributes.put(UserModel.FIRST_NAME, "John");
        attributes.put(UserModel.LAST_NAME, "Doe");
        attributes.put(UserModel.EMAIL, "canchange@foo.bar");

        UserProfileProvider provider = getUserProfileProvider(session);

        // configure email r/w for user
        provider.setConfiguration("{\"attributes\": [{\"name\": \"email\", \"permissions\": {\"edit\": [ \"user\", \"admin\"]}}]}");

        UserProfile profile = provider.create(UserProfileContext.ACCOUNT, attributes);
        UserModel user = profile.create();

        assertThat(profile.getAttributes().nameSet(),
                containsInAnyOrder(UserModel.USERNAME, UserModel.EMAIL, UserModel.FIRST_NAME, UserModel.LAST_NAME, UserModel.LOCALE));

        profile = provider.create(UserProfileContext.USER_API, attributes, user);

        Set<String> attributesUpdated = new HashSet<>();

        profile.update((attributeName, userModel, oldValue) -> assertTrue(attributesUpdated.add(attributeName)));

        attributes.put("email", "changed@foo.bar");

        profile = provider.create(UserProfileContext.ACCOUNT, attributes, user);

        profile.update();

        assertEquals("E-Mail address should have been changed!", "changed@foo.bar", user.getEmail());
    }

    @Test
    public void testDoNotUpdateUndefinedAttributes() {
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testDoNotUpdateUndefinedAttributes);
    }

    private static void testDoNotUpdateUndefinedAttributes(KeycloakSession session) {
        Map<String, Object> attributes = new HashMap<>();

        attributes.put(UserModel.USERNAME, org.keycloak.models.utils.KeycloakModelUtils.generateId());
        attributes.put(UserModel.FIRST_NAME, "John");
        attributes.put(UserModel.LAST_NAME, "Doe");
        attributes.put(UserModel.EMAIL, org.keycloak.models.utils.KeycloakModelUtils.generateId() + "@keycloak.org");
        attributes.put("address", Arrays.asList("fixed-address"));
        attributes.put("department", Arrays.asList("sales"));
        attributes.put("phone", Arrays.asList("fixed-phone"));

        UserProfileProvider provider = getUserProfileProvider(session);

        provider.setConfiguration("{\"attributes\": [{\"name\": \"department\", \"permissions\": {\"edit\": [\"admin\"]}},"
                + "{\"name\": \"phone\", \"permissions\": {\"edit\": [\"admin\"]}},"
                + "{\"name\": \"address\", \"permissions\": {\"edit\": [\"admin\"]}}]}");
        UserProfile profile = provider.create(UserProfileContext.ACCOUNT, attributes);
        UserModel user = profile.create();
        assertThat(profile.getAttributes().nameSet(),
                containsInAnyOrder(UserModel.USERNAME, UserModel.EMAIL, UserModel.FIRST_NAME, UserModel.LAST_NAME, UserModel.LOCALE, "address", "department", "phone"));

        attributes.put("address", Arrays.asList("change-address"));
        attributes.put("department", Arrays.asList("changed-sales"));
        attributes.put("phone", Arrays.asList("changed-phone"));
        profile = provider.create(UserProfileContext.USER_API, attributes, user);
        Set<String> attributesUpdated = new HashSet<>();
        profile.update((attributeName, userModel, oldValue) -> assertTrue(attributesUpdated.add(attributeName)));
        assertThat(attributesUpdated, containsInAnyOrder("department", "address", "phone"));

        provider.setConfiguration("{\"attributes\": [{\"name\": \"department\", \"permissions\": {\"edit\": [\"admin\"]}},"
                + "{\"name\": \"phone\", \"permissions\": {\"edit\": [\"admin\"]}}]}");
        attributesUpdated.clear();
        attributes.remove("address");
        attributes.put("department", "foo");
        attributes.put("phone", "foo");
        profile = provider.create(UserProfileContext.USER_API, attributes, user);
        profile.update((attributeName, userModel, oldValue) -> assertTrue(attributesUpdated.add(attributeName)));
        assertThat(attributesUpdated, containsInAnyOrder("department", "phone"));
        assertTrue(user.getAttributes().containsKey("address"));

        provider.setConfiguration("{\"attributes\": [{\"name\": \"department\", \"permissions\": {\"edit\": [\"admin\"]}},"
                + "{\"name\": \"phone\", \"permissions\": {\"edit\": [\"admin\"]}},"
                + "{\"name\": \"address\", \"permissions\": {\"edit\": [\"admin\"]}}]}");
        attributes.put("department", "foo");
        attributes.put("phone", "foo");
        attributes.put("address", "bar");
        attributesUpdated.clear();
        profile = provider.create(UserProfileContext.USER_API, attributes, user);
        profile.update((attributeName, userModel, oldValue) -> assertTrue(attributesUpdated.add(attributeName)));
        assertThat(attributesUpdated, containsInAnyOrder("address"));
        assertEquals("bar", user.getFirstAttribute("address"));
        assertEquals("foo", user.getFirstAttribute("phone"));
        assertEquals("foo", user.getFirstAttribute("department"));

        attributes.remove("address");
        attributesUpdated.clear();
        profile = provider.create(UserProfileContext.USER_API, attributes, user);
        profile.update((attributeName, userModel, oldValue) -> assertTrue(attributesUpdated.add(attributeName)));
        assertThat(attributesUpdated, containsInAnyOrder("address"));
        assertFalse(user.getAttributes().containsKey("address"));
        assertTrue(user.getAttributes().containsKey("phone"));
        assertTrue(user.getAttributes().containsKey("department"));

        String prefixedAttributeName = Constants.USER_ATTRIBUTES_PREFIX.concat("prefixed");
        attributes.put(prefixedAttributeName, "foo");
        attributesUpdated.clear();
        profile = provider.create(UserProfileContext.USER_API, attributes, user);
        profile.update((attributeName, userModel, oldValue) -> assertTrue(attributesUpdated.add(attributeName)));
        assertTrue(attributesUpdated.isEmpty());
        assertFalse(user.getAttributes().containsKey("prefixedAttributeName"));
    }

    @Test
    public void testComponentModelId() {
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testComponentModelId);
    }

    private static void testComponentModelId(KeycloakSession session) {
        setDefaultConfiguration(session);
        Optional<ComponentModel> component = getComponentModel(session);
        assertTrue(component.isPresent());
        assertEquals("declarative-user-profile", component.get().getProviderId());
    }

    @Test
    public void testInvalidConfiguration() {
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testInvalidConfiguration);
    }

    private static void testInvalidConfiguration(KeycloakSession session) {
        UserProfileProvider provider = getUserProfileProvider(session);

        try {
            provider.setConfiguration("{\"validateConfigAttribute\": true}");
            fail("Should fail validation");
        } catch (ComponentValidationException ve) {
            // OK
        }

    }

    @Test
    public void testConfigurationChunks() {
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testConfigurationChunks);
    }

    private static void testConfigurationChunks(KeycloakSession session) throws IOException {
        ComponentModel component = setAndGetDefaultConfiguration(session).orElse(null);
        assertNotNull(component);

        String newConfig = generateLargeProfileConfig();
        UserProfileProvider provider = getUserProfileProvider(session);

        provider.setConfiguration(newConfig);
        component = getComponentModel(session).orElse(null);

        // assert config is persisted in 2 pieces
        Assert.assertEquals("2", component.get(DeclarativeUserProfileProvider.UP_PIECES_COUNT_COMPONENT_CONFIG_KEY));
        // assert config is returned correctly
        Assert.assertEquals(newConfig, provider.getConfiguration());
    }

    @Test
    public void testResetConfiguration() {
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testResetConfiguration);
    }

    private static void testResetConfiguration(KeycloakSession session) throws IOException {
        setConfiguration(session, null);
        assertFalse(getComponentModel(session).isPresent());
    }

    @Test
    public void testDefaultConfig() {
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testDefaultConfig);
    }

    private static void testDefaultConfig(KeycloakSession session) {
        UserProfileProvider provider = getUserProfileProvider(session);

        // reset configuration to default
        provider.setConfiguration(null);

        // failed required validations
        UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, Collections.emptyMap());

        try {
            profile.validate();
            fail("Should fail validation");
        } catch (ValidationException ve) {
            assertTrue(ve.isAttributeOnError(UserModel.USERNAME));
        }

        // failed for blank values also
        Map<String, Object> attributes = new HashMap<>();

        attributes.put(UserModel.FIRST_NAME, "");
        attributes.put(UserModel.LAST_NAME, " ");
        attributes.put(UserModel.EMAIL, "");

        profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);

        try {
            profile.validate();
            fail("Should fail validation");
        } catch (ValidationException ve) {
            assertTrue(ve.isAttributeOnError(UserModel.USERNAME));
            assertTrue(ve.isAttributeOnError(UserModel.FIRST_NAME));
            assertTrue(ve.isAttributeOnError(UserModel.LAST_NAME));
            assertTrue(ve.isAttributeOnError(UserModel.EMAIL));
        }

        // all OK
        attributes.put(UserModel.USERNAME, "jdoeusername");
        attributes.put(UserModel.FIRST_NAME, "John");
        attributes.put(UserModel.LAST_NAME, "Doe");
        attributes.put(UserModel.EMAIL, "jdoe@acme.org");

        profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
        profile.validate();
    }

    @Test
    public void testCustomValidationForUsername() {
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testCustomValidationForUsername);
    }

    private static void testCustomValidationForUsername(KeycloakSession session) throws IOException {
        UPConfig config = new UPConfig();
        UPAttribute attribute = new UPAttribute();

        attribute.setName(UserModel.USERNAME);

        Map<String, Object> validatorConfig = new HashMap<>();

        validatorConfig.put("min", 4);

        attribute.addValidation(LengthValidator.ID, validatorConfig);

        config.addAttribute(attribute);

        UserProfileProvider provider = getUserProfileProvider(session);
        provider.setConfiguration(JsonSerialization.writeValueAsString(config));

        Map<String, Object> attributes = new HashMap<>();

        attributes.put(UserModel.USERNAME, "us");
        attributes.put(UserModel.FIRST_NAME, "John");
        attributes.put(UserModel.LAST_NAME, "Doe");
        attributes.put(UserModel.EMAIL, org.keycloak.models.utils.KeycloakModelUtils.generateId() + "@keycloak.org");

        UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);

        try {
            profile.validate();
            fail("Should fail validation");
        } catch (ValidationException ve) {
            assertTrue(ve.isAttributeOnError(UserModel.USERNAME));
            assertTrue(ve.hasError(LengthValidator.MESSAGE_INVALID_LENGTH_TOO_SHORT));
        }

        attributes.put(UserModel.USERNAME, "user");

        profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);

        profile.validate();

        provider.setConfiguration(null);

        attributes.put(UserModel.USERNAME, "user");
        attributes.put(UserModel.EMAIL, "user@keycloak.org");
        attributes.put(UserModel.FIRST_NAME, "Joe");
        attributes.put(UserModel.LAST_NAME, "Doe");

        profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);

        profile.validate();
    }

    @Test
    public void testRemoveDefaultValidationFromUsername() {
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testRemoveDefaultValidationFromUsername);
    }

    private static void testRemoveDefaultValidationFromUsername(KeycloakSession session) throws IOException {
        UserProfileProvider provider = getUserProfileProvider(session);

        // reset configuration to default
        provider.setConfiguration(null);

        Map<String, Object> attributes = new HashMap<>();

        attributes.put(UserModel.USERNAME, "你好世界");
        attributes.put(UserModel.EMAIL, "test@keycloak.org");
        attributes.put(UserModel.FIRST_NAME, "Foo");
        attributes.put(UserModel.LAST_NAME, "Bar");

        UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);

        try {
            profile.validate();
            fail("Should fail validation");
        } catch (ValidationException ve) {
            assertTrue(ve.hasError(Messages.INVALID_USERNAME));
        }

        UPConfig config = UPConfigUtils.readConfig(new ByteArrayInputStream(provider.getConfiguration().getBytes()));

        for (UPAttribute attribute : config.getAttributes()) {
            if (UserModel.USERNAME.equals(attribute.getName())) {
                attribute.getValidations().remove(UsernameIDNHomographValidator.ID);
                break;
            }
        }

        provider.setConfiguration(JsonSerialization.writeValueAsString(config));

        profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);

        profile.validate();
    }

    @Test
    public void testOptionalAttributes() {
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testOptionalAttributes);
    }

    private static void testOptionalAttributes(KeycloakSession session) throws IOException {
        UserProfileProvider provider = getUserProfileProvider(session);
        UPConfig config = new UPConfig();
        UPAttribute attribute = new UPAttribute();
        attribute.setName(UserModel.FIRST_NAME);
        Map<String, Object> validatorConfig = new HashMap<>();
        validatorConfig.put(LengthValidator.KEY_MAX, 4);
        attribute.addValidation(LengthValidator.ID, validatorConfig);
        config.addAttribute(attribute);

        attribute = new UPAttribute();
        attribute.setName(UserModel.LAST_NAME);
        attribute.addValidation(LengthValidator.ID, validatorConfig);
        config.addAttribute(attribute);

        provider.setConfiguration(JsonSerialization.writeValueAsString(config));

        Map<String, Object> attributes = new HashMap<>();

        attributes.put(UserModel.USERNAME, "user");
        attributes.put(UserModel.FIRST_NAME, "John");
        attributes.put(UserModel.LAST_NAME, "Doe");
        attributes.put(UserModel.EMAIL, org.keycloak.models.utils.KeycloakModelUtils.generateId() + "@keycloak.org");

        // not present attributes are OK
        UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
        profile.validate();

        //empty attributes are OK
        attributes.put(UserModel.FIRST_NAME, "");
        attributes.put(UserModel.LAST_NAME, "");
        profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
        profile.validate();

        //filled attributes are OK
        attributes.put(UserModel.FIRST_NAME, "John");
        attributes.put(UserModel.LAST_NAME, "Doe");
        profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
        profile.validate();

        // fails due to additional length validation so it is executed correctly
        attributes.put(UserModel.FIRST_NAME, "JohnTooLong");
        attributes.put(UserModel.LAST_NAME, "DoeTooLong");
        profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
        try {
            profile.validate();
            fail("Should fail validation");
        } catch (ValidationException ve) {
            assertTrue(ve.isAttributeOnError(UserModel.FIRST_NAME));
            assertTrue(ve.isAttributeOnError(UserModel.LAST_NAME));
        }
    }

    @Test
    public void testCustomAttributeRequired() {
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testCustomAttributeRequired);
    }

    private static void testCustomAttributeRequired(KeycloakSession session) throws IOException {
        UserProfileProvider provider = getUserProfileProvider(session);
        UPConfig config = new UPConfig();
        UPAttribute attribute = new UPAttribute();

        attribute.setName(ATT_ADDRESS);

        Map<String, Object> validatorConfig = new HashMap<>();

        validatorConfig.put(LengthValidator.KEY_MIN, 4);

        attribute.addValidation(LengthValidator.ID, validatorConfig);

        // make it ALWAYS required
        UPAttributeRequired requirements = new UPAttributeRequired();
        attribute.setRequired(requirements);

        UPAttributePermissions permissions = new UPAttributePermissions();
        permissions.setEdit(Collections.singleton(ROLE_USER));
        attribute.setPermissions(permissions);

        config.addAttribute(attribute);

        provider.setConfiguration(JsonSerialization.writeValueAsString(config));

        Map<String, Object> attributes = new HashMap<>();

        attributes.put(UserModel.USERNAME, "user");
        attributes.put(UserModel.FIRST_NAME, "John");
        attributes.put(UserModel.LAST_NAME, "Doe");
        attributes.put(UserModel.EMAIL, org.keycloak.models.utils.KeycloakModelUtils.generateId() + "@keycloak.org");

        UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);

        // fails on required validation
        try {
            profile.validate();
            fail("Should fail validation");
        } catch (ValidationException ve) {
            assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
        }

        // fails on length validation
        attributes.put(ATT_ADDRESS, "adr");
        profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
        try {
            profile.validate();
            fail("Should fail validation");
        } catch (ValidationException ve) {
            assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
        }

        // all OK
        attributes.put(ATT_ADDRESS, "adress ok");
        attributes.put(UserModel.FIRST_NAME, "Joe");
        attributes.put(UserModel.LAST_NAME, "Doe");

        profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
        profile.validate();
    }

    @Test
    public void testCustomAttributeOptional() {
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testCustomAttributeOptional);
    }

    private static void testCustomAttributeOptional(KeycloakSession session) throws IOException {
        UserProfileProvider provider = getUserProfileProvider(session);
        UPConfig config = new UPConfig();
        UPAttribute attribute = new UPAttribute();

        attribute.setName(ATT_ADDRESS);

        Map<String, Object> validatorConfig = new HashMap<>();
        validatorConfig.put(LengthValidator.KEY_MIN, 4);
        attribute.addValidation(LengthValidator.ID, validatorConfig);

        config.addAttribute(attribute);

        provider.setConfiguration(JsonSerialization.writeValueAsString(config));

        Map<String, Object> attributes = new HashMap<>();
        attributes.put(UserModel.USERNAME, "user");
        attributes.put(UserModel.FIRST_NAME, "John");
        attributes.put(UserModel.LAST_NAME, "Doe");
        attributes.put(UserModel.EMAIL, org.keycloak.models.utils.KeycloakModelUtils.generateId() + "@keycloak.org");

        // null is OK as attribute is optional
        UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
        profile.validate();

        //blank String have to be OK as it is what UI forms send for not filled in optional attributes
        attributes.put(ATT_ADDRESS, "");
        profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
        profile.validate();

        // fails on length validation
        attributes.put(ATT_ADDRESS, "adr");
        profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
        try {
            profile.validate();
            fail("Should fail validation");
        } catch (ValidationException ve) {
            assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
        }

        // all OK
        attributes.put(ATT_ADDRESS, "adress ok");
        profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
        profile.validate();

    }

    @Test
    public void testRequiredIfUser() {
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testRequiredIfUser);
    }

    private static void testRequiredIfUser(KeycloakSession session) throws IOException {
        UserProfileProvider provider = getUserProfileProvider(session);
        UPConfig config = new UPConfig();
        UPAttribute attribute = new UPAttribute();

        attribute.setName(ATT_ADDRESS);

        UPAttributeRequired requirements = new UPAttributeRequired();

        requirements.setRoles(Collections.singleton(ROLE_USER));

        attribute.setRequired(requirements);

        UPAttributePermissions permissions = new UPAttributePermissions();
        permissions.setEdit(Collections.singleton(ROLE_USER));
        attribute.setPermissions(permissions);

        config.addAttribute(attribute);

        provider.setConfiguration(JsonSerialization.writeValueAsString(config));

        Map<String, Object> attributes = new HashMap<>();

        attributes.put(UserModel.USERNAME, "user");

        // fail on common contexts
        UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
        try {
            profile.validate();
            fail("Should fail validation");
        } catch (ValidationException ve) {
            assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
        }

        profile = provider.create(UserProfileContext.ACCOUNT, attributes);
        try {
            profile.validate();
            fail("Should fail validation");
        } catch (ValidationException ve) {
            assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
        }

        profile = provider.create(UserProfileContext.REGISTRATION_PROFILE, attributes);
        try {
            profile.validate();
            fail("Should fail validation");
        } catch (ValidationException ve) {
            assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
        }

        attributes.put(UserModel.FIRST_NAME, "Joe");
        attributes.put(UserModel.LAST_NAME, "Doe");

        // no fail on User API
        profile = provider.create(UserProfileContext.USER_API, attributes);
        profile.validate();
    }

    @Test
    public void testRequiredIfAdmin() {
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testRequiredIfAdmin);
    }

    private static void testRequiredIfAdmin(KeycloakSession session) throws IOException {
        UserProfileProvider provider = getUserProfileProvider(session);
        UPConfig config = new UPConfig();
        UPAttribute attribute = new UPAttribute();

        attribute.setName(ATT_ADDRESS);

        UPAttributeRequired requirements = new UPAttributeRequired();

        requirements.setRoles(Collections.singleton(ROLE_ADMIN));

        attribute.setRequired(requirements);

        UPAttributePermissions permissions = new UPAttributePermissions();
        permissions.setEdit(Collections.singleton(UPConfigUtils.ROLE_ADMIN));
        attribute.setPermissions(permissions);

        config.addAttribute(attribute);

        provider.setConfiguration(JsonSerialization.writeValueAsString(config));

        Map<String, Object> attributes = new HashMap<>();

        attributes.put(UserModel.USERNAME, "user");
        attributes.put(UserModel.FIRST_NAME, "John");
        attributes.put(UserModel.LAST_NAME, "Doe");
        attributes.put(UserModel.EMAIL, org.keycloak.models.utils.KeycloakModelUtils.generateId() + "@keycloak.org");

        // NO fail on common contexts
        UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
        profile.validate();

        profile = provider.create(UserProfileContext.ACCOUNT, attributes);
        profile.validate();

        profile = provider.create(UserProfileContext.REGISTRATION_PROFILE, attributes);
        profile.validate();

        // fail on User API
        try {
            profile = provider.create(UserProfileContext.USER_API, attributes);
            profile.validate();
            fail("Should fail validation");
        } catch (ValidationException ve) {
            assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
        }

    }

    @Test
    public void testNoValidationsIfUserReadOnly() {
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testNoValidationsIfUserReadOnly);
    }

    private static void testNoValidationsIfUserReadOnly(KeycloakSession session) throws IOException {
        UserProfileProvider provider = getUserProfileProvider(session);
        UPConfig config = new UPConfig();
        UPAttribute attribute = new UPAttribute();

        attribute.setName(ATT_ADDRESS);

        UPAttributeRequired requirements = new UPAttributeRequired();
        attribute.setRequired(requirements);

        UPAttributePermissions permissions = new UPAttributePermissions();
        permissions.setEdit(Collections.singleton(UPConfigUtils.ROLE_ADMIN));
        attribute.setPermissions(permissions);

        config.addAttribute(attribute);

        provider.setConfiguration(JsonSerialization.writeValueAsString(config));

        Map<String, Object> attributes = new HashMap<>();

        attributes.put(UserModel.USERNAME, "user");
        attributes.put(UserModel.FIRST_NAME, "user");
        attributes.put(UserModel.LAST_NAME, "user");
        attributes.put(UserModel.EMAIL, org.keycloak.models.utils.KeycloakModelUtils.generateId() + "@keycloak.org");

        // NO fail on USER contexts
        UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
        profile.validate();

        // Fails on ADMIN context - User REST API
        try {
            profile = provider.create(UserProfileContext.USER_API, attributes);
            profile.validate();
            fail("Should fail validation");
        } catch (ValidationException ve) {
            assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
        }

    }

    @Test
    public void testNoValidationsIfAdminReadOnly() {
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testNoValidationsIfAdminReadOnly);
    }

    private static void testNoValidationsIfAdminReadOnly(KeycloakSession session) throws IOException {
        UserProfileProvider provider = getUserProfileProvider(session);
        UPConfig config = new UPConfig();
        UPAttribute attribute = new UPAttribute();

        attribute.setName(ATT_ADDRESS);

        UPAttributeRequired requirements = new UPAttributeRequired();
        attribute.setRequired(requirements);

        UPAttributePermissions permissions = new UPAttributePermissions();
        permissions.setEdit(Collections.singleton(UPConfigUtils.ROLE_USER));
        attribute.setPermissions(permissions);

        config.addAttribute(attribute);

        provider.setConfiguration(JsonSerialization.writeValueAsString(config));

        Map<String, Object> attributes = new HashMap<>();

        attributes.put(UserModel.USERNAME, "user");

        // Fails on USER context
        UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
        try {
            profile.validate();
            fail("Should fail validation");
        } catch (ValidationException ve) {
            assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
        }

        // NO fail on ADMIN context - User REST API
        profile = provider.create(UserProfileContext.USER_API, attributes);
        profile.validate();
    }

    @Test
    public void testIgnoreReadOnlyAttribute() {
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testIgnoreReadOnlyAttribute);
    }

    private static void testIgnoreReadOnlyAttribute(KeycloakSession session) throws IOException {
        UserProfileProvider provider = getUserProfileProvider(session);
        UPConfig config = new UPConfig();
        UPAttribute firstName = new UPAttribute();

        firstName.setName(UserModel.FIRST_NAME);

        UPAttribute address = new UPAttribute();

        address.setName(ATT_ADDRESS);

        UPAttributeRequired requirements = new UPAttributeRequired();
        requirements.setRoles(Collections.singleton(UPConfigUtils.ROLE_USER));
        address.setRequired(requirements);
        firstName.setRequired(requirements);

        UPAttributePermissions permissions = new UPAttributePermissions();
        permissions.setEdit(Collections.singleton(UPConfigUtils.ROLE_USER));
        permissions.setView(Collections.singleton(ROLE_ADMIN));
        address.setPermissions(permissions);
        firstName.setPermissions(permissions);

        config.addAttribute(address);
        config.addAttribute(firstName);

        provider.setConfiguration(JsonSerialization.writeValueAsString(config));

        Map<String, Object> attributes = new HashMap<>();

        attributes.put(UserModel.USERNAME, org.keycloak.models.utils.KeycloakModelUtils.generateId());

        // Fails on USER context
        UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
        try {
            profile.validate();
            fail("Should fail validation");
        } catch (ValidationException ve) {
            assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
        }

        // attribute ignored for admin when not provided and creating user
        profile = provider.create(UserProfileContext.USER_API, attributes);
        profile.validate();

        // attribute ignored for admin when empty and creating user
        attributes.put(ATT_ADDRESS, List.of(""));
        attributes.put(UserModel.FIRST_NAME, List.of(""));
        profile = provider.create(UserProfileContext.USER_API, attributes);
        UserModel user = profile.create();

        // attribute ignored for admin when empty and updating user
        profile = provider.create(UserProfileContext.USER_API, attributes, user);
        profile.validate();

        // attribute not ignored for admin when empty and updating user
        user.setFirstName("alice");
        profile = provider.create(UserProfileContext.USER_API, attributes, user);
        try {
            profile.validate();
            fail("Should fail validation");
        } catch (ValidationException ve) {
            assertTrue(ve.isAttributeOnError(UserModel.FIRST_NAME));
        }
    }

    @Test
    public void testReadOnlyInternalAttributeValidation() {
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testReadOnlyInternalAttributeValidation);
    }

    private static void testReadOnlyInternalAttributeValidation(KeycloakSession session) throws IOException {
        RealmModel realm = session.getContext().getRealm();
        UserModel maria = session.users().addUser(realm, "maria");

        maria.setAttribute(LDAPConstants.LDAP_ID, List.of("1"));

        UserProfileProvider provider = getUserProfileProvider(session);
        Map<String, List<String>> attributes = new HashMap<>();

        attributes.put(LDAPConstants.LDAP_ID, List.of("2"));

        UserProfile profile = provider.create(UserProfileContext.USER_API, attributes, maria);

        try {
            profile.validate();
            fail("Should fail validation");
        } catch (ValidationException ve) {
            assertTrue(ve.isAttributeOnError(LDAPConstants.LDAP_ID));
        }
    }

    @Test
    public void testRequiredByClientScope() {
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testRequiredByClientScope);
    }

    private static void testRequiredByClientScope(KeycloakSession session) throws IOException {
        UserProfileProvider provider = getUserProfileProvider(session);
        UPConfig config = new UPConfig();
        UPAttribute attribute = new UPAttribute();

        attribute.setName(ATT_ADDRESS);

        UPAttributeRequired requirements = new UPAttributeRequired();

        requirements.setScopes(Collections.singleton("client-a"));

        attribute.setRequired(requirements);

        UPAttributePermissions permissions = new UPAttributePermissions();
        permissions.setEdit(Collections.singleton("user"));
        attribute.setPermissions(permissions);

        config.addAttribute(attribute);

        provider.setConfiguration(JsonSerialization.writeValueAsString(config));

        Map<String, Object> attributes = new HashMap<>();

        attributes.put(UserModel.USERNAME, "user");
        attributes.put(UserModel.FIRST_NAME, "John");
        attributes.put(UserModel.LAST_NAME, "Doe");
        attributes.put(UserModel.EMAIL, "user@email.test");

        // client with default scopes for which is attribute NOT configured as required
        configureAuthenticationSession(session, "client-b", null);

        // no fail on User API nor Account console as they do not have scopes
        UserProfile profile = provider.create(UserProfileContext.USER_API, attributes);
        profile.validate();
        profile = provider.create(UserProfileContext.ACCOUNT, attributes);
        profile.validate();
        profile = provider.create(UserProfileContext.ACCOUNT_OLD, attributes);
        profile.validate();

        // no fail on auth flow scopes when scope is not required
        profile = provider.create(UserProfileContext.REGISTRATION_PROFILE, attributes);
        profile.validate();
        profile = provider.create(UserProfileContext.REGISTRATION_USER_CREATION, attributes);
        profile.validate();
        profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
        profile.validate();
        profile = provider.create(UserProfileContext.IDP_REVIEW, attributes);
        profile.validate();

        // client with default scope for which is attribute configured as required
        configureAuthenticationSession(session, "client-a", null);

        // no fail on User API nor Account console as they do not have scopes
        profile = provider.create(UserProfileContext.USER_API, attributes);
        profile.validate();
        profile = provider.create(UserProfileContext.ACCOUNT, attributes);
        profile.validate();
        profile = provider.create(UserProfileContext.ACCOUNT_OLD, attributes);
        profile.validate();

        // fail on auth flow scopes when scope is required
        try {
            profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
            profile.validate();
            fail("Should fail validation");
        } catch (ValidationException ve) {
            assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
        }
        try {
            profile = provider.create(UserProfileContext.REGISTRATION_PROFILE, attributes);
            profile.validate();
            fail("Should fail validation");
        } catch (ValidationException ve) {
            assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
        }
        try {
            profile = provider.create(UserProfileContext.IDP_REVIEW, attributes);
            profile.validate();
            fail("Should fail validation");
        } catch (ValidationException ve) {
            assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
        }

    }

    @Test
    public void testConfigurationInvalidScope() {
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testConfigurationInvalidScope);
    }

    private static void testConfigurationInvalidScope(KeycloakSession session) throws IOException {
        RealmModel realm = session.getContext().getRealm();
        UserProfileProvider provider = getUserProfileProvider(session);
        UPConfig config = new UPConfig();
        UPAttribute attribute = new UPAttribute();

        attribute.setName(ATT_ADDRESS);

        UPAttributeRequired requirements = new UPAttributeRequired();

        requirements.setScopes(Collections.singleton("invalid"));

        attribute.setRequired(requirements);

        attribute.setSelector(new UPAttributeSelector());
        attribute.getSelector().setScopes(Collections.singleton("invalid"));

        config.addAttribute(attribute);

        try {
            provider.setConfiguration(JsonSerialization.writeValueAsString(config));
            Assert.fail("Expected to fail due to invalid client scope");
        } catch (ComponentValidationException cve) {
            //ignore
        }
    }

    @Test
    public void testUsernameAndEmailPermissionNotSetIfEmpty() {
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testUsernameAndEmailPermissionNotSetIfEmpty);
    }

    private static void testUsernameAndEmailPermissionNotSetIfEmpty(KeycloakSession session) throws IOException {
        UserProfileProvider provider = getUserProfileProvider(session);
        UPConfig config = JsonSerialization.readValue(provider.getConfiguration(), UPConfig.class);

        for (UPAttribute attribute : config.getAttributes()) {
            if (attribute.getName().equals(UserModel.USERNAME) || attribute.getName().equals(UserModel.EMAIL)) {
                attribute.setPermissions(new UPAttributePermissions());
            }
        }

        provider.setConfiguration(JsonSerialization.writeValueAsString(config));

        RealmModel realm = session.getContext().getRealm();
        String username = "profiled-user-profile";
        UserModel user = session.users().addUser(realm, username);
        Map<String, Object> attributes = new HashMap<>();

        attributes.put(UserModel.USERNAME, user.getUsername());
        attributes.put(UserModel.EMAIL, "test@keycloak.com");

        UserProfile profile = provider.create(UserProfileContext.USER_API, attributes, user);

        profile.update();

        user = session.users().getUserById(realm, user.getId());

        assertEquals("test@keycloak.com", user.getEmail());
    }

    @Test
    public void testDoNotRemoveAttributes() {
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testDoNotRemoveAttributes);
    }

    private static void testDoNotRemoveAttributes(KeycloakSession session) {
        Map<String, Object> attributes = new HashMap<>();

        attributes.put(UserModel.USERNAME, org.keycloak.models.utils.KeycloakModelUtils.generateId());
        attributes.put(UserModel.EMAIL, Arrays.asList("test@test.com"));
        attributes.put("test-attribute", Arrays.asList("Test Value"));
        attributes.put("foo", Arrays.asList("foo"));

        UserProfileProvider provider = getUserProfileProvider(session);

        provider.setConfiguration("{\"attributes\": ["
                + "{\"name\": \"test-attribute\", \"permissions\": {\"edit\": [\"admin\", \"user\"]}},"
                + "{\"name\": \"foo\", \"permissions\": {\"edit\": [\"admin\", \"user\"]}},"
                + "{\"name\": \"email\", \"permissions\": {\"edit\": [\"admin\", \"user\"]}}]}");

        UserProfile profile = provider.create(UserProfileContext.USER_API, attributes);
        UserModel user = profile.create();

        attributes.clear();
        attributes.put(UserModel.EMAIL, Arrays.asList("new-email@test.com"));
        attributes.put("foo", "changed");
        profile = provider.create(UserProfileContext.USER_API, attributes, user);
        try {
            profile.update(false);
            fail("Should fail validation");
        } catch (ValidationException ve) {
            assertTrue(ve.isAttributeOnError(UserModel.USERNAME));
            assertTrue(ve.hasError(Messages.MISSING_USERNAME));
        }

        attributes.put(UserModel.USERNAME, Collections.singletonList(user.getUsername()));
        profile = provider.create(UserProfileContext.USER_API, attributes, user);
        profile.update(false);

        profile = provider.create(UserProfileContext.USER_API, user);
        Attributes userAttributes = profile.getAttributes();
        assertEquals("new-email@test.com", userAttributes.getFirstValue(UserModel.EMAIL));
        assertEquals("Test Value", userAttributes.getFirstValue("test-attribute"));
        assertEquals("changed", userAttributes.getFirstValue("foo"));

        attributes.remove("foo");
        attributes.put("test-attribute", userAttributes.getFirstValue("test-attribute"));
        profile = provider.create(UserProfileContext.USER_API, attributes, user);
        profile.update(true);
        profile = provider.create(UserProfileContext.USER_API, user);
        userAttributes = profile.getAttributes();
        // remove attribute if not set
        assertEquals("new-email@test.com", userAttributes.getFirstValue(UserModel.EMAIL));
        assertEquals("Test Value", userAttributes.getFirstValue("test-attribute"));
        assertNull(userAttributes.getFirstValue("foo"));

        provider.setConfiguration("{\"attributes\": ["
                + "{\"name\": \"test-attribute\", \"permissions\": {\"edit\": [\"user\"]}},"
                + "{\"name\": \"foo\", \"permissions\": {\"edit\": [\"admin\", \"user\"]}},"
                + "{\"name\": \"email\", \"permissions\": {\"edit\": [\"admin\", \"user\"]}}]}");
        attributes.remove("test-attribute");
        profile = provider.create(UserProfileContext.USER_API, attributes, user);
        profile.update(true);
        profile = provider.create(UserProfileContext.USER_API, user);
        userAttributes = profile.getAttributes();
        // do not remove test-attribute because admin does not have write permissions
        assertEquals("new-email@test.com", userAttributes.getFirstValue(UserModel.EMAIL));
        assertEquals("Test Value", userAttributes.getFirstValue("test-attribute"));

        provider.setConfiguration("{\"attributes\": ["
                + "{\"name\": \"test-attribute\", \"permissions\": {\"edit\": [\"admin\", \"user\"]}},"
                + "{\"name\": \"foo\", \"permissions\": {\"edit\": [\"admin\", \"user\"]}},"
                + "{\"name\": \"email\", \"permissions\": {\"edit\": [\"admin\", \"user\"]}}]}");
        attributes.remove("test-attribute");
        profile = provider.create(UserProfileContext.USER_API, attributes, user);
        profile.update(true);
        profile = provider.create(UserProfileContext.USER_API, user);
        userAttributes = profile.getAttributes();
        // removes the test-attribute attribute because now admin has write permission
        assertEquals("new-email@test.com", userAttributes.getFirstValue(UserModel.EMAIL));
        assertNull(userAttributes.getFirstValue("test-attribute"));
    }

    @Test
    public void testRemoveEmptyRootAttribute() {
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testRemoveEmptyRootAttribute);
    }

    private static void testRemoveEmptyRootAttribute(KeycloakSession session) {
        Map<String, List<String>> attributes = new HashMap<>();

        attributes.put(UserModel.USERNAME, List.of(org.keycloak.models.utils.KeycloakModelUtils.generateId()));
        attributes.put(UserModel.EMAIL, List.of(""));
        attributes.put(UserModel.FIRST_NAME, List.of(""));
        attributes.put("test-attribute", List.of(""));

        UserProfileProvider provider = getUserProfileProvider(session);

        provider.setConfiguration("{\"attributes\": ["
                + "{\"name\": \"test-attribute\", \"permissions\": {\"edit\": [\"admin\", \"user\"]}},"
                + "{\"name\": \"firstName\", \"permissions\": {\"edit\": [\"admin\", \"user\"]}},"
                + "{\"name\": \"lastName\", \"permissions\": {\"edit\": [\"admin\", \"user\"]}},"
                + "{\"name\": \"email\", \"permissions\": {\"edit\": [\"admin\", \"user\"]}}]}");

        UserProfile profile = provider.create(UserProfileContext.USER_API, attributes);
        UserModel user = profile.create();
        assertNull(user.getEmail());
        assertNull(user.getFirstName());
        assertNull(user.getLastName());

        attributes.remove(UserModel.EMAIL);
        attributes.put(UserModel.FIRST_NAME, List.of("myfname"));
        profile = provider.create(UserProfileContext.USER_API, attributes);
        Attributes upAttributes = profile.getAttributes();
        assertRemoveEmptyRootAttribute(attributes, user, upAttributes);

        profile = provider.create(UserProfileContext.USER_API, attributes, user);
        profile.update(false);
        upAttributes = profile.getAttributes();
        assertRemoveEmptyRootAttribute(attributes, user, upAttributes);
    }

    private static void assertRemoveEmptyRootAttribute(Map<String, List<String>> attributes, UserModel user, Attributes upAttributes) {
        assertNull(upAttributes.getFirstValue(UserModel.LAST_NAME));
        assertNull(user.getLastName());
        assertNull(upAttributes.getFirstValue(UserModel.EMAIL));
        assertNull(user.getEmail());
        assertEquals(upAttributes.getFirstValue(UserModel.FIRST_NAME), attributes.get(UserModel.FIRST_NAME).get(0));
    }

    @Test
    public void testRemoveOptionalAttributesFromDefaultConfigIfNotSet() {
        getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testRemoveOptionalAttributesFromDefaultConfigIfNotSet);
    }

    private static void testRemoveOptionalAttributesFromDefaultConfigIfNotSet(KeycloakSession session) throws IOException {
        UPConfig config = new UPConfig();
        UPAttribute attribute = new UPAttribute();

        attribute.setName("foo");

        config.addAttribute(attribute);

        UserProfileProvider provider = getUserProfileProvider(session);
        provider.setConfiguration(JsonSerialization.writeValueAsString(config));

        Map<String, Object> attributes = new HashMap<>();

        attributes.put(UserModel.USERNAME, org.keycloak.models.utils.KeycloakModelUtils.generateId() + "@keycloak.org");
        attributes.put(UserModel.EMAIL, org.keycloak.models.utils.KeycloakModelUtils.generateId() + "@keycloak.org");
        attributes.put("foo", "foo");

        UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
        UserModel user = profile.create();

        assertFalse(profile.getAttributes().contains(UserModel.FIRST_NAME));
        assertFalse(profile.getAttributes().contains(UserModel.LAST_NAME));

        UPAttribute firstName = new UPAttribute();
        firstName.setName(UserModel.FIRST_NAME);
        config.addAttribute(firstName);
        UPAttribute lastName = new UPAttribute();
        lastName.setName(UserModel.LAST_NAME);
        config.addAttribute(lastName);
        provider.setConfiguration(JsonSerialization.writeValueAsString(config));
        profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes, user);
        assertTrue(profile.getAttributes().contains(UserModel.FIRST_NAME));
        assertTrue(profile.getAttributes().contains(UserModel.LAST_NAME));
    }
}
