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

import static org.apache.jackrabbit.oak.OakAssert.assertSequence;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

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

import org.apache.jackrabbit.oak.Oak;
import org.apache.jackrabbit.oak.OakBaseTest;
import org.apache.jackrabbit.oak.fixture.NodeStoreFixture;
import org.apache.jackrabbit.oak.plugins.commit.AnnotatingConflictHandler;
import org.apache.jackrabbit.oak.plugins.commit.ChildOrderConflictHandler;
import org.apache.jackrabbit.oak.plugins.commit.ConflictValidatorProvider;
import org.apache.jackrabbit.oak.spi.commit.CompositeConflictHandler;
import org.apache.jackrabbit.oak.spi.commit.ConflictHandlers;
import org.apache.jackrabbit.oak.spi.security.OpenSecurityProvider;
import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

/**
 * Contains tests related to {@link Tree}
 */
public class TreeTest extends OakBaseTest {

    private ContentRepository repository;

    public TreeTest(NodeStoreFixture fixture) {
        super(fixture);
    }

    @Before
    public void setUp() {
        repository = new Oak(store)
            .with(new OpenSecurityProvider())
            .with(new CompositeConflictHandler(List.of(
                    ConflictHandlers.wrap(new ChildOrderConflictHandler() {
                        /**
                         * Allow deleting changed node.
                         * See {@link TreeTest#removeWithConcurrentOrderBefore()}
                         */
                        @Override
                        public Resolution deleteChangedNode(NodeBuilder parent,
                                String name,
                                NodeState theirs) {
                            return Resolution.OURS;
                        }
                    }),
                    new AnnotatingConflictHandler()
            )))
            .with(new ConflictValidatorProvider())
            .createContentRepository();
    }

    @After
    public void tearDown() {
        repository = null;
    }

    @Test
    public void orderBefore() throws Exception {
        ContentSession s = repository.login(null, null);
        try {
            Root r = s.getLatestRoot();
            Tree t = r.getTree("/");
            t.addChild("node1");
            t.addChild("node2");
            t.addChild("node3");
            r.commit();

            t.getChild("node1").orderBefore("node2");
            t.getChild("node3").orderBefore(null);
            assertSequence(t.getChildren(), "node1", "node2", "node3");
            r.commit();
            // check again after commit
            assertSequence(t.getChildren(), "node1", "node2", "node3");

            t.getChild("node3").orderBefore("node2");
            assertSequence(t.getChildren(), "node1", "node3", "node2");
            r.commit();
            assertSequence(t.getChildren(), "node1", "node3", "node2");

            t.getChild("node1").orderBefore(null);
            assertSequence(t.getChildren(), "node3", "node2", "node1");
            r.commit();
            assertSequence(t.getChildren(), "node3", "node2", "node1");

            // :childOrder property invisible?
            assertTrue(t.getProperty(":childOrder") == null);
            assertEquals("must not have any properties", 0, t.getPropertyCount());
        } finally {
            s.close();
        }
    }

    @Test
    public void concurrentOrderBefore() throws Exception {
        ContentSession s1 = repository.login(null, null);
        try {
            Root r1 = s1.getLatestRoot();
            Tree t1 = r1.getTree("/");
            t1.addChild("node1");
            t1.addChild("node2");
            t1.addChild("node3");
            r1.commit();

            ContentSession s2 = repository.login(null, null);
            try {
                Root r2 = s2.getLatestRoot();
                Tree t2 = r2.getTree("/");

                t1.getChild("node2").orderBefore("node1");
                t1.getChild("node3").orderBefore(null);
                r1.commit();
                assertSequence(t1.getChildren(), "node2", "node1", "node3");

                t2.getChild("node3").orderBefore("node1");
                t2.getChild("node2").orderBefore(null);
                r2.commit();
                // other session wins
                assertSequence(t2.getChildren(), "node2", "node1", "node3");

                // try again on current root
                t2.getChild("node3").orderBefore("node1");
                t2.getChild("node2").orderBefore(null);
                r2.commit();
                assertSequence(t2.getChildren(), "node3", "node1", "node2");

            } finally {
                s2.close();
            }
        } finally {
            s1.close();
        }
    }

    @Test
    public void concurrentOrderBeforeWithAdd() throws Exception {
        ContentSession s1 = repository.login(null, null);
        try {
            Root r1 = s1.getLatestRoot();
            Tree t1 = r1.getTree("/");
            t1.addChild("node1");
            t1.addChild("node2");
            t1.addChild("node3");
            r1.commit();

            ContentSession s2 = repository.login(null, null);
            try {
                Root r2 = s2.getLatestRoot();
                Tree t2 = r2.getTree("/");

                t1.getChild("node2").orderBefore("node1");
                t1.getChild("node3").orderBefore(null);
                t1.addChild("node4");
                r1.commit();
                assertSequence(t1.getChildren(), "node2", "node1", "node3", "node4");

                t2.getChild("node3").orderBefore("node1");
                r2.commit();
                // other session wins
                assertSequence(t2.getChildren(), "node2", "node1", "node3", "node4");

                // try again on current root
                t2.getChild("node3").orderBefore("node1");
                r2.commit();
                assertSequence(t2.getChildren(), "node2", "node3", "node1", "node4");

            } finally {
                s2.close();
            }
        } finally {
            s1.close();
        }
    }

    @Test
    public void concurrentOrderBeforeWithRemove() throws Exception {
        ContentSession s1 = repository.login(null, null);
        try {
            Root r1 = s1.getLatestRoot();
            Tree t1 = r1.getTree("/");
            t1.addChild("node1");
            t1.addChild("node2");
            t1.addChild("node3");
            t1.addChild("node4");
            r1.commit();

            ContentSession s2 = repository.login(null, null);
            try {
                Root r2 = s2.getLatestRoot();
                Tree t2 = r2.getTree("/");

                t1.getChild("node2").orderBefore("node1");
                t1.getChild("node3").orderBefore(null);
                t1.getChild("node4").remove();
                r1.commit();
                assertSequence(t1.getChildren(), "node2", "node1", "node3");

                t2.getChild("node3").orderBefore("node1");
                r2.commit();
                // other session wins
                assertSequence(t2.getChildren(), "node2", "node1", "node3");

                // try again on current root
                t2.getChild("node3").orderBefore("node1");
                r2.commit();
                assertSequence(t2.getChildren(), "node2", "node3", "node1");

            } finally {
                s2.close();
            }
        } finally {
            s1.close();
        }
    }

    @Test
    public void concurrentOrderBeforeWithRemoveOtherSession() throws Exception {
        ContentSession s1 = repository.login(null, null);
        try {
            Root r1 = s1.getLatestRoot();
            Tree t1 = r1.getTree("/");
            t1.addChild("node1").orderBefore(null);
            t1.addChild("node2");
            t1.addChild("node3");
            t1.addChild("node4");
            r1.commit();

            ContentSession s2 = repository.login(null, null);
            try {
                Root r2 = s2.getLatestRoot();
                Tree t2 = r2.getTree("/");

                t1.getChild("node2").orderBefore("node1");
                t1.getChild("node3").orderBefore(null);
                r1.commit();
                assertSequence(t1.getChildren(), "node2", "node1", "node4", "node3");

                t2.getChild("node3").orderBefore("node1");
                t2.getChild("node4").remove();
                r2.commit();
                // other session wins wrt ordering, but node4 is gone
                assertSequence(t2.getChildren(), "node2", "node1", "node3");

                // try reorder again on current root
                t2.getChild("node3").orderBefore("node1");
                r2.commit();
                assertSequence(t2.getChildren(), "node2", "node3", "node1");

            } finally {
                s2.close();
            }
        } finally {
            s1.close();
        }
    }

    @Test
    public void concurrentOrderBeforeRemoved() throws Exception {
        ContentSession s1 = repository.login(null, null);
        try {
            Root r1 = s1.getLatestRoot();
            Tree t1 = r1.getTree("/");
            t1.addChild("node1");
            t1.addChild("node2");
            t1.addChild("node3");
            r1.commit();

            ContentSession s2 = repository.login(null, null);
            try {
                Root r2 = s2.getLatestRoot();
                Tree t2 = r2.getTree("/");

                t1.getChild("node2").orderBefore("node1");
                t1.getChild("node3").remove();
                r1.commit();
                assertSequence(t1.getChildren(), "node2", "node1");

                t2.getChild("node3").orderBefore("node1");
                r2.commit();
                assertSequence(t2.getChildren(), "node2", "node1");

            } finally {
                s2.close();
            }
        } finally {
            s1.close();
        }
    }

    @Test
    public void concurrentOrderBeforeAllRemoved() throws Exception {
        ContentSession s1 = repository.login(null, null);
        try {
            Root r1 = s1.getLatestRoot();
            Tree t1 = r1.getTree("/").addChild("c");
            t1.addChild("node1").orderBefore(null);
            t1.addChild("node2");
            t1.addChild("node3");
            r1.commit();

            ContentSession s2 = repository.login(null, null);
            try {
                Root r2 = s2.getLatestRoot();
                Tree t2 = r2.getTree("/c");

                t1.remove();
                // now 'c' does not have ordered children anymore
                r1.getTree("/").addChild("c");
                r1.commit();
                assertSequence(t1.getChildren());

                t2.getChild("node3").orderBefore("node1");
                r2.commit();
                assertSequence(t2.getChildren());

            } finally {
                s2.close();
            }
        } finally {
            s1.close();
        }
    }

    @Test
    public void concurrentOrderBeforeTargetRemoved() throws Exception {
        ContentSession s1 = repository.login(null, null);
        try {
            Root r1 = s1.getLatestRoot();
            Tree t1 = r1.getTree("/");
            t1.addChild("node1").orderBefore(null);
            t1.addChild("node2");
            t1.addChild("node3");
            t1.addChild("node4");
            r1.commit();

            ContentSession s2 = repository.login(null, null);
            try {
                Root r2 = s2.getLatestRoot();
                Tree t2 = r2.getTree("/");

                t1.getChild("node2").orderBefore("node1");
                t1.getChild("node3").remove();
                r1.commit();
                assertSequence(t1.getChildren(), "node2", "node1", "node4");

                t2.getChild("node4").orderBefore("node3");
                r2.commit();
                assertSequence(t2.getChildren(), "node2", "node1", "node4");

            } finally {
                s2.close();
            }
        } finally {
            s1.close();
        }
    }

    @Test
    public void concurrentAddChildOrderable() throws Exception {
        ContentSession s1 = repository.login(null, null);
        try {
            Root r1 = s1.getLatestRoot();
            Tree t1 = r1.getTree("/");
            t1.addChild("node1").orderBefore(null);
            t1.addChild("node2");
            r1.commit();
            ContentSession s2 = repository.login(null, null);
            try {
                Root r2 = s2.getLatestRoot();
                Tree t2 = r2.getTree("/");

                t1 = r1.getTree("/");
                // node3 from s1
                t1.addChild("node3");
                r1.commit();

                // node4 from s2
                t2.addChild("node4");
                r2.commit();

                t1 = s1.getLatestRoot().getTree("/");
                assertSequence(
                        t1.getChildren(), "node1", "node2", "node3", "node4");
            } finally {
                s2.close();
            }
        } finally {
            s1.close();
        }

    }

    @Test
    public void concurrentAddChildMakeOrderable() throws Exception {
        ContentSession s1 = repository.login(null, null);
        try {
            Root r1 = s1.getLatestRoot();
            Tree t1 = r1.getTree("/");
            t1.addChild("node1");
            t1.addChild("node2");
            r1.commit();
            ContentSession s2 = repository.login(null, null);
            try {
                Root r2 = s2.getLatestRoot();
                Tree t2 = r2.getTree("/");

                t1 = r1.getTree("/");
                // node3 from s1
                t1.addChild("node3").orderBefore(null);
                r1.commit();

                // get current sequence of child names
                List<String> names = new ArrayList<>();
                for (Tree t : r1.getTree("/").getChildren()) {
                    names.add(t.getName());
                }

                // node4 from s2
                t2.addChild("node4").orderBefore(null);
                r2.commit();

                names.add("node4");

                t1 = s1.getLatestRoot().getTree("/");
                assertSequence(
                        t1.getChildren(), names.toArray(new String[names.size()]));
            } finally {
                s2.close();
            }
        } finally {
            s1.close();
        }

    }

    @Test
    public void concurrentAddChild() throws Exception {
        ContentSession s1 = repository.login(null, null);
        try {
            Root r1 = s1.getLatestRoot();
            Tree t1 = r1.getTree("/");
            t1.addChild("node1");
            t1.addChild("node2");
            t1.addChild("node3");
            r1.commit();
            ContentSession s2 = repository.login(null, null);
            try {
                Root r2 = s2.getLatestRoot();
                Tree t2 = r2.getTree("/");

                t1 = r1.getTree("/");
                // node4 from s1
                t1.addChild("node4");
                r1.commit();

                // node5 from s2
                t2.addChild("node5");
                r2.commit();

                r1 = s1.getLatestRoot();
                t1 = r1.getTree("/");
                Set<String> names = new HashSet<>();
                for (Tree t : t1.getChildren()) {
                    names.add(t.getName());
                }
                assertEquals(Set.of("node1", "node2", "node3", "node4", "node5"), names);
            } finally {
                s2.close();
            }
        } finally {
            s1.close();
        }
    }

    @Test
    public void removeWithConcurrentOrderBefore() throws Exception {
        ContentSession s1 = repository.login(null, null);
        try {
            Root r1 = s1.getLatestRoot();
            Tree t1 = r1.getTree("/").addChild("c");
            t1.addChild("node1").orderBefore(null);
            t1.addChild("node2");
            r1.commit();
            ContentSession s2 = repository.login(null, null);
            try {
                Root r2 = s2.getLatestRoot();
                Tree t2 = r2.getTree("/c");

                t1 = r1.getTree("/c");
                t1.getChild("node2").orderBefore("node1");
                r1.commit();
                assertSequence(t1.getChildren(), "node2", "node1");

                t2.remove();
                r2.commit();
                assertFalse(r2.getTree("/").hasChild("c"));

            } finally {
                s2.close();
            }
        } finally {
            s1.close();
        }
    }
}
