/*
 * ModeShape (http://www.modeshape.org)
 *
 * 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.modeshape.jcr.cache.document;

import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsNull.notNullValue;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.anyObject;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.modeshape.jcr.cache.CachedNode;
import org.modeshape.jcr.cache.ChildReferences;
import org.modeshape.jcr.cache.NodeCache;
import org.modeshape.jcr.cache.NodeKey;

public class NodeCacheIteratorTest {

    private final static String SOURCE_KEY = NodeKey.keyForSourceName("source");
    private final static String WORKSPACE_KEY = NodeKey.keyForSourceName("workspace");

    protected NodeCache cache;
    private List<NodeKey> allKeys;

    @Before
    public void beforeEach() {
        allKeys = new ArrayList<NodeKey>();
        cache = Mockito.mock(NodeCache.class);

        addNode("root", "node1", "node2", "node3");
        addNode("node1", "node11", "node12", "node13");
        addNode("node2", "node21", "node22", "node23");
        addNode("node3", "node31", "node32", "node33");
        addNodeWithLeafChildren("node11", "node111", "node112", "node113");
        addNodeWithLeafChildren("node12", "node121", "node122", "node123");
        addNodeWithLeafChildren("node13", "node131", "node132", "node133");
        addNodeWithLeafChildren("node21", "node211", "node212", "node213");
        addNodeWithLeafChildren("node22", "node221", "node222", "node223");
        addNodeWithLeafChildren("node23", "node231", "node232", "node233");
        addNodeWithLeafChildren("node31", "node311", "node312", "node313");
        addNodeWithLeafChildren("node32", "node321", "node322", "node323");
        addNodeWithLeafChildren("node33", "node331", "node332", "node333");

        when(cache.getAllNodeKeys()).thenAnswer(new Answer<Iterator<NodeKey>>() {
            @Override
            public Iterator<NodeKey> answer( InvocationOnMock invocation ) throws Throwable {
                // Need to create a new iterator every time ...
                return new NodeCacheIterator(cache, nodeKey("root"));
            }
        });
        when(cache.getAllNodeKeysAtAndBelow((NodeKey)anyObject())).thenAnswer(new Answer<Iterator<NodeKey>>() {
            @Override
            public Iterator<NodeKey> answer( InvocationOnMock invocation ) throws Throwable {
                // Need to create a new iterator every time ...
                NodeKey nodeKey = (NodeKey)invocation.getArguments()[0];
                return new NodeCacheIterator(cache, nodeKey);
            }
        });
    }

    @After
    public void afterEach() {
        cache = null;
    }

    @Test
    public void shouldIterateOverAllNodeKeysInTheCacheWhenUsingHasNextAndNext() {
        Set<NodeKey> expected = new HashSet<NodeKey>(allKeys);
        Iterator<NodeKey> iter = cache.getAllNodeKeys();
        while (iter.hasNext()) {
            NodeKey key = iter.next();
            assertThat(key, is(notNullValue()));
            assertThat(expected.remove(key), is(true));
        }
        assertThat(expected.isEmpty(), is(true));
    }

    @Test
    public void shouldIterateOverAllNodeKeysInTheCacheWhenUsingOnlyNext() {
        Set<NodeKey> expected = new HashSet<NodeKey>(allKeys);
        Iterator<NodeKey> iter = cache.getAllNodeKeys();
        for (int i = 0; i != allKeys.size(); ++i) {
            NodeKey key = iter.next();
            assertThat(key, is(notNullValue()));
            assertThat(expected.remove(key), is(true));
        }
        assertThat(expected.isEmpty(), is(true));
        try {
            iter.next();
            fail("Should have thrown a NoSuchElementException");
        } catch (NoSuchElementException e) {
            // expected
        }
    }

    @Test
    public void shouldIterateOverAllNodeKeysInTheCacheBelowBranchWhenUsingHasNextAndNext() {
        assertIterateOverSubtreeWhenUsingHasNextAndNext(nodeKey("node1"));
    }

    @Test
    public void shouldIterateOverAllNodeKeysInTheCacheBelowBranchWhenUsingOnlyNext() {
        assertIterateOverSubtreeWhenUsingOnlyNext(nodeKey("node1"));
    }

    protected void assertIterateOverSubtreeWhenUsingHasNextAndNext( NodeKey startingKey ) {
        Set<NodeKey> expected = findAllNodesAtOrBelow(startingKey);
        Iterator<NodeKey> iter = cache.getAllNodeKeysAtAndBelow(startingKey);
        while (iter.hasNext()) {
            NodeKey key = iter.next();
            assertThat(key, is(notNullValue()));
            assertThat(expected.remove(key), is(true));
        }
        assertThat(expected.isEmpty(), is(true));
    }

    protected void assertIterateOverSubtreeWhenUsingOnlyNext( NodeKey startingKey ) {
        Set<NodeKey> expected = findAllNodesAtOrBelow(startingKey);
        Iterator<NodeKey> iter = cache.getAllNodeKeysAtAndBelow(startingKey);
        long size = expected.size();
        for (int i = 0; i != size; ++i) {
            NodeKey key = iter.next();
            assertThat(key, is(notNullValue()));
            assertThat(expected.remove(key), is(true));
        }
        assertThat(expected.isEmpty(), is(true));
        try {
            iter.next();
            fail("Should have thrown a NoSuchElementException");
        } catch (NoSuchElementException e) {
            // expected
        }
    }

    protected Set<NodeKey> findAllNodesAtOrBelow( NodeKey key ) {
        Set<NodeKey> foundKeys = new HashSet<NodeKey>();
        findAllNodesAtOrBelow(key, foundKeys);
        return foundKeys;
    }

    protected void findAllNodesAtOrBelow( NodeKey key,
                                          Set<NodeKey> foundKeys ) {
        CachedNode node = cache.getNode(key);
        assertThat(node, is(notNullValue()));
        foundKeys.add(key);
        Iterator<NodeKey> iter = node.getChildReferences(cache).getAllKeys();
        while (iter.hasNext()) {
            findAllNodesAtOrBelow(iter.next(), foundKeys);
        }
    }

    // ----------------------------------------------------------------
    // Utility methods to construct the NodeCache ...
    // ----------------------------------------------------------------

    protected NodeKey nodeKey( String id ) {
        return new NodeKey(SOURCE_KEY, WORKSPACE_KEY, id);
    }

    protected void addNode( String key,
                            String... childKeys ) {
        // Mock the ChildReferences and stub out the only method we'll call ...
        final List<NodeKey> childKeyList = new ArrayList<NodeKey>();
        for (String childKey : childKeys) {
            childKeyList.add(nodeKey(childKey));
        }
        ChildReferences childRefs = Mockito.mock(ChildReferences.class);
        when(childRefs.getAllKeys()).thenAnswer(new Answer<Iterator<NodeKey>>() {
            @Override
            public Iterator<NodeKey> answer( InvocationOnMock invocation ) throws Throwable {
                // Need to create a new iterator every time ...
                return childKeyList.iterator();
            }
        });

        // Mock the CachedNode and stub out the only method we'll call ...
        NodeKey nodeKey = nodeKey(key);
        CachedNode node = Mockito.mock(CachedNode.class);
        when(node.getChildReferences(cache)).thenReturn(childRefs);

        // Stub the cache invocation ...
        when(cache.getNode(nodeKey)).thenReturn(node);

        // Add the key to our master list ...
        allKeys.add(nodeKey);
    }

    protected void addNodeWithLeafChildren( String key,
                                            String... childKeys ) {
        addNode(key, childKeys);
        for (String childKey : childKeys) {
            addNode(childKey);
        }
    }
}
