/*
 * 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.test.api.observation;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import org.apache.jackrabbit.test.NotExecutableException;

import javax.jcr.RepositoryException;
import javax.jcr.Node;
import javax.jcr.observation.Event;

/**
 * Tests if {@link javax.jcr.Node#orderBefore(String, String)} operations trigger
 * the appropriate observation events.
 * <p>
 * @tck.config testroot must allow orderable child nodes of type
 * <code>nodetype</code>, otherwise the test cases throw a
 * {@link NotExecutableException}. Some tests are only executed if the node
 * at <code>testroot</code> support same name sibling child nodes.
 * @tck.config nodetype node type that allows child nodes of the same type.
 * @tck.config nodename1 child node name of type <code>nodetype</code>
 * @tck.config nodename2 child node name of type <code>nodetype</code>
 * @tck.config nodename3 child node name of type <code>nodetype</code>
 *
 * @test
 * @sources NodeReorderTest.java
 * @executeClass org.apache.jackrabbit.test.api.observation.NodeReorderTest
 * @keywords observation
 */
public class NodeReorderTest extends AbstractObservationTest {

    /**
     * The key <code>srcChildRelPath</code> in the info map.
     */
    private static final String SRC_CHILD_REL_PATH = "srcChildRelPath";

    /**
     * The key <code>destChildRelPath</code> in the info map.
     */
    private static final String DEST_CHILD_REL_PATH = "destChildRelPath";

    private void doTestNodeReorder(List<Event> added, List<Event> removed, List<Event> moved)
            throws RepositoryException, NotExecutableException {
        if (!testRootNode.getDefinition().getDeclaringNodeType().hasOrderableChildNodes()) {
            throw new NotExecutableException("Node at '" + testRoot + "' does not support orderable child nodes.");
        }

        /**
         * Initial tree:
         *  + testroot
         *      + nodename1
         *      + nodename2
         *      + nodename3
         *
         * After reorder:
         *  + testroot
         *      + nodename1
         *      + nodename3
         *      + nodename2
         */
        testRootNode.addNode(nodeName1, testNodeType);
        testRootNode.addNode(nodeName2, testNodeType);
        testRootNode.addNode(nodeName3, testNodeType);
        testRootNode.getSession().save();
        EventResult addNodeListener = new EventResult(log);
        EventResult removeNodeListener = new EventResult(log);
        EventResult moveNodeListener = new EventResult(log);
        addEventListener(addNodeListener, Event.NODE_ADDED);
        addEventListener(removeNodeListener, Event.NODE_REMOVED);
        addEventListener(moveNodeListener, Event.NODE_MOVED);
        testRootNode.orderBefore(nodeName3, nodeName2);
        testRootNode.getSession().save();
        added.addAll(Arrays.asList(addNodeListener.getEvents(DEFAULT_WAIT_TIMEOUT)));
        removed.addAll(Arrays.asList(removeNodeListener.getEvents(DEFAULT_WAIT_TIMEOUT)));
        moved.addAll(Arrays.asList(moveNodeListener.getEvents(DEFAULT_WAIT_TIMEOUT)));
        removeEventListener(addNodeListener);
        removeEventListener(removeNodeListener);
        removeEventListener(moveNodeListener);
    }

    /**
     * Tests if reordering a child node triggers a {@link Event#NODE_REMOVED}
     * and a {@link Event#NODE_ADDED} event.
     */
    public void testNodeReorderAddRemove() throws RepositoryException, NotExecutableException {

        List<Event> added = new ArrayList<Event>();
        List<Event> removed = new ArrayList<Event>();
        List<Event> moved = new ArrayList<Event>();

        doTestNodeReorder(added, removed, moved);

        // either
        // 1) nodename2 has been reordered to the end
        // or:
        // 2) nodename3 has been reordered before nodename2
        // that is, the following event sets are correct:
        // 1) nodename2:remove, nodename2:add
        // or:
        // 2) nodename3:remove, nodename3:add

        // if true, check for option 1)
        boolean reorderEnd = false;
        for (Event e : added) {
            if (e.getPath().endsWith(nodeName2)) {
                reorderEnd = true;
                break;
            }
        }

        if (reorderEnd) {
            checkNodeAdded(added, new String[] { nodeName2 }, null);
            checkNodeRemoved(removed, new String[] { nodeName2 }, null);
        } else {
            checkNodeAdded(added, new String[] { nodeName3 }, null);
            checkNodeRemoved(removed, new String[] { nodeName3 }, null);
        }
    }

    /**
     * Tests if reordering a child node triggers a {@link Event#NODE_MOVED}
     * event.
     */
    public void testNodeReorderMove() throws RepositoryException, NotExecutableException {

        List<Event> added = new ArrayList<Event>();
        List<Event> removed = new ArrayList<Event>();
        List<Event> moved = new ArrayList<Event>();

        doTestNodeReorder(added, removed, moved);

        checkNodeReordered(moved, nodeName3, nodeName3, nodeName2);
    }

    /**
     * Tests if reordering a child node triggers a {@link Event#NODE_REMOVED}
     * and a {@link Event#NODE_ADDED} event with same name siblings.
     */
    public void testNodeReorderSameName()
            throws RepositoryException, NotExecutableException {
        if (!testRootNode.getDefinition().getDeclaringNodeType().hasOrderableChildNodes()) {
            throw new NotExecutableException("Node at '" + testRoot + "' does not support orderable child nodes.");
        }

        /**
         * Initial tree:
         *  + testroot
         *      + nodename1[1]
         *      + nodename1[2]
         *      + nodename1[3]
         *
         * After reorder:
         *  + testroot
         *      + nodename1[1]
         *      + nodename1[2] (was 3)
         *      + nodename1[3] (was 2)
         */
        Node n = testRootNode.addNode(nodeName1, testNodeType);
        if (!n.getDefinition().allowsSameNameSiblings()) {
            throw new NotExecutableException("Node at " + testRoot + " does not allow same name siblings with name " + nodeName1);
        }
        testRootNode.addNode(nodeName1, testNodeType);
        testRootNode.addNode(nodeName1, testNodeType);
        testRootNode.getSession().save();
        EventResult addNodeListener = new EventResult(log);
        EventResult removeNodeListener = new EventResult(log);
        EventResult moveNodeListener = new EventResult(log);
        addEventListener(addNodeListener, Event.NODE_ADDED);
        addEventListener(removeNodeListener, Event.NODE_REMOVED);
        addEventListener(moveNodeListener, Event.NODE_MOVED);
        testRootNode.orderBefore(nodeName1 + "[3]", nodeName1 + "[2]");
        //testRootNode.orderBefore(nodeName1 + "[2]", null);
        testRootNode.getSession().save();
        Event[] added = addNodeListener.getEvents(DEFAULT_WAIT_TIMEOUT);
        Event[] removed = removeNodeListener.getEvents(DEFAULT_WAIT_TIMEOUT);
        Event[] moved = moveNodeListener.getEvents(DEFAULT_WAIT_TIMEOUT);
        removeEventListener(addNodeListener);
        removeEventListener(removeNodeListener);
        removeEventListener(moveNodeListener);
        // either
        // 1) nodename1[2] has been reordered to the end
        // or:
        // 2) nodename1[3] has been reordered before nodename1[2]
        // that is, the following event sets are correct:
        // 1) nodename1[2]:remove, nodename1[3]:add
        // or:
        // 2) nodename1[3]:remove, nodename1[2]:add

        // if true, check for option 1)
        boolean reorderEnd = false;
        for (int i = 0; i < added.length; i++) {
            if (added[i].getPath().endsWith(nodeName1 + "[3]")) {
                reorderEnd = true;
                break;
            }
        }
        if (reorderEnd) {
            checkNodeAdded(added, new String[]{nodeName1 + "[3]"}, null);
            checkNodeRemoved(removed, new String[]{nodeName1 + "[2]"}, null);
            checkNodeReordered(moved, nodeName1 + "[2]", nodeName1 + "[3]", null);
        } else {
            checkNodeAdded(added, new String[]{nodeName1 + "[2]"}, null);
            checkNodeRemoved(removed, new String[]{nodeName1 + "[3]"}, null);
            checkNodeReordered(moved, nodeName1 + "[3]", nodeName1 + "[2]", nodeName1 + "[2]");
        }
    }

    /**
     * Tests if reordering a child node triggers a {@link Event#NODE_REMOVED}
     * and a {@link Event#NODE_ADDED} event with same name siblings. Furthermore
     * a node is removed in the same save scope.
     */
    public void testNodeReorderSameNameWithRemove()
            throws RepositoryException, NotExecutableException {
        if (!testRootNode.getDefinition().getDeclaringNodeType().hasOrderableChildNodes()) {
            throw new NotExecutableException("Node at '" + testRoot + "' does not support orderable child nodes.");
        }

        /**
         * Initial tree:
         *  + testroot
         *      + nodename1[1]
         *      + nodename2
         *      + nodename1[2]
         *      + nodename1[3]
         *      + nodename3
         *
         * After reorder:
         *  + testroot
         *      + nodename1[1]
         *      + nodename2
         *      + nodename1[2] (was 3)
         *      + nodename1[3] (was 2)
         */
        Node n = testRootNode.addNode(nodeName1, testNodeType);
        if (!n.getDefinition().allowsSameNameSiblings()) {
            throw new NotExecutableException("Node at " + testRoot + " does not allow same name siblings with name " + nodeName1);
        }
        testRootNode.addNode(nodeName2, testNodeType);
        testRootNode.addNode(nodeName1, testNodeType);
        testRootNode.addNode(nodeName1, testNodeType);
        testRootNode.addNode(nodeName3, testNodeType);
        testRootNode.getSession().save();
        EventResult addNodeListener = new EventResult(log);
        EventResult removeNodeListener = new EventResult(log);
        EventResult moveNodeListener = new EventResult(log);
        addEventListener(addNodeListener, Event.NODE_ADDED);
        addEventListener(removeNodeListener, Event.NODE_REMOVED);
        addEventListener(moveNodeListener, Event.NODE_MOVED);
        testRootNode.orderBefore(nodeName1 + "[2]", null);
        testRootNode.getNode(nodeName3).remove();
        testRootNode.getSession().save();
        Event[] added = addNodeListener.getEvents(DEFAULT_WAIT_TIMEOUT);
        Event[] removed = removeNodeListener.getEvents(DEFAULT_WAIT_TIMEOUT);
        Event[] moved = moveNodeListener.getEvents(DEFAULT_WAIT_TIMEOUT);
        removeEventListener(addNodeListener);
        removeEventListener(removeNodeListener);
        removeEventListener(moveNodeListener);
        // either
        // 1) nodename1[2] has been reordered to the end
        // or:
        // 2) nodename1[3] has been reordered before nodename1[2]
        //
        // that is, the following event sets are correct:
        // 1) nodename1[2]:remove, nodename1[3]:add, nodename3:remove
        // or:
        // 2) nodename1[3]:remove, nodename1[2]:add, nodename3:remove

        // if true, check for option 1)
        boolean reorderEnd = false;
        for (int i = 0; i < added.length; i++) {
            if (added[i].getPath().endsWith(nodeName1 + "[3]")) {
                reorderEnd = true;
                break;
            }
        }
        if (reorderEnd) {
            checkNodeAdded(added, new String[]{nodeName1 + "[3]"}, null);
            checkNodeRemoved(removed, new String[]{nodeName1 + "[2]", nodeName3}, null);
            checkNodeReordered(moved, nodeName1 + "[2]", nodeName1 + "[3]", null);
        } else {
            checkNodeAdded(added, new String[]{nodeName1 + "[2]"}, null);
            checkNodeRemoved(removed, new String[]{nodeName1 + "[3]", nodeName3}, null);
            checkNodeReordered(moved, nodeName1 + "[3]", nodeName1 + "[2]", nodeName1 + "[2]");
        }
    }

    /**
     * Checks <code>Events</code> for paths. All <code>relPaths</code> are
     * relative to {@link #testRoot}.
     *
     * @param events the <code>Event</code>s.
     * @param src    the source child path where the node was reordered from.
     * @param dest   the destination child path where the node was reordered to.
     * @param before the destination child path where the node was reordered before.
     * @throws RepositoryException if an error occurs while retrieving the nodes
     *                             from event instances.
     */
    protected void checkNodeReordered(Event[] events, String src,
                                      String dest, String before)
            throws RepositoryException {
        checkNodes(events, new String[]{dest}, null, Event.NODE_MOVED);
        assertEquals("Wrong number of events", 1, events.length);
        Map<?, ?> info = events[0].getInfo();
        checkInfoEntry(info, SRC_CHILD_REL_PATH, src);
        checkInfoEntry(info, DEST_CHILD_REL_PATH, before);
    }

    protected void checkNodeReordered(List<Event> events, String src, String dest, String before)
            throws RepositoryException {
        checkNodes(events.toArray(new Event[0]), new String[] { dest }, null, Event.NODE_MOVED);
        assertEquals("Wrong number of events", 1, events.size());
        Map<?, ?> info = events.get(0).getInfo();
        checkInfoEntry(info, SRC_CHILD_REL_PATH, src);
        checkInfoEntry(info, DEST_CHILD_REL_PATH, before);
    }

    /**
     * Checks if the info map contains the given <code>key</code> with the
     * <code>expected</code> value.
     *
     * @param info the event info map.
     * @param key the name of the key.
     * @param expected the expected value.
     */
    protected void checkInfoEntry(Map<?, ?> info, String key, String expected) {
        assertTrue("Missing event info key: " + key, info.containsKey(key));
        assertEquals("Wrong event info value for: " + key,
                expected, (String) info.get(key));
    }

    protected void checkNodeAdded(List<Event> events, String[] requiredRelPaths, String[] optionalRelPaths)
            throws RepositoryException {
        checkNodes(events.toArray(new Event[0]), requiredRelPaths, optionalRelPaths, Event.NODE_ADDED);
    }

    protected void checkNodeRemoved(List<Event> events, String[] requiredRelPaths, String[] optionalRelPaths)
            throws RepositoryException {
        checkNodes(events.toArray(new Event[0]), requiredRelPaths, optionalRelPaths, Event.NODE_REMOVED);
    }

}
