/*
 * Copyright 2016 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.model;

import com.google.common.collect.ImmutableMap;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction;
import org.keycloak.services.managers.ClientManager;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.junit.Assert.assertNotNull;

/**
 * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
 */
public class UserModelTest extends AbstractModelTest {

    @Test
    public void persistUser() {
        RealmModel realm = realmManager.createRealm("original");
        KeycloakSession session = realmManager.getSession();
        UserModel user = session.users().addUser(realm, "user");
        user.setFirstName("first-name");
        user.setLastName("last-name");
        user.setEmail("email");
        assertNotNull(user.getCreatedTimestamp());
        // test that timestamp is current with 10s tollerance
        Assert.assertTrue((System.currentTimeMillis() - user.getCreatedTimestamp()) < 10000);

        user.addRequiredAction(RequiredAction.CONFIGURE_TOTP);
        user.addRequiredAction(RequiredAction.UPDATE_PASSWORD);

        RealmModel searchRealm = realmManager.getRealm(realm.getId());
        UserModel persisted = session.users().getUserByUsername("user", searchRealm);

        assertEquals(user, persisted);

        searchRealm = realmManager.getRealm(realm.getId());
        UserModel persisted2 =  session.users().getUserById(user.getId(), searchRealm);
        assertEquals(user, persisted2);

        Map<String, String> attributes = new HashMap<String, String>();
        attributes.put(UserModel.LAST_NAME, "last-name");
        List<UserModel> search = session.users().searchForUser(attributes, realm);
        Assert.assertEquals(search.size(), 1);
        Assert.assertEquals(search.get(0).getUsername(), "user");

        attributes.clear();
        attributes.put(UserModel.EMAIL, "email");
        search = session.users().searchForUser(attributes, realm);
        Assert.assertEquals(search.size(), 1);
        Assert.assertEquals(search.get(0).getUsername(), "user");

        attributes.clear();
        attributes.put(UserModel.LAST_NAME, "last-name");
        attributes.put(UserModel.EMAIL, "email");
        search = session.users().searchForUser(attributes, realm);
        Assert.assertEquals(search.size(), 1);
        Assert.assertEquals(search.get(0).getUsername(), "user");
    }
    
    @Test
    public void webOriginSetTest() {
        RealmModel realm = realmManager.createRealm("original");
        ClientModel client = realm.addClient("user");

        Assert.assertTrue(client.getWebOrigins().isEmpty());

        client.addWebOrigin("origin-1");
        Assert.assertEquals(1, client.getWebOrigins().size());

        client.addWebOrigin("origin-2");
        Assert.assertEquals(2, client.getWebOrigins().size());

        client.removeWebOrigin("origin-2");
        Assert.assertEquals(1, client.getWebOrigins().size());

        client.removeWebOrigin("origin-1");
        Assert.assertTrue(client.getWebOrigins().isEmpty());

        client = realm.addClient("oauthclient2");

        Assert.assertTrue(client.getWebOrigins().isEmpty());

        client.addWebOrigin("origin-1");
        Assert.assertEquals(1, client.getWebOrigins().size());

        client.addWebOrigin("origin-2");
        Assert.assertEquals(2, client.getWebOrigins().size());

        client.removeWebOrigin("origin-2");
        Assert.assertEquals(1, client.getWebOrigins().size());

        client.removeWebOrigin("origin-1");
        Assert.assertTrue(client.getWebOrigins().isEmpty());

    }

    @Test
    public void testUserRequiredActions() throws Exception {
        RealmModel realm = realmManager.createRealm("original");
        UserModel user = session.users().addUser(realm, "user");

        Assert.assertTrue(user.getRequiredActions().isEmpty());

        user.addRequiredAction(RequiredAction.CONFIGURE_TOTP);
        String id = realm.getId();
        commit();
        realm = realmManager.getRealm(id);
        user = session.users().getUserByUsername("user", realm);

        Assert.assertEquals(1, user.getRequiredActions().size());
        Assert.assertTrue(user.getRequiredActions().contains(RequiredAction.CONFIGURE_TOTP.name()));

        user.addRequiredAction(RequiredAction.CONFIGURE_TOTP);
        user = session.users().getUserByUsername("user", realm);

        Assert.assertEquals(1, user.getRequiredActions().size());
        Assert.assertTrue(user.getRequiredActions().contains(RequiredAction.CONFIGURE_TOTP.name()));

        user.addRequiredAction(RequiredAction.VERIFY_EMAIL.name());
        user = session.users().getUserByUsername("user", realm);

        Assert.assertEquals(2, user.getRequiredActions().size());
        Assert.assertTrue(user.getRequiredActions().contains(RequiredAction.CONFIGURE_TOTP.name()));
        Assert.assertTrue(user.getRequiredActions().contains(RequiredAction.VERIFY_EMAIL.name()));

        user.removeRequiredAction(RequiredAction.CONFIGURE_TOTP.name());
        user = session.users().getUserByUsername("user", realm);

        Assert.assertEquals(1, user.getRequiredActions().size());
        Assert.assertTrue(user.getRequiredActions().contains(RequiredAction.VERIFY_EMAIL.name()));

        user.removeRequiredAction(RequiredAction.VERIFY_EMAIL.name());
        user = session.users().getUserByUsername("user", realm);

        Assert.assertTrue(user.getRequiredActions().isEmpty());
    }

    @Test
    public void testUserMultipleAttributes() throws Exception {
        RealmModel realm = realmManager.createRealm("original");
        UserModel user = session.users().addUser(realm, "user");
        UserModel userNoAttrs = session.users().addUser(realm, "user-noattrs");

        user.setSingleAttribute("key1", "value1");
        List<String> attrVals = new ArrayList<>(Arrays.asList( "val21", "val22" ));
        user.setAttribute("key2", attrVals);

        commit();

        // Test read attributes
        realm = realmManager.getRealmByName("original");
        user = session.users().getUserByUsername("user", realm);

        attrVals = user.getAttribute("key1");
        Assert.assertEquals(1, attrVals.size());
        Assert.assertEquals("value1", attrVals.get(0));
        Assert.assertEquals("value1", user.getFirstAttribute("key1"));

        attrVals = user.getAttribute("key2");
        Assert.assertEquals(2, attrVals.size());
        Assert.assertTrue(attrVals.contains("val21"));
        Assert.assertTrue(attrVals.contains("val22"));

        attrVals = user.getAttribute("key3");
        Assert.assertTrue(attrVals.isEmpty());
        Assert.assertNull(user.getFirstAttribute("key3"));

        Map<String, List<String>> allAttrVals = user.getAttributes();
        Assert.assertEquals(2, allAttrVals.size());
        Assert.assertEquals(allAttrVals.get("key1"), user.getAttribute("key1"));
        Assert.assertEquals(allAttrVals.get("key2"), user.getAttribute("key2"));

        // Test remove and rewrite attribute
        user.removeAttribute("key1");
        user.setSingleAttribute("key2", "val23");

        commit();

        realm = realmManager.getRealmByName("original");
        user = session.users().getUserByUsername("user", realm);
        Assert.assertNull(user.getFirstAttribute("key1"));
        attrVals = user.getAttribute("key2");
        Assert.assertEquals(1, attrVals.size());
        Assert.assertEquals("val23", attrVals.get(0));
    }

    // KEYCLOAK-3494
    @Test
    public void testUpdateUserAttribute() throws Exception {
        RealmModel realm = realmManager.createRealm("original");
        UserModel user = session.users().addUser(realm, "user");
        
        user.setSingleAttribute("key1", "value1");        

        commit();

        realm = realmManager.getRealmByName("original");
        user = session.users().getUserByUsername("user", realm);

        // Update attribute
        List<String> attrVals = new ArrayList<>(Arrays.asList( "val2" ));
        user.setAttribute("key1", attrVals);
        Map<String, List<String>> allAttrVals = user.getAttributes();

        // Ensure same transaction is able to see updated value
        Assert.assertEquals(1, allAttrVals.size());
        Assert.assertEquals(allAttrVals.get("key1"), Arrays.asList("val2"));

        commit();
    }
    
    // KEYCLOAK-3608
    @Test
    public void testUpdateUserSingleAttribute() {
        Map<String, List<String>> expected = ImmutableMap.of(
                "key1", Arrays.asList("value3"), 
                "key2", Arrays.asList("value2"));
        
        RealmModel realm = realmManager.createRealm("original");
        UserModel user = session.users().addUser(realm, "user");
        
        user.setSingleAttribute("key1", "value1");
        user.setSingleAttribute("key2", "value2");
        
        // Overwrite the first attribute
        user.setSingleAttribute("key1", "value3");
        
        Assert.assertEquals(expected, user.getAttributes());
        
        commit();
        
        realm = session.realms().getRealmByName("original");        
        Assert.assertEquals(expected, session.users().getUserByUsername("user", realm).getAttributes());        
    }

    @Test
    public void testSearchByString() {
        RealmModel realm = realmManager.createRealm("original");
        UserModel user1 = session.users().addUser(realm, "user1");

        commit();
        realm = session.realms().getRealmByName("original");
        List<UserModel> users = session.users().searchForUser("user", realm, 0, 7);
        Assert.assertTrue(users.contains(user1));
    }

    @Test
    public void testSearchByUserAttribute() throws Exception {
        RealmModel realm = realmManager.createRealm("original");
        UserModel user1 = session.users().addUser(realm, "user1");
        UserModel user2 = session.users().addUser(realm, "user2");
        UserModel user3 = session.users().addUser(realm, "user3");

        user1.setSingleAttribute("key1", "value1");
        user1.setSingleAttribute("key2", "value21");

        user2.setSingleAttribute("key1", "value1");
        user2.setSingleAttribute("key2", "value22");

        user3.setSingleAttribute("key2", "value21");

        commit();
        realm = session.realms().getRealmByName("original");

        List<UserModel> users = session.users().searchForUserByUserAttribute("key1", "value1", realm);
        Assert.assertEquals(2, users.size());
        Assert.assertTrue(users.contains(user1));
        Assert.assertTrue(users.contains(user2));

        users = session.users().searchForUserByUserAttribute("key2", "value21", realm);
        Assert.assertEquals(2, users.size());
        Assert.assertTrue(users.contains(user1));
        Assert.assertTrue(users.contains(user3));

        users = session.users().searchForUserByUserAttribute("key2", "value22", realm);
        Assert.assertEquals(1, users.size());
        Assert.assertTrue(users.contains(user2));

        users = session.users().searchForUserByUserAttribute("key3", "value3", realm);
        Assert.assertEquals(0, users.size());
    }

    @Test
    public void testServiceAccountLink() throws Exception {
        RealmModel realm = realmManager.createRealm("original");
        ClientModel client = realm.addClient("foo");

        UserModel user1 = session.users().addUser(realm, "user1");
        user1.setFirstName("John");
        user1.setLastName("Doe");

        UserModel user2 = session.users().addUser(realm, "user2");
        user2.setFirstName("John");
        user2.setLastName("Doe");

        // Search
        Assert.assertNull(session.users().getServiceAccount(client));
        List<UserModel> users = session.users().searchForUser("John Doe", realm);
        Assert.assertEquals(2, users.size());
        Assert.assertTrue(users.contains(user1));
        Assert.assertTrue(users.contains(user2));

        // Link service account
        user1.setServiceAccountClientLink(client.getId());

        commit();

        // Search and assert service account user not found
        realm = realmManager.getRealmByName("original");
        client = realm.getClientByClientId("foo");
        UserModel searched = session.users().getServiceAccount(client);
        Assert.assertEquals(searched, user1);
        users = session.users().searchForUser("John Doe", realm);
        Assert.assertEquals(1, users.size());
        Assert.assertFalse(users.contains(user1));
        Assert.assertTrue(users.contains(user2));

        users = session.users().getUsers(realm, false);
        Assert.assertEquals(1, users.size());
        Assert.assertFalse(users.contains(user1));
        Assert.assertTrue(users.contains(user2));

        users = session.users().getUsers(realm, true);
        Assert.assertEquals(2, users.size());
        Assert.assertTrue(users.contains(user1));
        Assert.assertTrue(users.contains(user2));

        Assert.assertEquals(2, session.users().getUsersCount(realm));

        // Remove client
        new ClientManager(realmManager).removeClient(realm, client);
        commit();

        // Assert service account removed as well
        realm = realmManager.getRealmByName("original");
        Assert.assertNull(session.users().getUserByUsername("user1", realm));
    }

    @Test
    public void testGrantToAll() {
        RealmModel realm1 = realmManager.createRealm("realm1");
        RoleModel role1 = realm1.addRole("role1");
        UserModel user1 = realmManager.getSession().users().addUser(realm1, "user1");
        UserModel user2 = realmManager.getSession().users().addUser(realm1, "user2");

        RealmModel realm2 = realmManager.createRealm("realm2");
        UserModel realm2User1 = realmManager.getSession().users().addUser(realm2, "user1");

        commit();

        realm1 = realmManager.getRealmByName("realm1");
        role1 = realm1.getRole("role1");
        realmManager.getSession().users().grantToAllUsers(realm1, role1);

        commit();

        realm1 = realmManager.getRealmByName("realm1");
        role1 = realm1.getRole("role1");
        user1 = realmManager.getSession().users().getUserByUsername("user1", realm1);
        user2 = realmManager.getSession().users().getUserByUsername("user2", realm1);
        Assert.assertTrue(user1.hasRole(role1));
        Assert.assertTrue(user2.hasRole(role1));

        realm2 = realmManager.getRealmByName("realm2");
        realm2User1 = realmManager.getSession().users().getUserByUsername("user1", realm2);
        Assert.assertFalse(realm2User1.hasRole(role1));
    }

    public static void assertEquals(UserModel expected, UserModel actual) {
        Assert.assertEquals(expected.getUsername(), actual.getUsername());
        Assert.assertEquals(expected.getCreatedTimestamp(), actual.getCreatedTimestamp());
        Assert.assertEquals(expected.getFirstName(), actual.getFirstName());
        Assert.assertEquals(expected.getLastName(), actual.getLastName());

        String[] expectedRequiredActions = expected.getRequiredActions().toArray(new String[expected.getRequiredActions().size()]);
        Arrays.sort(expectedRequiredActions);
        String[] actualRequiredActions = actual.getRequiredActions().toArray(new String[actual.getRequiredActions().size()]);
        Arrays.sort(actualRequiredActions);

        Assert.assertArrayEquals(expectedRequiredActions, actualRequiredActions);
    }

}

