/*
 * 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.api.security.user;

import org.apache.jackrabbit.test.NotExecutableException;

import javax.jcr.RepositoryException;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

/**
 * <code>GroupTest</code>...
 */
public class GroupTest extends AbstractUserTest {

    private static void assertTrueIsMember(Iterator<Authorizable> members, Authorizable auth) throws RepositoryException {
        boolean contained = false;
        while (members.hasNext() && !contained) {
            Object next = members.next();
            assertTrue(next instanceof Authorizable);
            contained = ((Authorizable)next).getID().equals(auth.getID());
        }
        assertTrue("The given set of members must contain '" + auth.getID() + "'", contained);
    }

    private static void assertFalseIsMember(Iterator<Authorizable> members, Authorizable auth) throws RepositoryException {
        boolean contained = false;
        while (members.hasNext() && !contained) {
            Object next = members.next();
            assertTrue(next instanceof Authorizable);
            contained = ((Authorizable)next).getID().equals(auth.getID());
        }
        assertFalse("The given set of members must not contain '" + auth.getID() + "'", contained);
    }

    private static void assertTrueMemberOfContainsGroup(Iterator<Group> groups, Group gr) throws RepositoryException {
        boolean contained = false;
        while (groups.hasNext() && !contained) {
            Object next = groups.next();
            assertTrue(next instanceof Group);
            contained = ((Group)next).getID().equals(gr.getID());
        }
        assertTrue("All members of a group must contain that group upon 'memberOf'.", contained);
    }

    private static void assertFalseMemberOfContainsGroup(Iterator<Group> groups, Group gr) throws RepositoryException {
        boolean contained = false;
        while (groups.hasNext() && !contained) {
            Object next = groups.next();
            assertTrue(next instanceof Group);
            contained = ((Group)next).getID().equals(gr.getID());
        }
        assertFalse("All members of a group must contain that group upon 'memberOf'.", contained);
    }

    public void testIsGroup() throws NotExecutableException, RepositoryException {
        Group gr = getTestGroup(superuser);
        assertTrue(gr.isGroup());
    }

    public void testGetDeclaredMembers() throws NotExecutableException, RepositoryException {
        Group gr = getTestGroup(superuser);
        Iterator<Authorizable> it = gr.getDeclaredMembers();
        assertNotNull(it);
        while (it.hasNext()) {
            assertTrue(it.next() != null);
        }
    }

    public void testGetMembers() throws NotExecutableException, RepositoryException {
        Group gr = getTestGroup(superuser);
        Iterator<Authorizable> it = gr.getMembers();
        assertNotNull(it);
        while (it.hasNext()) {
            assertTrue(it.next() != null);
        }
    }

    public void testGetMembersAgainstIsMember() throws NotExecutableException, RepositoryException {
        Group gr = getTestGroup(superuser);

        Iterator<Authorizable> it = gr.getMembers();
        while (it.hasNext()) {
            Authorizable auth = it.next();
            assertTrue(gr.isMember(auth));
        }
    }

    public void testGetMembersAgainstMemberOf() throws NotExecutableException, RepositoryException {
        Group gr = getTestGroup(superuser);

        Iterator<Authorizable> it = gr.getMembers();
        while (it.hasNext()) {
            Authorizable auth = it.next();
            assertTrueMemberOfContainsGroup(auth.memberOf(), gr);
        }
    }

    public void testGetDeclaredMembersAgainstDeclaredMemberOf() throws NotExecutableException, RepositoryException {
        Group gr = getTestGroup(superuser);

        Iterator<Authorizable> it = gr.getDeclaredMembers();
        while (it.hasNext()) {
            Authorizable auth = it.next();
            assertTrueMemberOfContainsGroup(auth.declaredMemberOf(), gr);
        }
    }

    public void testGetMembersContainsDeclaredMembers() throws NotExecutableException, RepositoryException {
        Group gr = getTestGroup(superuser);
        List<String> l = new ArrayList<String>();
        for (Iterator<Authorizable> it = gr.getMembers(); it.hasNext();) {
            l.add(it.next().getID());
        }
        for (Iterator<Authorizable> it = gr.getDeclaredMembers(); it.hasNext();) {
            assertTrue("All declared members must also be part of the Iterator " +
                    "returned upon getMembers()",l.contains(it.next().getID()));
        }
    }

    public void testAddMember() throws NotExecutableException, RepositoryException {
        User auth = getTestUser(superuser);
        Group newGroup = null;
        try {
            newGroup = userMgr.createGroup(getTestPrincipal());
            save(superuser);

            assertFalse(newGroup.isMember(auth));
            assertFalse(newGroup.removeMember(auth));
            save(superuser);

            assertTrue(newGroup.addMember(auth));
            save(superuser);
            assertTrue(newGroup.isMember(auth));
            assertTrue(newGroup.isMember(userMgr.getAuthorizable(auth.getID())));

        } finally {
            if (newGroup != null) {
                newGroup.removeMember(auth);
                newGroup.remove();
                save(superuser);
            }
        }
    }

    public void testAddMembers() throws NotExecutableException, RepositoryException {
        User auth = getTestUser(superuser);
        Group newGroup = null;
        int size = 100;
        List<User> users = new ArrayList<User>(size);
        try {
            newGroup = userMgr.createGroup(getTestPrincipal());
            save(superuser);

            for (int k = 0; k < size; k++) {
                users.add(userMgr.createUser("user_" + k, "pass_" + k));
            }
            save(superuser);

            for (User user : users) {
                assertTrue(newGroup.addMember(user));
            }
            save(superuser);

            for (User user : users) {
                assertTrue(newGroup.isMember(user));
            }

            for (User user : users) {
                assertTrue(newGroup.removeMember(user));
            }
            save(superuser);

            for (User user : users) {
                assertFalse(newGroup.isMember(user));
            }
        } finally {
            for (User user : users) {
                user.remove();
                save(superuser);
            }
            if (newGroup != null) {
                newGroup.removeMember(auth);
                newGroup.remove();
                save(superuser);
            }
        }
    }

    public void testAddRemoveMember() throws NotExecutableException, RepositoryException {
        User auth = getTestUser(superuser);
        Group newGroup1 = null;
        Group newGroup2 = null;
        try {
            newGroup1 = userMgr.createGroup(getTestPrincipal());
            newGroup2 = userMgr.createGroup(getTestPrincipal());
            save(superuser);

            assertFalse(newGroup1.isMember(auth));
            assertFalse(newGroup1.removeMember(auth));
            save(superuser);
            assertFalse(newGroup2.isMember(auth));
            assertFalse(newGroup2.removeMember(auth));
            save(superuser);

            assertTrue(newGroup1.addMember(auth));
            save(superuser);
            assertTrue(newGroup1.isMember(auth));
            assertTrue(newGroup1.isMember(userMgr.getAuthorizable(auth.getID())));

            assertTrue(newGroup2.addMember(auth));
            save(superuser);
            assertTrue(newGroup2.isMember(auth));
            assertTrue(newGroup2.isMember(userMgr.getAuthorizable(auth.getID())));

            assertTrue(newGroup1.removeMember(auth));
            save(superuser);
            assertTrue(newGroup2.removeMember(auth));
            save(superuser);

            assertTrue(newGroup1.addMember(auth));
            save(superuser);
            assertTrue(newGroup1.isMember(auth));
            assertTrue(newGroup1.isMember(userMgr.getAuthorizable(auth.getID())));
            assertTrue(newGroup1.removeMember(auth));
            save(superuser);


        } finally {
            if (newGroup1 != null) {
                newGroup1.removeMember(auth);
                newGroup1.remove();
                save(superuser);
            }
            if (newGroup2 != null) {
                newGroup2.removeMember(auth);
                newGroup2.remove();
                save(superuser);
            }
        }
    }

    public void testIsDeclaredMember() throws RepositoryException, NotExecutableException {
        User auth = getTestUser(superuser);
        Group newGroup1 = null;
        Group newGroup2 = null;
        try {
            newGroup1 = userMgr.createGroup(getTestPrincipal());
            newGroup2 = userMgr.createGroup(getTestPrincipal());
            save(superuser);

            assertFalse(newGroup1.isDeclaredMember(auth));
            assertFalse(newGroup2.isDeclaredMember(auth));

            assertTrue(newGroup2.addMember(auth));
            save(superuser);
            assertTrue(newGroup2.isDeclaredMember(auth));
            assertTrue(newGroup2.isDeclaredMember(userMgr.getAuthorizable(auth.getID())));

            assertTrue(newGroup1.addMember(newGroup2));
            save(superuser);
            assertTrue(newGroup1.isDeclaredMember(newGroup2));
            assertTrue(newGroup1.isDeclaredMember(userMgr.getAuthorizable(newGroup2.getID())));
            assertTrue(newGroup1.isMember(auth));
            assertTrue(newGroup1.isMember(userMgr.getAuthorizable(auth.getID())));
            assertFalse(newGroup1.isDeclaredMember(auth));
            assertFalse(newGroup1.isDeclaredMember(userMgr.getAuthorizable(auth.getID())));
        } finally {
            if (newGroup1 != null) {
                newGroup1.remove();
                save(superuser);
            }
            if (newGroup2 != null) {
                newGroup2.remove();
                save(superuser);
            }
        }
    }

    public void testAddMemberTwice() throws NotExecutableException, RepositoryException {
        User auth = getTestUser(superuser);
        Group newGroup = null;
        try {
            newGroup = userMgr.createGroup(getTestPrincipal());
            save(superuser);

            assertTrue(newGroup.addMember(auth));
            save(superuser);
            assertFalse(newGroup.addMember(auth));
            save(superuser);
            assertTrue(newGroup.isMember(auth));

        } finally {
            if (newGroup != null) {
                newGroup.removeMember(auth);
                newGroup.remove();
                save(superuser);
            }
        }
    }

    public void testAddMemberModifiesMemberOf() throws NotExecutableException, RepositoryException {
        User auth = getTestUser(superuser);
        Group newGroup = null;
        try {
            newGroup = userMgr.createGroup(getTestPrincipal());
            save(superuser);

            assertFalseMemberOfContainsGroup(auth.memberOf(), newGroup);
            assertTrue(newGroup.addMember(auth));
            save(superuser);

            assertTrueMemberOfContainsGroup(auth.declaredMemberOf(), newGroup);
            assertTrueMemberOfContainsGroup(auth.memberOf(), newGroup);
        } finally {
            if (newGroup != null) {
                newGroup.removeMember(auth);
                newGroup.remove();
                save(superuser);
            }
        }
    }

    public void testAddMemberModifiesGetMembers() throws NotExecutableException, RepositoryException {
        User auth = getTestUser(superuser);
        Group newGroup = null;
        try {
            newGroup = userMgr.createGroup(getTestPrincipal());
            save(superuser);

            assertFalseIsMember(newGroup.getMembers(), auth);
            assertFalseIsMember(newGroup.getDeclaredMembers(), auth);
            assertTrue(newGroup.addMember(auth));
            save(superuser);

            assertTrueIsMember(newGroup.getMembers(), auth);
            assertTrueIsMember(newGroup.getDeclaredMembers(), auth);
        } finally {
            if (newGroup != null) {
                newGroup.removeMember(auth);
                newGroup.remove();
                save(superuser);
            }
        }
    }

    public void testIndirectMembers() throws NotExecutableException, RepositoryException {
        User auth = getTestUser(superuser);
        Group newGroup = null;
        Group newGroup2 = null;
        try {
            newGroup = userMgr.createGroup(getTestPrincipal());
            newGroup2 = userMgr.createGroup(getTestPrincipal());
            save(superuser);

            newGroup.addMember(newGroup2);
            save(superuser);
            assertTrue(newGroup.isMember(newGroup2));

            newGroup2.addMember(auth);
            save(superuser);

            // testuser must not be declared member of 'newGroup'
            assertFalseIsMember(newGroup.getDeclaredMembers(), auth);
            assertFalseMemberOfContainsGroup(auth.declaredMemberOf(), newGroup);

            // testuser must however be member of 'newGroup' (indirect).
            assertTrueIsMember(newGroup.getMembers(), auth);
            assertTrueMemberOfContainsGroup(auth.memberOf(), newGroup);

            // testuser cannot be removed from 'newGroup'
            assertFalse(newGroup.removeMember(auth));
            save(superuser);
        } finally {
            if (newGroup != null) {
                newGroup.removeMember(newGroup2);
                newGroup.remove();
                save(superuser);
            }
            if (newGroup2 != null) {
                newGroup2.removeMember(auth);
                newGroup2.remove();
                save(superuser);
            }
        }
    }

    public void testMembersInPrincipal() throws NotExecutableException, RepositoryException {
        User auth = getTestUser(superuser);
        Group newGroup = null;
        Group newGroup2 = null;
        try {
            newGroup = userMgr.createGroup(getTestPrincipal());
            newGroup2 = userMgr.createGroup(getTestPrincipal());
            save(superuser);

            newGroup.addMember(newGroup2);
            save(superuser);
            newGroup2.addMember(auth);
            save(superuser);

            java.security.acl.Group ngPrincipal = (java.security.acl.Group) newGroup.getPrincipal();
            java.security.acl.Group ng2Principal = (java.security.acl.Group) newGroup2.getPrincipal();

            assertFalse(ng2Principal.isMember(ngPrincipal));

            // newGroup2 must be member of newGroup's principal
            assertTrue(ngPrincipal.isMember(newGroup2.getPrincipal()));

            // testuser must be member of newGroup2's and newGroup's principal (indirect)
            assertTrue(ng2Principal.isMember(auth.getPrincipal()));
            assertTrue(ngPrincipal.isMember(auth.getPrincipal()));

        } finally {
            if (newGroup != null) {
                newGroup.removeMember(newGroup2);
                newGroup.remove();
                save(superuser);
            }
            if (newGroup2 != null) {
                newGroup2.removeMember(auth);
                newGroup2.remove();
                save(superuser);
            }
        }
    }

    public void testDeeplyNestedGroups() throws NotExecutableException, RepositoryException {
        Set<Group> groups = new HashSet<Group>();
        try {
            User auth = getTestUser(superuser);
            Group topGroup = userMgr.createGroup(getTestPrincipal());

            // Create chain of nested groups with auth member of bottom group
            Group bottomGroup = topGroup;
            for (int k = 0; k < 100; k++) {
                Group g = userMgr.createGroup(getTestPrincipal());
                groups.add(g);
                bottomGroup.addMember(g);
                bottomGroup = g;
            }
            bottomGroup.addMember(auth);

            // Check that every groups has exactly one member
            for (Group g : groups) {
                Iterator<Authorizable> declaredMembers = g.getDeclaredMembers();
                assertTrue(declaredMembers.hasNext());
                declaredMembers.next();
                assertFalse(declaredMembers.hasNext());
            }

            // Check that we get all members from the getMembers call
            HashSet<Group> allGroups = new HashSet<Group>(groups);
            for (Iterator<Authorizable> it = topGroup.getMembers(); it.hasNext(); ) {
                Authorizable a = it.next();
                assertTrue(a.equals(auth) || allGroups.remove(a));
            }
            assertTrue(allGroups.isEmpty());
        } finally {
            for (Group g : groups) {
                g.remove();
            }
        }
    }

    public void testInheritedMembers() throws Exception {
        Set<Authorizable> authorizables = new HashSet<Authorizable>();
        try {
            User testUser = userMgr.createUser(getTestPrincipal().getName(), "pw");
            authorizables.add(testUser);
            Group group1 = userMgr.createGroup(getTestPrincipal());
            authorizables.add(group1);
            Group group2 = userMgr.createGroup(getTestPrincipal());
            authorizables.add(group2);
            Group group3 = userMgr.createGroup(getTestPrincipal());

            group1.addMember(testUser);
            group2.addMember(testUser);
            group3.addMember(group1);
            group3.addMember(group2);

            Iterator<Authorizable> members = group3.getMembers();
            while (members.hasNext()) {
                Authorizable a = members.next();
                assertTrue(authorizables.contains(a));
                assertTrue(authorizables.remove(a));
            }

            assertTrue(authorizables.isEmpty());
        } finally {
            for (Authorizable a : authorizables) {
                a.remove();
            }
        }
    }

    public void testCyclicGroups() throws AuthorizableExistsException, RepositoryException, NotExecutableException {
        Group group1 = null;
        Group group2 = null;
        Group group3 = null;
        try {
            group1 = userMgr.createGroup(getTestPrincipal());
            group2 = userMgr.createGroup(getTestPrincipal());
            group3 = userMgr.createGroup(getTestPrincipal());

            group1.addMember(getTestUser(superuser));
            group2.addMember(getTestUser(superuser));

            assertTrue(group1.addMember(group2));
            assertTrue(group2.addMember(group3));
            assertFalse(group3.addMember(group1));
        }
        finally {
            if (group1 != null) group1.remove();
            if (group2 != null) group2.remove();
            if (group3 != null) group3.remove();
        }
    }

    public void testRemoveMemberTwice() throws NotExecutableException, RepositoryException {
        User auth = getTestUser(superuser);
        Group newGroup = null;
        try {
            newGroup = userMgr.createGroup(getTestPrincipal());
            save(superuser);

            assertTrue(newGroup.addMember(auth));
            save(superuser);
            assertTrue(newGroup.removeMember(userMgr.getAuthorizable(auth.getID())));
            save(superuser);
            assertFalse(newGroup.removeMember(auth));
            save(superuser);
        } finally {
            if (newGroup != null) {
                newGroup.remove();
                save(superuser);
            }
        }
    }

    public void testAddItselfAsMember() throws RepositoryException, NotExecutableException {
        Group newGroup = null;
        try {
            newGroup = userMgr.createGroup(getTestPrincipal());
            save(superuser);

            assertFalse(newGroup.addMember(newGroup));
            save(superuser);
            newGroup.removeMember(newGroup);
            save(superuser);
        } finally {
            if (newGroup != null) {
                newGroup.remove();
                save(superuser);
            }
        }
    }

    /**
     * Removing a GroupImpl must be possible even if there are still existing
     * members present.
     *
     * @throws RepositoryException if an error occurs
     * @throws NotExecutableException if not executable
     */
    public void testRemoveGroupIfMemberExist() throws RepositoryException, NotExecutableException {
        User auth = getTestUser(superuser);
        String newGroupId = null;

        try {
            Group newGroup = userMgr.createGroup(getTestPrincipal());
            save(superuser);
            newGroupId = newGroup.getID();

            assertTrue(newGroup.addMember(auth));
            newGroup.remove();
            save(superuser);
        } finally {
            Group gr = (Group) userMgr.getAuthorizable(newGroupId);
            if (gr != null) {
                gr.removeMember(auth);
                gr.remove();
                save(superuser);
            }
        }
    }

    public void testRemoveGroupClearsMembership() throws NotExecutableException, RepositoryException {
        User auth = getTestUser(superuser);
        Group newGroup = null;
        String groupId;
        try {
            newGroup = userMgr.createGroup(getTestPrincipal());
            groupId = newGroup.getID();
            save(superuser);

            assertTrue(newGroup.addMember(auth));
            save(superuser);

            boolean isMember = false;
            Iterator<Group> it = auth.declaredMemberOf();
            while (it.hasNext() && !isMember) {
                isMember = groupId.equals(it.next().getID());
            }
            assertTrue(isMember);

        } finally {
            if (newGroup != null) {
                newGroup.remove();
                save(superuser);
            }
        }

        Iterator<Group> it = auth.declaredMemberOf();
        while (it.hasNext()) {
            assertFalse(groupId.equals(it.next().getID()));
        }

        it = auth.memberOf();
        while (it.hasNext()) {
            assertFalse(groupId.equals(it.next().getID()));
        }
    }

}
