/*
 * 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.api.security.user.QueryBuilder.Direction;
import org.apache.jackrabbit.spi.commons.iterator.Iterators;

import javax.jcr.RepositoryException;
import javax.jcr.Value;
import java.security.Principal;
import java.util.*;
import java.util.function.Predicate;

public class UserManagerSearchTest extends AbstractUserTest {

    // users
    private User blackWidow;
    private User gardenSpider;
    private User jumpingSpider;
    private User ant;
    private User bee;
    private User fly;
    private User jackrabbit;
    private User deer;
    private User opposum;
    private User kangaroo;
    private User elephant;
    private User lemur;
    private User gibbon;
    private User crocodile;
    private User turtle;
    private User lizard;
    private User kestrel;
    private User goose;
    private User pelican;
    private User dove;
    private User salamander;
    private User goldenToad;
    private User poisonDartFrog;

    private final Set<User> users = new HashSet<User>();

    // groups
    private Group animals;
    private Group invertebrates;
    private Group arachnids;
    private Group insects;
    private Group vertebrates;
    private Group mammals;
    private Group apes;
    private Group reptiles;
    private Group birds;
    private Group amphibians;

    private final Set<Group> groups = new HashSet<Group>();
    private final Set<Authorizable> authorizables = new HashSet<Authorizable>();

    private final Set<Authorizable> systemDefined = new HashSet<Authorizable>();

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

        Iterator<Authorizable> systemAuthorizables = userMgr.findAuthorizables("rep:principalName", null);
        while (systemAuthorizables.hasNext()) {
            Authorizable authorizable =  systemAuthorizables.next();
            if (authorizable.isGroup()) {
                groups.add((Group) authorizable);
            }
            else {
                users.add((User) authorizable);
            }
            systemDefined.add(authorizable);
        }

        // Create a zoo. Please excuse my ignorance in zoology ;-)
        animals = createGroup("animals");
            invertebrates = createGroup("invertebrates");
                arachnids = createGroup("arachnids");
                insects = createGroup("insects");
            vertebrates = createGroup("vertebrates");
                mammals = createGroup("mammals");
                    apes = createGroup("apes");
                reptiles = createGroup("reptiles");
                birds = createGroup("birds");
                amphibians = createGroup("amphibians");

        animals.addMember(invertebrates);
        animals.addMember(vertebrates);
        invertebrates.addMember(arachnids);
        invertebrates.addMember(insects);
        vertebrates.addMember(mammals);
        vertebrates.addMember(reptiles);
        vertebrates.addMember(birds);
        vertebrates.addMember(amphibians);
        mammals.addMember(apes);

        blackWidow = createUser("black widow", "flies", 2, false);
        gardenSpider = createUser("garden spider", "flies", 2, false);
        jumpingSpider = createUser("jumping spider", "insects", 1, false);
        addMembers(arachnids, blackWidow, gardenSpider, jumpingSpider);

        ant = createUser("ant", "leaves", 0.5, false);
        bee = createUser("bee", "honey", 2.5, true);
        fly = createUser("fly", "dirt", 1.3, false);
        addMembers(insects, ant, bee, fly);

        jackrabbit = createUser("jackrabbit", "carrots", 2500, true);
        deer = createUser("deer", "leaves", 120000, true);
        opposum = createUser("opposum", "fruit", 1200, true);
        kangaroo = createUser("kangaroo", "grass", 90000, true);
        elephant = createUser("elephant", "leaves", 5000000, true);
        addMembers(mammals, jackrabbit, deer, opposum, kangaroo, elephant);

        lemur = createUser("lemur", "nectar", 1100, true);
        gibbon = createUser("gibbon", "meat", 20000, true);
        addMembers(apes, lemur, gibbon);

        crocodile = createUser("crocodile", "meat", 80000, false);
        turtle = createUser("turtle", "leaves", 10000, true);
        lizard = createUser("lizard", "leaves", 1900, false);
        addMembers(reptiles, crocodile, turtle, lizard);

        kestrel = createUser("kestrel", "mice", 2000, false);
        goose = createUser("goose", "snails", 13000, true);
        pelican = createUser("pelican", "fish", 15000, true);
        dove = createUser("dove", "insects", 1600, false);
        addMembers(birds, kestrel, goose, pelican, dove);

        salamander = createUser("salamander", "insects", 800, true);
        goldenToad = createUser("golden toad", "insects", 700, false);
        poisonDartFrog = createUser("poison dart frog", "insects", 40, false);
        addMembers(amphibians, salamander, goldenToad, poisonDartFrog);

        setProperty("canFly", vf.createValue(true), bee, fly, kestrel, goose, pelican, dove);
        setProperty("poisonous",vf.createValue(true), blackWidow, bee, poisonDartFrog );
        setProperty("poisonous", vf.createValue(false), turtle, lemur);
        setProperty("hasWings", vf.createValue(false), blackWidow, gardenSpider, jumpingSpider, ant,
                jackrabbit, deer, opposum, kangaroo, elephant, lemur, gibbon, crocodile, turtle, lizard,
                salamander, goldenToad, poisonDartFrog);
        setProperty("color", vf.createValue("black"), blackWidow, gardenSpider, ant, fly, lizard, salamander);
        setProperty("color", vf.createValue("WHITE"), opposum, goose, pelican, dove);
        setProperty("color", vf.createValue("gold"), goldenToad);
        setProperty("numberOfLegs", vf.createValue(2), kangaroo, gibbon, kestrel, goose, dove);
        setProperty("numberOfLegs", vf.createValue(4), jackrabbit, deer, opposum, elephant, lemur, crocodile,
                turtle, lizard, salamander, goldenToad, poisonDartFrog);
        setProperty("numberOfLegs", vf.createValue(6), ant, bee, fly);
        setProperty("numberOfLegs", vf.createValue(8), blackWidow, gardenSpider, jumpingSpider);

        elephant.getImpersonation().grantImpersonation(jackrabbit.getPrincipal());

        authorizables.addAll(users);
        authorizables.addAll(groups);

        if (!userMgr.isAutoSave()) {
            superuser.save();
        }

    }

    @Override
    public void tearDown() throws Exception {
        for (Authorizable authorizable : authorizables) {
            if (!systemDefined.contains(authorizable)) {
                authorizable.remove();
            }
        }
        authorizables.clear();
        groups.clear();
        users.clear();

        if (!userMgr.isAutoSave()) {
            superuser.save();
        }

        super.tearDown();
    }

    public void testAny() throws RepositoryException {
        Iterator<Authorizable> result = userMgr.findAuthorizables(new Query() {
            public <T> void build(QueryBuilder<T> builder) { /* any */ }
        });

        assertSameElements(result, authorizables.iterator());
    }

    public void testSelector() throws RepositoryException {
        List<Class<? extends Authorizable>> selectors = new ArrayList<Class<? extends Authorizable>>();
        selectors.add(Authorizable.class);
        selectors.add(Group.class);
        selectors.add(User.class);

        for (final Class<? extends Authorizable> s : selectors) {
            Iterator<Authorizable> result = userMgr.findAuthorizables(new Query() {
                public <T> void build(QueryBuilder<T> builder) {
                    builder.setSelector(s);
                }
            });

            if (User.class.isAssignableFrom(s)) {
                assertSameElements(result, users.iterator());
            }
            else if (Group.class.isAssignableFrom(s)) {
                assertSameElements(result, groups.iterator());
            }
            else {
                assertSameElements(result, authorizables.iterator());
            }
        }
    }

    public void testDirectScope() throws RepositoryException {
        Group[] groups = new Group[]{mammals, vertebrates, apes};
        for (final Group g : groups) {
            Iterator<Authorizable> result = userMgr.findAuthorizables(new Query() {
                public <T> void build(QueryBuilder<T> builder) {
                    try {
                        builder.setScope(g.getID(), true);
                    } catch (RepositoryException e) {
                        fail(e.getMessage());
                    }
                }
            });

            Iterator<Authorizable> members = g.getDeclaredMembers();
            assertSameElements(result, members);
        }
    }

    public void testIndirectScope() throws RepositoryException {
        Group[] groups = new Group[]{mammals, vertebrates, apes};
        for (final Group g : groups) {
            Iterator<Authorizable> result = userMgr.findAuthorizables(new Query() {
                public <T> void build(QueryBuilder<T> builder) {
                    try {
                        builder.setScope(g.getID(), false);
                    } catch (RepositoryException e) {
                        fail(e.getMessage());
                    }
                }
            });

            Iterator<Authorizable> members = g.getMembers();
            assertSameElements(result, members);
        }
    }

    public void testFindUsersInGroup() throws RepositoryException {
        Group[] groups = new Group[]{mammals, vertebrates, apes};
        for (final Group g : groups) {
            Iterator<Authorizable> result = userMgr.findAuthorizables(new Query() {
                public <T> void build(QueryBuilder<T> builder) {
                    try {
                        builder.setSelector(User.class);
                        builder.setScope(g.getID(), false);
                    } catch (RepositoryException e) {
                        fail(e.getMessage());
                    }
                }
            });

            Iterator<Authorizable> members = g.getMembers();
            Iterator<Authorizable> users = Iterators.filterIterator(members, new Predicate<Authorizable>() {
                public boolean test(Authorizable authorizable) {
                    return !authorizable.isGroup();
                }
            });
            assertSameElements(result, users);
        }
    }

    public void testFindGroupsInGroup() throws RepositoryException {
        Group[] groups = new Group[]{mammals, vertebrates, apes};
        for (final Group g : groups) {
            Iterator<Authorizable> result = userMgr.findAuthorizables(new Query() {
                public <T> void build(QueryBuilder<T> builder) {
                    try {
                        builder.setSelector(Group.class);
                        builder.setScope(g.getID(), true);
                    } catch (RepositoryException e) {
                        fail(e.getMessage());
                    }
                }
            });

            Iterator<Authorizable> members = g.getDeclaredMembers();
            Iterator<Authorizable> users = Iterators.filterIterator(members, new Predicate<Authorizable>() {
                public boolean test(Authorizable authorizable) {
                    return authorizable.isGroup();
                }
            });
            assertSameElements(result, users);
        }
    }

    public void testNameMatch() throws RepositoryException {
        Iterator<Authorizable> result = userMgr.findAuthorizables(new Query() {
            public <T> void build(QueryBuilder<T> builder) {
                builder.setCondition(builder.nameMatches("a%"));
            }
        });

        Iterator<Authorizable> expected = Iterators.filterIterator(authorizables.iterator(), new Predicate<Authorizable>() {
            public boolean test(Authorizable authorizable) {
                try {
                    String name = authorizable.getID();
                    Principal principal = authorizable.getPrincipal();
                    return name.startsWith("a") || principal != null && principal.getName().startsWith("a");
                } catch (RepositoryException e) {
                    fail(e.getMessage());
                }
                return false;
            }
        });

        assertTrue(result.hasNext());
        assertSameElements(result, expected);
    }

    public void testFindProperty1() throws RepositoryException {
        Iterator<Authorizable> result = userMgr.findAuthorizables(new Query() {
            public <T> void build(QueryBuilder<T> builder) {
                builder.setCondition(builder.
                        eq("@canFly", vf.createValue(true)));
            }
        });

        Iterator<User> expected = Iterators.filterIterator(users.iterator(), new Predicate<User>() {
            public boolean test(User user) {
                try {
                    Value[] canFly = user.getProperty("canFly");
                    return canFly != null && canFly.length == 1 && canFly[0].getBoolean();
                } catch (RepositoryException e) {
                    fail(e.getMessage());
                }
                return false;
            }
        });

        assertTrue(result.hasNext());
        assertSameElements(result, expected);
    }

    public void testFindProperty2() throws RepositoryException {
        Iterator<Authorizable> result = userMgr.findAuthorizables(new Query() {
            public <T> void build(QueryBuilder<T> builder) {
                builder.setCondition(builder.
                        gt("profile/@weight", vf.createValue(2000.0)));
            }
        });

        Iterator<User> expected = Iterators.filterIterator(users.iterator(), new Predicate<User>() {
            public boolean test(User user) {
                try {
                    Value[] weight = user.getProperty("profile/weight");
                    return weight != null && weight.length == 1 && weight[0].getDouble() > 2000.0;
                } catch (RepositoryException e) {
                    fail(e.getMessage());
                }
                return false;
            }
        });

        assertTrue(result.hasNext());
        assertSameElements(result, expected);
    }

    public void testFindProperty3() throws RepositoryException {
        Iterator<Authorizable> result = userMgr.findAuthorizables(new Query() {
            public <T> void build(QueryBuilder<T> builder) {
                builder.setCondition(builder.
                        eq("@numberOfLegs", vf.createValue(8)));
            }
        });

        Iterator<User> expected = Iterators.filterIterator(users.iterator(), new Predicate<User>() {
            public boolean test(User user) {
                try {
                    Value[] numberOfLegs = user.getProperty("numberOfLegs");
                    return numberOfLegs != null && numberOfLegs.length == 1 && numberOfLegs[0].getLong() == 8;
                } catch (RepositoryException e) {
                    fail(e.getMessage());
                }
                return false;
            }
        });

        assertTrue(result.hasNext());
        assertSameElements(result, expected);
    }

    public void testPropertyExistence() throws RepositoryException {
        Iterator<Authorizable> result = userMgr.findAuthorizables(new Query() {
            public <T> void build(QueryBuilder<T> builder) {
                builder.setCondition(builder.
                        exists("@poisonous"));
            }
        });

        Iterator<User> expected = Iterators.filterIterator(users.iterator(), new Predicate<User>() {
            public boolean test(User user) {
                try {
                    Value[] poisonous = user.getProperty("poisonous");
                    return poisonous != null && poisonous.length == 1;
                } catch (RepositoryException e) {
                    fail(e.getMessage());
                }
                return false;
            }
        });

        assertTrue(result.hasNext());
        assertSameElements(result, expected);
    }

    public void testPropertyLike() throws RepositoryException {
        Iterator<Authorizable> result = userMgr.findAuthorizables(new Query() {
            public <T> void build(QueryBuilder<T> builder) {
                builder.setCondition(builder.
                        like("profile/@food", "m%"));
            }
        });

        Iterator<User> expected = Iterators.filterIterator(users.iterator(), new Predicate<User>() {
            public boolean test(User user) {
                try {
                    Value[] food = user.getProperty("profile/food");
                    if (food == null || food.length != 1) {
                        return false;
                    }
                    else {
                        String value = food[0].getString();
                        return value.startsWith("m");
                    }
                } catch (RepositoryException e) {
                    fail(e.getMessage());
                }
                return false;
            }
        });

        assertTrue(result.hasNext());
        assertSameElements(result, expected);
    }

    public void testContains1() throws RepositoryException {
        Iterator<Authorizable> result = userMgr.findAuthorizables(new Query() {
            public <T> void build(QueryBuilder<T> builder) {
                builder.setCondition(builder.
                        contains(".", "gold"));
            }
        });

        Iterator<User> expected = Iterators.singleton(goldenToad);
        assertTrue(result.hasNext());
        assertSameElements(result, expected);
    }

    public void testContains2() throws RepositoryException {
        Iterator<Authorizable> result = userMgr.findAuthorizables(new Query() {
            public <T> void build(QueryBuilder<T> builder) {
                builder.setCondition(builder.
                        contains("@color", "gold"));
            }
        });

        Iterator<User> expected = Iterators.singleton(goldenToad);
        assertTrue(result.hasNext());
        assertSameElements(result, expected);
    }

    public void testContains3() throws RepositoryException {
        Iterator<Authorizable> result = userMgr.findAuthorizables(new Query() {
            public <T> void build(QueryBuilder<T> builder) {
                builder.setCondition(builder.
                        contains("profile/.", "grass"));
            }
        });

        Iterator<User> expected = Iterators.singleton(kangaroo);
        assertTrue(result.hasNext());
        assertSameElements(result, expected);
    }

    public void testContains4() throws RepositoryException {
        Iterator<Authorizable> result = userMgr.findAuthorizables(new Query() {
            public <T> void build(QueryBuilder<T> builder) {
                builder.setCondition(builder.
                        contains("profile/@food", "grass"));
            }
        });

        Iterator<User> expected = Iterators.singleton(kangaroo);
        assertTrue(result.hasNext());
        assertSameElements(result, expected);
    }

    public void testCondition1() throws RepositoryException {
        Iterator<Authorizable> result = userMgr.findAuthorizables(new Query() {
            public <T> void build(QueryBuilder<T> builder) {
                builder.setCondition(builder.
                        and(builder.
                            eq("profile/@cute", vf.createValue(true)), builder.
                            not(builder.
                                eq("@color", vf.createValue("black")))));
            }
        });

        Iterator<User> expected = Iterators.filterIterator(users.iterator(), new Predicate<User>() {
            public boolean test(User user) {
                try {
                    Value[] cute = user.getProperty("profile/cute");
                    Value[] black = user.getProperty("color");
                    return cute != null && cute.length == 1 && cute[0].getBoolean() &&
                           !(black != null && black.length == 1 && black[0].getString().equals("black"));
                } catch (RepositoryException e) {
                    fail(e.getMessage());
                }
                return false;
            }
        });

        assertTrue(result.hasNext());
        assertSameElements(result, expected);
    }

    public void testCondition2() throws RepositoryException {
        Iterator<Authorizable> result = userMgr.findAuthorizables(new Query() {
            public <T> void build(QueryBuilder<T> builder) {
                builder.setCondition(builder.
                        or(builder.
                            eq("profile/@food", vf.createValue("mice")), builder.
                            eq("profile/@food", vf.createValue("nectar"))));
            }
        });

        Iterator<User> expected = Iterators.filterIterator(users.iterator(), new Predicate<User>() {
            public boolean test(User user) {
                try {
                    Value[] food = user.getProperty("profile/food");
                    return food != null && food.length == 1 &&
                          (food[0].getString().equals("mice") || food[0].getString().equals("nectar"));
                } catch (RepositoryException e) {
                    fail(e.getMessage());
                }
                return false;
            }
        });

        assertTrue(result.hasNext());
        assertSameElements(result, expected);
    }

    public void testImpersonation() throws RepositoryException {
        Iterator<Authorizable> result = userMgr.findAuthorizables(new Query() {
            public <T> void build(QueryBuilder<T> builder) {
                builder.setCondition(builder.
                        impersonates("jackrabbit"));
            }
        });

        Iterator<User> expected = Iterators.singleton(elephant);
        assertTrue(result.hasNext());
        assertSameElements(result, expected);
    }

    public void testSortOrder1() throws RepositoryException {
        Iterator<Authorizable> result = userMgr.findAuthorizables(new Query() {
            public <T> void build(QueryBuilder<T> builder) {
                builder.setCondition(builder.
                        exists("@color"));
                builder.setSortOrder("@color", Direction.DESCENDING);
            }
        });

        assertTrue(result.hasNext());
        String prev = null;
        while (result.hasNext()) {
            Authorizable authorizable = result.next();
            Value[] color = authorizable.getProperty("color");
            assertNotNull(color);
            assertEquals(1, color.length);
            assertTrue(prev == null || prev.compareToIgnoreCase(color[0].getString()) >= 0);
            prev = color[0].getString();
        }
    }

    public void testSortOrder2() throws RepositoryException {
        Iterator<Authorizable> result = userMgr.findAuthorizables(new Query() {
            public <T> void build(QueryBuilder<T> builder) {
                builder.setCondition(builder.
                        exists("profile/@weight"));
                builder.setSortOrder("profile/@weight", Direction.ASCENDING, true);
            }
        });

        assertTrue(result.hasNext());
        double prev = Double.MIN_VALUE;
        while (result.hasNext()) {
            Authorizable authorizable = result.next();
            Value[] weight = authorizable.getProperty("profile/weight");
            assertNotNull(weight);
            assertEquals(1, weight.length);
            assertTrue(prev <= weight[0].getDouble());
            prev = weight[0].getDouble();
        }
    }

    public void testOffset() throws RepositoryException {
        long[] offsets = {2, 0, 3, 0,      100000};
        long[] counts =  {4, 4, 0, 100000, 100000};

        for (int k = 0; k < offsets.length; k++) {
            final long offset = offsets[k];
            final long count = counts[k];
            Iterator<Authorizable> result = userMgr.findAuthorizables(new Query() {
                public <T> void build(QueryBuilder<T> builder) {
                    builder.setSortOrder("profile/@weight", Direction.ASCENDING);
                    builder.setLimit(offset, count);
                }
            });

            Iterator<Authorizable> expected = userMgr.findAuthorizables(new Query() {
                public <T> void build(QueryBuilder<T> builder) {
                    builder.setSortOrder("profile/@weight", Direction.ASCENDING);
                }
            });

            skip(expected, offset);
            assertSame(expected, result, count);
            assertFalse(result.hasNext());
        }
    }

    public void testSetBound() throws RepositoryException {
        List<User> sortedUsers = new ArrayList<User>(users);
        sortedUsers.removeAll(systemDefined);  // remove system defined users: no @weight  

        Comparator<? super User> comp = new Comparator<User>() {
            public int compare(User user1, User user2) {
                try {
                    Value[] weight1 = user1.getProperty("profile/weight");
                    assertNotNull(weight1);
                    assertEquals(1, weight1.length);

                    Value[] weight2 = user2.getProperty("profile/weight");
                    assertNotNull(weight2);
                    assertEquals(1, weight2.length);

                    return weight1[0].getDouble() < weight2[0].getDouble() ? -1 : 1;
                } catch (RepositoryException e) {
                    fail(e.getMessage());
                    return 0;  // Make the compiler happy
                }
            }
        };
        Collections.sort(sortedUsers, comp);

        long[] counts = {4, 0, 100000};
        for (final long count : counts) {
            Iterator<Authorizable> result = userMgr.findAuthorizables(new Query() {
                public <T> void build(QueryBuilder<T> builder) {
                    builder.setCondition(builder.
                            eq("profile/@cute", vf.createValue(true)));
                    builder.setSortOrder("profile/@weight", Direction.ASCENDING, true);
                    builder.setLimit(vf.createValue(1000.0), count);
                }
            });

            Iterator<User> expected = Iterators.filterIterator(sortedUsers.iterator(), new Predicate<User>() {
                public boolean test(User user) {
                    try {
                        Value[] cute = user.getProperty("profile/cute");
                        Value[] weight = user.getProperty("profile/weight");
                        return cute != null && cute.length == 1 && cute[0].getBoolean() &&
                                weight != null && weight.length == 1 && weight[0].getDouble() > 1000.0;

                    } catch (RepositoryException e) {
                        fail(e.getMessage());
                    }
                    return false;
                }
            });

            assertSame(expected, result, count);
            assertFalse(result.hasNext());
        }
    }

    public void testScopeWithOffset() throws RepositoryException {
        final int offset = 5;
        final int count = 10000;

        Iterator<Authorizable> result = userMgr.findAuthorizables(new Query() {
            public <T> void build(QueryBuilder<T> builder) {
                builder.setScope("vertebrates", false);
                builder.setSortOrder("profile/@weight", Direction.ASCENDING);
                builder.setLimit(offset, count);
            }
        });

        Iterator<Authorizable> expected = userMgr.findAuthorizables(new Query() {
            public <T> void build(QueryBuilder<T> builder) {
                builder.setScope("vertebrates", false);
                builder.setSortOrder("profile/@weight", Direction.ASCENDING);
            }
        });

        skip(expected, offset);
        assertSame(expected, result, count);
        assertFalse(result.hasNext());
    }

    public void testScopeWithMax() throws RepositoryException {
        final int offset = 0;
        final int count = 22;

        Iterator<Authorizable> result = userMgr.findAuthorizables(new Query() {
            public <T> void build(QueryBuilder<T> builder) {
                builder.setScope("vertebrates", false);
                builder.setSortOrder("profile/@weight", Direction.ASCENDING);
                builder.setLimit(offset, count);
            }
        });

        Iterator<Authorizable> expected = userMgr.findAuthorizables(new Query() {
            public <T> void build(QueryBuilder<T> builder) {
                builder.setScope("vertebrates", false);
                builder.setSortOrder("profile/@weight", Direction.ASCENDING);
            }
        });

        assertSameElements(expected, result);
        assertFalse(result.hasNext());
    }

    //------------------------------------------< private >---

    private static void addMembers(Group group, Authorizable... authorizables) throws RepositoryException {
        for (Authorizable authorizable : authorizables) {
            group.addMember(authorizable);
        }
    }

    private Group createGroup(String name) throws RepositoryException {
        Group group = userMgr.createGroup(name);
        groups.add(group);
        return group;
    }

    private User createUser(String name, String food, double weight, boolean cute) throws RepositoryException {
        User user = userMgr.createUser(name, "");
        user.setProperty("profile/food", vf.createValue(food));
        user.setProperty("profile/weight", vf.createValue(weight));
        user.setProperty("profile/cute", vf.createValue(cute));
        users.add(user);
        return user;
    }

    private static void setProperty(String relPath, Value value, Authorizable... authorizables) throws RepositoryException {
        for (Authorizable authorizable : authorizables) {
            authorizable.setProperty(relPath, value);
        }
    }

    private static <T> void assertSameElements(Iterator<? extends T> it1, Iterator<? extends T> it2) {
        Set<? extends T> set1 = toSet(it1);
        Set<? extends T> set2 = toSet(it2);

        Set<? super T> missing = new HashSet<T>();
        missing.addAll(set2);
        missing.removeAll(set1);

        Set<? super T> excess = new HashSet<T>();
        excess.addAll(set1);
        excess.removeAll(set2);

        if (!missing.isEmpty()) {
            fail("Missing elements in query result: " + missing);
        }

        if (!excess.isEmpty()) {
            fail("Excess elements in query result: " + excess);
        }
    }

    private static <T> Set<T> toSet(Iterator<T> it) {
        Set<T> set = new HashSet<T>();
        while (it.hasNext()) {
            set.add(it.next());
        }
        return set;
    }

    private static <T> void assertSame(Iterator<? extends T> expected, Iterator<? extends T> actual, long count) {
        for (int k = 0; k < count && actual.hasNext(); k++) {
            assertTrue(expected.hasNext());
            assertEquals(expected.next(), actual.next());
        }
    }

    private static <T> void skip(Iterator<T> iterator, long count) {
        for (int k = 0; k < count && iterator.hasNext(); k++) {
            iterator.next();
        }
    }


}
