package org.keycloak.testsuite.admin;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.keycloak.testsuite.forms.VerifyProfileTest.PERMISSIONS_ALL;
import static org.keycloak.testsuite.forms.VerifyProfileTest.enableDynamicUserProfile;
import static org.keycloak.testsuite.forms.VerifyProfileTest.setUserProfileConfiguration;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.admin.client.resource.UsersResource;
import org.keycloak.common.Profile;
import org.keycloak.models.UserModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ErrorRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.util.AdminClientUtil;
import org.keycloak.testsuite.util.UserBuilder;
import org.keycloak.userprofile.DeclarativeUserProfileProvider;
import org.keycloak.userprofile.UserProfileProvider;
import org.keycloak.utils.StringUtil;

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.Set;

import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.Response;

/**
 * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
 */
@EnableFeature(value = Profile.Feature.DECLARATIVE_USER_PROFILE)
public class DeclarativeUserTest extends AbstractAdminTest {

    private static final String LOCALE_ATTR_KEY = "locale";
    private static final String TEST_REALM_USER_MANAGER_NAME = "test-realm-user-manager";
    private static final String REQUIRED_ATTR_KEY = "required-attr";

    private Keycloak testRealmUserManagerClient;

    @Before
    public void onBefore() throws Exception {
        RealmRepresentation realmRep = realm.toRepresentation();
        realmRep.setInternationalizationEnabled(true);
        realmRep.setSupportedLocales(new HashSet<>(Arrays.asList("en", "de")));
        enableDynamicUserProfile(realmRep);
        realm.update(realmRep);
        setUserProfileConfiguration(realm, "{\"attributes\": ["
                + "{\"name\": \"username\", " + PERMISSIONS_ALL + "},"
                + "{\"name\": \"firstName\", " + PERMISSIONS_ALL + "},"
                + "{\"name\": \"email\", " + PERMISSIONS_ALL + "},"
                + "{\"name\": \"lastName\", " + PERMISSIONS_ALL + "},"
                + "{\"name\": \"aName\", " + PERMISSIONS_ALL + "},"
                + "{\"name\": \"custom-a\", " + PERMISSIONS_ALL + "},"
                + "{\"name\": \"custom-hidden\"},"
                + "{\"name\": \"attr1\", " + PERMISSIONS_ALL + "},"
                + "{\"name\": \"attr2\", " + PERMISSIONS_ALL + "}]}");

        UserRepresentation testRealmUserManager = UserBuilder.create().username(TEST_REALM_USER_MANAGER_NAME)
                .password(TEST_REALM_USER_MANAGER_NAME).build();
        String createdUserId = null;
        try (Response response = realm.users().create(testRealmUserManager)) {
            createdUserId = ApiUtil.getCreatedId(response);
        } catch (WebApplicationException e) {
            // it's ok when the user has already been created for a previous test
            assertThat(e.getResponse().getStatus(), equalTo(409));
        }

        if (createdUserId != null) {
            List<ClientRepresentation> foundClients = realm.clients().findByClientId("realm-management");
            assertThat(foundClients, hasSize(1));
            ClientRepresentation realmManagementClient = foundClients.get(0);

            RoleRepresentation manageUsersRole =
                    realm.clients().get(realmManagementClient.getId()).roles().get("manage-users").toRepresentation();
            assertThat(manageUsersRole, notNullValue());

            realm.users().get(createdUserId).roles().clientLevel(realmManagementClient.getId())
                    .add(Collections.singletonList(manageUsersRole));
        }

        ClientRepresentation testApp = new ClientRepresentation();
        testApp.setClientId("test-app");
        testApp.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
        testApp.setSecret("secret");
        try (Response response = realm.clients().create(testApp)) {
            ApiUtil.getCreatedId(response);
        } catch (WebApplicationException e) {
            // it's ok when the client has already been created for a previous test
        }

        testRealmUserManagerClient = AdminClientUtil.createAdminClient(true, realmRep.getRealm(),
                TEST_REALM_USER_MANAGER_NAME, TEST_REALM_USER_MANAGER_NAME, testApp.getClientId(), testApp.getSecret());
    }

    @After
    public void closeClient() {
        if (testRealmUserManagerClient != null) {
            testRealmUserManagerClient.close();
        }
    }

    @Test
    public void testReturnAllConfiguredAttributesEvenIfNotSet() {
        UserRepresentation user1 = new UserRepresentation();
        user1.setUsername("user1");
        user1.singleAttribute("attr1", "value1user1");
        user1.singleAttribute("attr2", "value2user1");
        String user1Id = createUser(user1);

        user1 = realm.users().get(user1Id).toRepresentation();
        Map<String, List<String>> attributes = user1.getAttributes();
        assertEquals(4, attributes.size());
        List<String> attr1 = attributes.get("attr1");
        assertEquals(1, attr1.size());
        assertEquals("value1user1", attr1.get(0));
        List<String> attr2 = attributes.get("attr2");
        assertEquals(1, attr2.size());
        assertEquals("value2user1", attr2.get(0));
        List<String> attrCustomA = attributes.get("custom-a");
        assertTrue(attrCustomA.isEmpty());
        assertTrue(attributes.containsKey("custom-a"));
        assertTrue(attributes.containsKey("aName"));
    }

    @Test
    public void testDoNotReturnAttributeIfNotReadble() {
        UserRepresentation user1 = new UserRepresentation();
        user1.setUsername("user1");
        user1.singleAttribute("attr1", "value1user1");
        user1.singleAttribute("attr2", "value2user1");
        String user1Id = createUser(user1);

        user1 = realm.users().get(user1Id).toRepresentation();
        Map<String, List<String>> attributes = user1.getAttributes();
        assertEquals(4, attributes.size());
        assertFalse(attributes.containsKey("custom-hidden"));

        setUserProfileConfiguration(this.realm, "{\"attributes\": ["
                + "{\"name\": \"username\", " + PERMISSIONS_ALL + "},"
                + "{\"name\": \"firstName\", " + PERMISSIONS_ALL + "},"
                + "{\"name\": \"email\", " + PERMISSIONS_ALL + "},"
                + "{\"name\": \"lastName\", " + PERMISSIONS_ALL + "},"
                + "{\"name\": \"aName\", " + PERMISSIONS_ALL + "},"
                + "{\"name\": \"custom-a\", " + PERMISSIONS_ALL + "},"
                + "{\"name\": \"custom-hidden\", " + PERMISSIONS_ALL + "},"
                + "{\"name\": \"attr1\", " + PERMISSIONS_ALL + "},"
                + "{\"name\": \"attr2\", " + PERMISSIONS_ALL + "}]}");


        user1 = realm.users().get(user1Id).toRepresentation();
        attributes = user1.getAttributes();
        assertEquals(5, attributes.size());
        assertTrue(attributes.containsKey("custom-hidden"));
    }

    @Test
    public void testUpdateUnsetAttributeWithEmptyValue() {
        setUserProfileConfiguration(this.realm, "{\"attributes\": ["
                + "{\"name\": \"username\", " + PERMISSIONS_ALL + "},"
                + "{\"name\": \"firstName\", " + PERMISSIONS_ALL + "},"
                + "{\"name\": \"email\", " + PERMISSIONS_ALL + "},"
                + "{\"name\": \"lastName\", " + PERMISSIONS_ALL + "},"
                + "{\"name\": \"attr1\", " + PERMISSIONS_ALL + "},"
                + "{\"name\": \"attr2\"}]}");

        UserRepresentation user1 = new UserRepresentation();
        user1.setUsername("user1");
        // set an attribute to later remove it from the configuration
        user1.singleAttribute("attr1", "some-value");
        String user1Id = createUser(user1);

        // remove the attr1 attribute from the configuration
        setUserProfileConfiguration(this.realm, "{\"attributes\": ["
                + "{\"name\": \"username\", " + PERMISSIONS_ALL + "},"
                + "{\"name\": \"firstName\", " + PERMISSIONS_ALL + "},"
                + "{\"name\": \"email\", " + PERMISSIONS_ALL + "},"
                + "{\"name\": \"lastName\", " + PERMISSIONS_ALL + "},"
                + "{\"name\": \"attr2\"}]}");

        UserResource userResource = realm.users().get(user1Id);
        user1 = userResource.toRepresentation();
        Map<String, List<String>> attributes = user1.getAttributes();
        attributes.put("attr2", Collections.singletonList(""));
        // should be able to update the user when a read-only attribute has an empty or null value
        userResource.update(user1);
        attributes.put("attr2", null);
        userResource.update(user1);
    }

    @Test
    public void testValidationUsingExistingAttributes() {
        setUserProfileConfiguration(this.realm, "{\"attributes\": ["
                + "{\"name\": \"username\", " + PERMISSIONS_ALL + "},"
                + "{\"name\": \"firstName\", " + PERMISSIONS_ALL + "},"
                + "{\"name\": \"email\", " + PERMISSIONS_ALL + "},"
                + "{\"name\": \"lastName\", " + PERMISSIONS_ALL + "},"
                + "{\"name\": \"" + REQUIRED_ATTR_KEY + "\", \"required\": {}, " + PERMISSIONS_ALL + "}]}");

        UserRepresentation user1 = new UserRepresentation();
        user1.setUsername("user1");
        // set an attribute to later remove it from the configuration
        user1.singleAttribute(REQUIRED_ATTR_KEY, "some-value");
        String user1Id = createUser(user1);

        UserResource userResource = realm.users().get(user1Id);
        user1 = userResource.toRepresentation();
        user1.setFirstName("changed");
        user1.setAttributes(null);

        // do not validate REQUIRED_ATTR_KEY because the attribute list is not provided and the user has the attribute
        userResource.update(user1);
        user1 = userResource.toRepresentation();
        assertEquals("changed", user1.getFirstName());

        user1.setAttributes(Collections.emptyMap());
        String expectedErrorMessage = String.format("Please specify attribute %s.", REQUIRED_ATTR_KEY);
        verifyUserUpdateFails(realm.users(), user1Id, user1, expectedErrorMessage);
    }

    private void verifyUserUpdateFails(UsersResource usersResource, String userId, UserRepresentation user,
            String expectedErrorMessage) {
        UserResource userResource = usersResource.get(userId);
        try {
            userResource.update(user);
            fail("Should fail with errorMessage: " + expectedErrorMessage);
        } catch (BadRequestException badRequest) {
            try (Response response = badRequest.getResponse()) {
                assertThat(response.getStatus(), equalTo(400));
                ErrorRepresentation error = response.readEntity(ErrorRepresentation.class);
                assertThat(error.getErrorMessage(), equalTo(expectedErrorMessage));
            }
        }
    }

    @Test
    public void validationErrorMessagesCanBeConfiguredWithRealmLocalization() {
        try {
            setUserProfileConfiguration(this.realm, "{\"attributes\": ["
                    + "{\"name\": \"username\", " + PERMISSIONS_ALL + "},"
                    + "{\"name\": \"firstName\", " + PERMISSIONS_ALL + "},"
                    + "{\"name\": \"email\", " + PERMISSIONS_ALL + "},"
                    + "{\"name\": \"lastName\", " + PERMISSIONS_ALL + "},"
                    + "{\"name\": \"" + LOCALE_ATTR_KEY + "\", " + PERMISSIONS_ALL + "},"
                    + "{\"name\": \"" + REQUIRED_ATTR_KEY + "\", \"required\": {}, " + PERMISSIONS_ALL + "}]}");

            realm.localization().saveRealmLocalizationText("en", "error-user-attribute-required",
                    "required-error en: {0}");
            getCleanup().addLocalization("en");
            realm.localization().saveRealmLocalizationText("de", "error-user-attribute-required",
                    "required-error de: {0}");
            getCleanup().addLocalization("de");

            UsersResource testRealmUserManagerClientUsersResource =
                    testRealmUserManagerClient.realm(REALM_NAME).users();

            // start with locale en
            changeTestRealmUserManagerLocale("en");

            UserRepresentation user = new UserRepresentation();
            user.setUsername("user-realm-localization");
            user.singleAttribute(REQUIRED_ATTR_KEY, "some-value");
            String userId = createUser(user);

            user.setAttributes(new HashMap<>());
            verifyUserUpdateFails(testRealmUserManagerClientUsersResource, userId, user,
                    "required-error en: " + REQUIRED_ATTR_KEY);

            // switch to locale de
            changeTestRealmUserManagerLocale("de");

            user.singleAttribute(REQUIRED_ATTR_KEY, "some-value");
            realm.users().get(userId).update(user);

            user.setAttributes(new HashMap<>());
            verifyUserUpdateFails(testRealmUserManagerClientUsersResource, userId, user,
                    "required-error de: " + REQUIRED_ATTR_KEY);
        } finally {
            changeTestRealmUserManagerLocale(null);
        }
    }

    private void changeTestRealmUserManagerLocale(String locale) {
        UsersResource testRealmUserManagerUsersResource = testRealmUserManagerClient.realm(REALM_NAME).users();

        List<UserRepresentation> foundUsers =
                testRealmUserManagerUsersResource.search(TEST_REALM_USER_MANAGER_NAME, true);
        assertThat(foundUsers, hasSize(1));
        UserRepresentation user = foundUsers.iterator().next();

        if (locale == null) {
            Map<String, List<String>> attributes = user.getAttributes();
            if (attributes != null) {
                attributes.remove(LOCALE_ATTR_KEY);
            }
        } else {
            user.singleAttribute(LOCALE_ATTR_KEY, locale);
        }

        // also set REQUIRED_ATTR_KEY, when not already set, otherwise the change will be rejected
        if (StringUtil.isBlank(user.firstAttribute(REQUIRED_ATTR_KEY))) {
            user.singleAttribute(REQUIRED_ATTR_KEY, "arbitrary-value");
        }

        testRealmUserManagerUsersResource.get(user.getId()).update(user);
    }

    @Test
    public void testDefaultUserProfileProviderIsActive() {
        getTestingClient().server(REALM_NAME).run(session -> {
            Set<UserProfileProvider> providers = session.getAllProviders(UserProfileProvider.class);
            assertThat(providers, notNullValue());
            assertThat(providers.isEmpty(), is(false));

            UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
            assertThat(provider, notNullValue());
            assertThat(DeclarativeUserProfileProvider.class.getName(), is(provider.getClass().getName()));
            assertThat(provider, instanceOf(DeclarativeUserProfileProvider.class));
        });
    }

    @Test
    public void testUserLocale() {
        RealmRepresentation realmRep = realm.toRepresentation();
        Boolean internationalizationEnabled = realmRep.isInternationalizationEnabled();
        realmRep.setInternationalizationEnabled(true);
        realm.update(realmRep);

        try {
            UserRepresentation user1 = new UserRepresentation();
            user1.setUsername("user1");
            user1.singleAttribute(UserModel.LOCALE, "pt_BR");
            String user1Id = createUser(user1);

            UserResource userResource = realm.users().get(user1Id);
            user1 = userResource.toRepresentation();
            assertEquals("pt_BR", user1.getAttributes().get(UserModel.LOCALE).get(0));

            realmRep.setInternationalizationEnabled(false);
            realm.update(realmRep);

            user1 = userResource.toRepresentation();
            assertNull(user1.getAttributes().get(UserModel.LOCALE));
        } finally {
            realmRep.setInternationalizationEnabled(internationalizationEnabled);
            realm.update(realmRep);
        }
    }

    private String createUser(UserRepresentation userRep) {
        Response response = realm.users().create(userRep);
        String createdId = ApiUtil.getCreatedId(response);
        response.close();
        getCleanup().addUserId(createdId);
        return createdId;
    }
}
