/*
 * 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.federation.storage.ldap;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import javax.ws.rs.ClientErrorException;
import javax.ws.rs.core.Response;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.rules.TestRule;
import org.junit.runners.MethodSorters;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.common.util.MultivaluedHashMap;
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.RealmProvider;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.UserModelDelegate;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.UserStorageProviderModel;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
import org.keycloak.storage.ldap.LDAPUtils;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapper;
import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.LDAPRule;

import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.MASTER;
import static org.keycloak.models.AdminRoles.ADMIN;
import static org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT;

/**
 * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
 */
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class LDAPBinaryAttributesTest {

    private static LDAPRule ldapRule = new LDAPRule();

    private static ComponentModel ldapModel = null;


    private static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {

        @Override
        public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
            MultivaluedHashMap<String,String> ldapConfig = LDAPTestUtils.getLdapRuleConfig(ldapRule);
            ldapConfig.putSingle(LDAPConstants.SYNC_REGISTRATIONS, "true");
            ldapConfig.putSingle(LDAPConstants.EDIT_MODE, UserStorageProvider.EditMode.WRITABLE.toString());
            UserStorageProviderModel model = new UserStorageProviderModel();
            model.setLastSync(0);
            model.setChangedSyncPeriod(-1);
            model.setFullSyncPeriod(-1);
            model.setName("test-ldap");
            model.setPriority(0);
            model.setProviderId(LDAPStorageProviderFactory.PROVIDER_NAME);
            model.setConfig(ldapConfig);

            ldapModel = appRealm.addComponentModel(model);
            LDAPTestUtils.addZipCodeLDAPMapper(appRealm, ldapModel);

            // Delete all LDAP users and add some new for testing
            LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
            LDAPTestUtils.removeAllLDAPUsers(ldapFedProvider, appRealm);

//            LDAPObject john = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnkeycloak", "John", "Doe", "john@email.org", null, "1234");
//            LDAPTestUtils.updateLDAPPassword(ldapFedProvider, john, "Password1");
//
//            LDAPObject existing = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "existing", "Existing", "Foo", "existing@email.org", null, "5678");

            appRealm.getClientByClientId("test-app").setDirectAccessGrantsEnabled(true);
        }
    });

    @ClassRule
    public static TestRule chain = RuleChain
            .outerRule(ldapRule)
            .around(keycloakRule);

    private static final String JPEG_PHOTO_BASE64 = " ";



    protected Keycloak adminClient;


    @Before
    public void before() {
        adminClient = Keycloak.getInstance(AUTH_SERVER_ROOT, MASTER, ADMIN, ADMIN, Constants.ADMIN_CLI_CLIENT_ID);
    }

    @After
    public void after() {
        ComponentRepresentation jpegMapper = adminClient.realm("test").components().query(ldapModel.getId(), LDAPStorageMapper.class.getName(), "jpeg-mapper").get(0);
        adminClient.realm("test").components().component(jpegMapper.getId()).remove();

        adminClient.close();
    }


    // Test invalid mapper configuration - validation exception thrown
    @Test
    public void test01InvalidMapperConfiguration() {
        KeycloakSession session = keycloakRule.startSession();
        try {
            RealmManager manager = new RealmManager(session);
            RealmModel appRealm = manager.getRealm("test");

            ComponentModel ldapComponentMapper = LDAPTestUtils.addUserAttributeMapper(appRealm, ldapModel, "jpeg-mapper", LDAPConstants.JPEG_PHOTO, LDAPConstants.JPEG_PHOTO);

            ldapComponentMapper.put(UserAttributeLDAPStorageMapper.IS_BINARY_ATTRIBUTE, true);
            try {
                appRealm.updateComponent(ldapComponentMapper);
                Assert.fail("Not expected to successfully update mapper");
            } catch (ComponentValidationException cve) {
                // Expected
            }

        } finally {
            keycloakRule.stopSession(session, true);
        }
    }


    @Test
    public void test02ReadOnly() {
        String mapperId = addPhotoMapper();

        KeycloakSession session = keycloakRule.startSession();
        try {
            RealmManager manager = new RealmManager(session);
            RealmModel appRealm = manager.getRealm("test");

            // Add user directly to LDAP
            LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
            addLDAPUser(ldapFedProvider, appRealm, "johnphoto", "John", "Photo", "john@photo.org", JPEG_PHOTO_BASE64);

            // Set mapper to be read-only
            ComponentModel ldapComponentMapper = appRealm.getComponent(mapperId);
            ldapComponentMapper.put(UserAttributeLDAPStorageMapper.READ_ONLY, true);
            appRealm.updateComponent(ldapComponentMapper);
        } finally {
            keycloakRule.stopSession(session, true);
        }

        // Assert john found
        getUserAndAssertPhoto("johnphoto", true);
    }


    @Test
    public void test03WritableMapper() {
        String mapperId = addPhotoMapper();

        // Create user joe with jpegPHoto
        UserRepresentation joe = new UserRepresentation();
        joe.setUsername("joephoto");
        joe.setEmail("joe@photo.org");
        joe.setAttributes(Collections.singletonMap(LDAPConstants.JPEG_PHOTO, Arrays.asList(JPEG_PHOTO_BASE64)));
        Response response = adminClient.realm("test").users().create(joe);
        response.close();


        // Assert he is found including jpegPhoto
        joe = getUserAndAssertPhoto("joephoto", true);


        // Try to update him with some big non-LDAP mapped attribute. It will fail
        try {
            joe.getAttributes().put("someOtherPhoto", Arrays.asList(JPEG_PHOTO_BASE64));
            adminClient.realm("test").users().get(joe.getId()).update(joe);

            // TODO: Workaround as on Mongo it is not limit for length of attribute. Should be removed/improved...
            KeycloakSession session = keycloakRule.startSession();
            String realmProviderId = session.getKeycloakSessionFactory().getProviderFactory(RealmProvider.class).getId();
            keycloakRule.stopSession(session, false);
            if (!realmProviderId.equals("mongo")) {
                Assert.fail("Not expected to successfully update user");
            }

        } catch (ClientErrorException cee) {
            // Expected
        }

        // Remove jpegPhoto attribute and assert it was successfully removed
        joe.getAttributes().remove("someOtherPhoto");
        joe.getAttributes().remove(LDAPConstants.JPEG_PHOTO);
        adminClient.realm("test").users().get(joe.getId()).update(joe);
        getUserAndAssertPhoto("joephoto", false);
    }


    private String addPhotoMapper() {
        KeycloakSession session = keycloakRule.startSession();
        try {
            RealmManager manager = new RealmManager(session);
            RealmModel appRealm = manager.getRealm("test");

            ComponentModel ldapComponentMapper = LDAPTestUtils.addUserAttributeMapper(appRealm, ldapModel, "jpeg-mapper", LDAPConstants.JPEG_PHOTO, LDAPConstants.JPEG_PHOTO);

            ldapComponentMapper.put(UserAttributeLDAPStorageMapper.IS_BINARY_ATTRIBUTE, true);
            ldapComponentMapper.put(UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, true);
            appRealm.updateComponent(ldapComponentMapper);
            return ldapComponentMapper.getId();
        } finally {
            keycloakRule.stopSession(session, true);
        }
    }


    private LDAPObject addLDAPUser(LDAPStorageProvider ldapProvider, RealmModel realm, final String username,
                                         final String firstName, final String lastName, final String email, String jpegPhoto) {
        UserModel helperUser = new UserModelDelegate(null) {

            @Override
            public String getUsername() {
                return username;
            }

            @Override
            public String getFirstName() {
                return firstName;
            }

            @Override
            public String getLastName() {
                return lastName;
            }

            @Override
            public String getEmail() {
                return email;
            }

            @Override
            public List<String> getAttribute(String name) {
                if (LDAPConstants.JPEG_PHOTO.equals(name)) {
                    return Arrays.asList(jpegPhoto);
                } else {
                    return Collections.emptyList();
                }
            }
        };
        return LDAPUtils.addUserToLDAP(ldapProvider, realm, helperUser);
    }


    private UserRepresentation getUserAndAssertPhoto(String username, boolean isPhotoExpected) {
        List<UserRepresentation> johns = adminClient.realm("test").users().search(username, 0, 10);
        Assert.assertEquals(1, johns.size());
        UserRepresentation john = johns.get(0);
        Assert.assertEquals(username, john.getUsername());
        Assert.assertTrue(john.getAttributes().containsKey(LDAPConstants.LDAP_ID)); // Doublecheck it's the LDAP mapped user

        if (isPhotoExpected) {
            Assert.assertEquals(JPEG_PHOTO_BASE64, john.getAttributes().get(LDAPConstants.JPEG_PHOTO).get(0));
        } else {
            Assert.assertFalse(john.getAttributes().containsKey(LDAPConstants.JPEG_PHOTO));
        }
        return john;
    }

}
