/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.jackrabbit.oak.spi.security.authentication.external.impl;

import org.apache.jackrabbit.guava.common.collect.ImmutableSet;
import org.apache.jackrabbit.api.security.user.Authorizable;
import org.apache.jackrabbit.api.security.user.Group;
import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters;
import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalUser;
import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncResult;
import org.apache.jackrabbit.oak.spi.security.authentication.external.basic.DefaultSyncConfig;
import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal;
import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration;
import org.apache.jackrabbit.oak.spi.xml.ImportBehavior;
import org.apache.jackrabbit.oak.spi.xml.ProtectedItemImporter;
import org.jetbrains.annotations.NotNull;
import org.junit.Test;

import javax.jcr.RepositoryException;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;

/**
 * Test combining dynamic sync, automembership and manual member relationship between external group and local group
 */
public class DynamicSyncTest extends AbstractDynamicTest {
    
    private static final String BASE_ID = "base";
    private static final String BASE2_ID = "base2";
    private static final String AUTO_GROUPS = "autoForGroups";
    private static final String AUTO_USERS = "autoForUsers";

    private Group autoForGroups;
    private Group autoForUsers;
    private Group base;
    private Group base2;

    @Override
    public void before() throws Exception {
        super.before();

        autoForGroups = userManager.getAuthorizable(AUTO_GROUPS, Group.class);
        autoForUsers = userManager.getAuthorizable(AUTO_USERS, Group.class);

        base = userManager.createGroup(BASE_ID);
        // add automembership groups
        assertTrue(base.addMembers(AUTO_GROUPS, AUTO_USERS).isEmpty());
        // add external groups as members breaching IDP boundary (Not recommended!)
        assertTrue(base.addMembers("a", "b").isEmpty());
        
        userManager.createGroup(EveryonePrincipal.getInstance());

        base2 = userManager.createGroup(BASE2_ID);
        base2.addMember(autoForUsers);
        
        r.commit();
    }

    @Override
    @NotNull ExternalUser syncPriorToDynamicMembership() {
        // method not needed
        return mock(ExternalUser.class);
    }

    @Override
    protected ConfigurationParameters getSecurityConfigParameters() {
        return ConfigurationParameters.of(UserConfiguration.NAME,
                ConfigurationParameters.of(
                        ProtectedItemImporter.PARAM_IMPORT_BEHAVIOR, ImportBehavior.NAME_BESTEFFORT)
        );
    }

    @Override
    protected @NotNull DefaultSyncConfig createSyncConfig() {
        DefaultSyncConfig config = super.createSyncConfig();
        config.group()
                .setDynamicGroups(true)
                .setAutoMembership(AUTO_GROUPS);
        config.user()
                .setEnforceDynamicMembership(true)
                .setMembershipNestingDepth(2)
                .setAutoMembership(AUTO_USERS);
        return config;
    }

    @Test
    public void testSyncedUser() throws Exception {
        ExternalUser externalUser = idp.getUser(USER_ID);
        sync(externalUser, SyncResult.Status.ADD);

        Authorizable user = userManager.getAuthorizable(USER_ID);
        assertNotNull(user);

        // assert membership
        Set<String> expDeclaredGroupIds = ImmutableSet.of("a", "b", "c", "aa", "aaa", AUTO_GROUPS, AUTO_USERS, EveryonePrincipal.NAME);
        assertExpectedIds(expDeclaredGroupIds, user.declaredMemberOf());

        Set<String> expGroupIds = ImmutableSet.of(BASE_ID, BASE2_ID, "a", "b", "c", "aa", "aaa", AUTO_GROUPS, AUTO_USERS, EveryonePrincipal.NAME);
        assertExpectedIds(expGroupIds, user.memberOf());

        // assert groups
        user.declaredMemberOf().forEachRemaining(group -> assertIsMember(group, true, user));
        user.memberOf().forEachRemaining(group -> assertIsMember(group, false, user));

        // assert principals
        List<String> principalNames = getPrincipalNames(getPrincipalManager(r).getGroupMembership(user.getPrincipal()));
        assertEquals(10, principalNames.size());
    }

    @Test
    public void testSyncedGroup() throws Exception {
        ExternalUser externalUser = idp.getUser(USER_ID);
        sync(externalUser, SyncResult.Status.ADD);

        // verify group 'a'
        Group aGroup = userManager.getAuthorizable("a", Group.class);
        assertNotNull(aGroup);
        
        assertExpectedIds(Collections.singleton(USER_ID), aGroup.getDeclaredMembers(), aGroup.getMembers());
        
        Set<String> expectedIds = ImmutableSet.of(AUTO_GROUPS, BASE_ID, EveryonePrincipal.NAME);
        assertExpectedIds(expectedIds, aGroup.declaredMemberOf(), aGroup.memberOf());
    }

    @Test
    public void testAutomembershipGroups() throws Exception {
        ExternalUser externalUser = idp.getUser(USER_ID);
        sync(externalUser, SyncResult.Status.ADD);

        Authorizable user = userManager.getAuthorizable(USER_ID);
        Group aGroup = userManager.getAuthorizable("a", Group.class);

        // verify group 'autoForGroups'
        Set<String> expMemberIds = ImmutableSet.of("a", "b", "c", "aa", "aaa", USER_ID);
        assertExpectedIds(expMemberIds, autoForGroups.getDeclaredMembers(), autoForGroups.getMembers());
        assertIsMember(autoForGroups, true, user, aGroup);
        assertIsMember(autoForGroups, false, user, aGroup);
        assertFalse(autoForGroups.isMember(base));
    }

    @Test
    public void testAutomembershipUsers() throws Exception {
        ExternalUser externalUser = idp.getUser(USER_ID);
        sync(externalUser, SyncResult.Status.ADD);

        Authorizable user = userManager.getAuthorizable(USER_ID);
        Group aGroup = userManager.getAuthorizable("a", Group.class);

        // verify group 'autoForUsers'
        Set<String> expMemberIds = ImmutableSet.of(USER_ID);
        assertExpectedIds(expMemberIds, autoForUsers.getDeclaredMembers(), autoForUsers.getMembers());
        assertTrue(autoForUsers.isMember(user));

        assertFalse(autoForUsers.isMember(aGroup));
        assertFalse(autoForUsers.isMember(base));
    }

    @Test
    public void testInheritedBaseGroup() throws Exception {
        ExternalUser externalUser = idp.getUser(USER_ID);
        sync(externalUser, SyncResult.Status.ADD);

        Authorizable user = userManager.getAuthorizable(USER_ID);

        // verify group 'base'
        Set<String> expDeclaredMemberIds = ImmutableSet.of(AUTO_GROUPS, AUTO_USERS, "a", "b");
        assertExpectedIds(expDeclaredMemberIds, base.getDeclaredMembers());
        assertFalse(base.isDeclaredMember(user));

        Set<String> expMemberIds = ImmutableSet.of(USER_ID, AUTO_GROUPS, AUTO_USERS, "a", "b", "c", "aa", "aaa");
       assertExpectedIds(expMemberIds, base.getMembers());
        assertTrue(base.isMember(user));
    }

    @Test
    public void testInheritedBase2Group() throws Exception {
        ExternalUser externalUser = idp.getUser(USER_ID);
        sync(externalUser, SyncResult.Status.ADD);

        Authorizable user = userManager.getAuthorizable(USER_ID);
        
        // verify group 'base2'    
        Set<String> expDeclaredMemberIds = ImmutableSet.of(AUTO_USERS);
        assertExpectedIds(expDeclaredMemberIds, base2.getDeclaredMembers());

        assertFalse(base2.isDeclaredMember(user));

        Set<String> expMemberIds = ImmutableSet.of(USER_ID, AUTO_USERS);
        assertExpectedIds(expMemberIds, base2.getMembers());
        assertTrue(base2.isMember(user));
    }

    private static void assertIsMember(@NotNull Group group, boolean declared, @NotNull Authorizable... members) {
        try {
            for (Authorizable member : members) {
                if (declared) {
                    assertTrue(group.isDeclaredMember(member));
                } else {
                    assertTrue(group.isMember(member));
                }
            }
        } catch (RepositoryException e) {
            fail(e.getMessage());
        }
    }
    
    private static void assertExpectedIds(@NotNull Set<String> expectedIds, @NotNull Iterator<? extends Authorizable>... iterators) {
        for (Iterator<? extends Authorizable> it : iterators) {
            List<String> ids = getIds(it);
            assertEquals("Expected "+expectedIds+" found "+ids, expectedIds.size(), ids.size());
            assertTrue("Expected "+expectedIds+" found "+ids, ids.containsAll(expectedIds));
        }
    }
}