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

import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;

import com.google.common.collect.Lists;
import com.google.common.collect.Sets;

import org.apache.jackrabbit.oak.commons.json.JsopReader;
import org.apache.jackrabbit.oak.commons.json.JsopTokenizer;
import org.apache.jackrabbit.oak.commons.sort.StringSort;
import org.apache.jackrabbit.oak.plugins.document.memory.MemoryDocumentStore;
import org.junit.Test;

import static java.util.Collections.singleton;
import static java.util.Collections.singletonList;
import static org.apache.jackrabbit.oak.plugins.document.Collection.JOURNAL;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

/**
 * Tests for {@link JournalEntry}.
 */
public class JournalEntryTest {

    @Test
    public void applyTo() throws Exception {
        DiffCache cache = new MemoryDiffCache(new DocumentMK.Builder());
        List<Path> paths = Lists.newArrayList();
        addRandomPaths(paths);
        StringSort sort = JournalEntry.newSorter();
        add(sort, paths);
        RevisionVector from = new RevisionVector(new Revision(1, 0, 1));
        RevisionVector to = new RevisionVector(new Revision(2, 0, 1));
        sort.sort();
        JournalEntry.applyTo(sort, cache, Path.ROOT, from, to);

        for (Path p : paths) {
            String changes = cache.getChanges(from, to, p, null);
            assertNotNull("missing changes for " + p, changes);
            for (String c : getChildren(changes)) {
                assertTrue(paths.contains(new Path(p, c)));
            }
        }
        sort.close();
    }

    @Test
    public void applyToWithPath() throws Exception {
        DiffCache cache = new MemoryDiffCache(new DocumentMK.Builder());
        StringSort sort = JournalEntry.newSorter();
        sort.add("/");
        sort.add("/foo");
        sort.add("/foo/a");
        sort.add("/foo/b");
        sort.add("/bar");
        sort.add("/bar/a");
        sort.add("/bar/b");
        RevisionVector from = new RevisionVector(Revision.newRevision(1));
        RevisionVector to = new RevisionVector(Revision.newRevision(1));
        sort.sort();
        JournalEntry.applyTo(sort, cache, p("/foo"), from, to);
        assertNotNull(cache.getChanges(from, to, p("/foo"), null));
        assertNotNull(cache.getChanges(from, to, p("/foo/a"), null));
        assertNotNull(cache.getChanges(from, to, p("/foo/b"), null));
        assertNull(cache.getChanges(from, to, p("/bar"), null));
        assertNull(cache.getChanges(from, to, p("/bar/a"), null));
        assertNull(cache.getChanges(from, to, p("/bar/b"), null));
    }

    //OAK-3494
    @Test
    public void useParentDiff() throws Exception {
        DiffCache cache = new MemoryDiffCache(new DocumentMK.Builder());
        RevisionVector from = new RevisionVector(new Revision(1, 0, 1));
        RevisionVector to = new RevisionVector(new Revision(2, 0, 1));
        RevisionVector unjournalled = new RevisionVector(new Revision(3, 0, 1));

        //Put one entry for (from, to, "/a/b")->["c1", "c2"] manually
        DiffCache.Entry entry = cache.newEntry(from, to, false);
        entry.append(p("/a/b"), "^\"c1\":{}^\"c2\":{}");
        entry.done();

        //NOTE: calling validateCacheUsage fills the cache with an empty diff for the path being validated.
        //So, we need to make sure that each validation is done on a separate path.

        //Cases that cache can answer (only c1 and c2 sub-trees are possibly changed)
        validateCacheUsage(cache, from, to, "/a/b/c3", true);
        validateCacheUsage(cache, from, to, "/a/b/c4/e/f/g", true);

        //Cases that cache can't answer
        validateCacheUsage(cache, from, to, "/a/b/c1", false); //cached entry says that c1 sub-tree is changed
        validateCacheUsage(cache, from, to, "/a/b/c2/d", false); //cached entry says that c2 sub-tree is changed
        validateCacheUsage(cache, from, to, "/c", false);//there is no cache entry for the whole hierarchy

        //Fill cache using journal
        List<Path> paths = Lists.newArrayList(
                p("/content/changed"),
                p("/content/changed1/child1")
        );
        StringSort sort = JournalEntry.newSorter();
        add(sort, paths);
        sort.sort();
        JournalEntry.applyTo(sort, cache, Path.ROOT, from, to);

        validateCacheUsage(cache, from, to, "/topUnchanged", true);
        validateCacheUsage(cache, from, to, "/content/changed/unchangedLeaf", true);
        validateCacheUsage(cache, from, to, "/content/changed1/child2", true);

        //check against an unjournalled revision (essentially empty cache)
        validateCacheUsage(cache, from, unjournalled, "/unjournalledPath", false);
    }

    @Test
    public void fillExternalChanges() throws Exception {
        DocumentStore store = new MemoryDocumentStore();
        JournalEntry entry = JOURNAL.newDocument(store);
        Set<Path> paths = Sets.newHashSet();
        addRandomPaths(paths);
        entry.modified(paths);
        Revision r1 = new Revision(1, 0, 1);
        Revision r2 = new Revision(2, 0, 1);
        Revision r3 = new Revision(3, 0, 1);
        UpdateOp op = entry.asUpdateOp(r2);
        assertTrue(store.create(JOURNAL, Collections.singletonList(op)));

        StringSort sort = JournalEntry.newSorter();
        StringSort inv = JournalEntry.newSorter();
        JournalEntry.fillExternalChanges(sort, inv, r2, r3, store);
        assertEquals(0, sort.getSize());
        assertEquals(0, inv.getSize());

        JournalEntry.fillExternalChanges(sort, inv, r1, r2, store);
        assertEquals(paths.size(), sort.getSize());
        assertEquals(0, inv.getSize());
        sort.close();

        sort = JournalEntry.newSorter();
        JournalEntry.fillExternalChanges(sort, inv, r1, r3, store);
        assertEquals(paths.size(), sort.getSize());
        assertEquals(0, inv.getSize());
        sort.close();
        inv.close();
    }

    @Test
    public void invalidateOnly() throws Exception {
        DocumentStore store = new MemoryDocumentStore();
        JournalEntry invalidateEntry = JOURNAL.newDocument(store);
        Set<Path> paths = Sets.newHashSet();
        addRandomPaths(paths);
        invalidateEntry.modified(paths);
        Revision r1 = new Revision(1, 0, 1);
        Revision r2 = new Revision(2, 0, 1);
        Revision r3 = new Revision(3, 0, 1);
        UpdateOp op = invalidateEntry.asUpdateOp(r1.asBranchRevision());
        assertTrue(store.create(JOURNAL, singletonList(op)));

        JournalEntry entry = JOURNAL.newDocument(store);
        entry.invalidate(singleton(r1));
        op = entry.asUpdateOp(r2);
        assertTrue(store.create(JOURNAL, singletonList(op)));

        StringSort sort = JournalEntry.newSorter();
        StringSort inv = JournalEntry.newSorter();
        JournalEntry.fillExternalChanges(sort, inv, r2, r3, store);
        assertEquals(0, sort.getSize());
        assertEquals(0, inv.getSize());

        JournalEntry.fillExternalChanges(sort, inv, r1, r2, store);
        assertEquals(1, sort.getSize());
        assertEquals(paths.size(), inv.getSize());
        inv.close();
        sort.close();

        sort = JournalEntry.newSorter();
        inv = JournalEntry.newSorter();
        JournalEntry.fillExternalChanges(sort, inv, r1, r3, store);
        assertEquals(1, sort.getSize());
        assertEquals(paths.size(), inv.getSize());
        sort.close();
        inv.close();
    }

    @Test
    public void fillExternalChanges2() throws Exception {
        Revision r1 = new Revision(1, 0, 1);
        Revision r2 = new Revision(2, 0, 1);
        Revision r3 = new Revision(3, 0, 1);
        Revision r4 = new Revision(4, 0, 1);
        DocumentStore store = new MemoryDocumentStore();
        JournalEntry entry = JOURNAL.newDocument(store);
        entry.modified(p("/"));
        entry.modified(p("/foo"));
        UpdateOp op = entry.asUpdateOp(r2);
        assertTrue(store.create(JOURNAL, Collections.singletonList(op)));

        entry = JOURNAL.newDocument(store);
        entry.modified(p("/"));
        entry.modified(p("/bar"));
        op = entry.asUpdateOp(r4);
        assertTrue(store.create(JOURNAL, Collections.singletonList(op)));

        StringSort sort = externalChanges(r1, r1, store);
        assertEquals(0, sort.getSize());
        sort.close();

        sort = externalChanges(r1, r2, store);
        assertEquals(Sets.newHashSet("/", "/foo"), Sets.newHashSet(sort));
        sort.close();

        sort = externalChanges(r1, r3, store);
        assertEquals(Sets.newHashSet("/", "/foo", "/bar"), Sets.newHashSet(sort));
        sort.close();

        sort = externalChanges(r1, r4, store);
        assertEquals(Sets.newHashSet("/", "/foo", "/bar"), Sets.newHashSet(sort));
        sort.close();

        sort = externalChanges(r2, r2, store);
        assertEquals(0, sort.getSize());
        sort.close();

        sort = externalChanges(r2, r3, store);
        assertEquals(Sets.newHashSet("/", "/bar"), Sets.newHashSet(sort));
        sort.close();

        sort = externalChanges(r2, r4, store);
        assertEquals(Sets.newHashSet("/", "/bar"), Sets.newHashSet(sort));
        sort.close();

        sort = externalChanges(r3, r3, store);
        assertEquals(0, sort.getSize());
        sort.close();

        sort = externalChanges(r3, r4, store);
        assertEquals(Sets.newHashSet("/", "/bar"), Sets.newHashSet(sort));
        sort.close();

        sort = externalChanges(r4, r4, store);
        assertEquals(0, sort.getSize());
        sort.close();
    }

    @Test
    public void fillExternalChangesWithPath() throws Exception {
        Revision r1 = new Revision(1, 0, 1);
        Revision r2 = new Revision(2, 0, 1);
        DocumentStore store = new MemoryDocumentStore();
        JournalEntry entry = JOURNAL.newDocument(store);
        entry.modified(p("/"));
        entry.modified(p("/foo"));
        entry.modified(p("/foo/a"));
        entry.modified(p("/foo/b"));
        entry.modified(p("/foo/c"));
        entry.modified(p("/bar"));
        entry.modified(p("/bar/a"));
        entry.modified(p("/bar/b"));
        entry.modified(p("/bar/c"));

        UpdateOp op = entry.asUpdateOp(r2);
        assertTrue(store.create(JOURNAL, Collections.singletonList(op)));

        StringSort sort = JournalEntry.newSorter();
        StringSort inv = JournalEntry.newSorter();
        JournalEntry.fillExternalChanges(sort, inv, p("/foo"), r1, r2, store, e -> {}, null, null);
        assertEquals(4, sort.getSize());
        assertEquals(0, inv.getSize());
        sort.close();
        inv.close();
    }

    @Test
    public void getRevisionTimestamp() throws Exception {
        DocumentStore store = new MemoryDocumentStore();
        JournalEntry entry = JOURNAL.newDocument(store);
        entry.modified(p("/foo"));
        Revision r = Revision.newRevision(1);
        assertTrue(store.create(JOURNAL,
                Collections.singletonList(entry.asUpdateOp(r))));
        entry = store.find(JOURNAL, JournalEntry.asId(r));
        assertEquals(r.getTimestamp(), entry.getRevisionTimestamp());
    }

    // OAK-4682
    @Test
    public void concurrentModification() throws Exception {
        DocumentNodeStore store = new DocumentMK.Builder().getNodeStore();
        try {
            final JournalEntry entry = store.getCurrentJournalEntry();
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 100000; i++) {
                        entry.modified(p("/node-" + i));
                    }
                }
            });
            t.start();
            StringSort sort = JournalEntry.newSorter();
            try {
                entry.addTo(sort, Path.ROOT);
            } finally {
                sort.close();
            }
            t.join();
        } finally {
            store.dispose();
        }
    }

    @Test
    public void addToWithPath() throws Exception {
        DocumentStore store = new MemoryDocumentStore();
        JournalEntry entry = JOURNAL.newDocument(store);
        entry.modified(p("/"));
        entry.modified(p("/foo"));
        entry.modified(p("/foo/a"));
        entry.modified(p("/foo/b"));
        entry.modified(p("/foo/c"));
        entry.modified(p("/bar"));
        entry.modified(p("/bar/a"));
        entry.modified(p("/bar/b"));
        entry.modified(p("/bar/c"));
        StringSort sort = JournalEntry.newSorter();
        entry.addTo(sort, p("/foo"));
        assertEquals(4, sort.getSize());
        sort.close();
    }

    @Test
    public void countUpdatedPaths() {
        DocumentStore store = new MemoryDocumentStore();
        JournalEntry entry = JOURNAL.newDocument(store);

        assertEquals("Incorrect number of initial paths", 0, entry.getNumChangedNodes());
        assertFalse("Incorrect hasChanges", entry.hasChanges());

        entry.modified(p("/foo"));
        entry.modified(p("/bar"));
        assertEquals("Incorrect number of paths", 2, entry.getNumChangedNodes());
        assertTrue("Incorrect hasChanges", entry.hasChanges());

        entry.modified(Arrays.asList(p("/foo1"), p("/bar1")));
        assertEquals("Incorrect number of paths", 4, entry.getNumChangedNodes());
        assertTrue("Incorrect hasChanges", entry.hasChanges());

        entry.modified(p("/foo/bar2"));
        assertEquals("Incorrect number of paths", 5, entry.getNumChangedNodes());
        assertTrue("Incorrect hasChanges", entry.hasChanges());

        entry.modified(p("/foo3/bar3"));
        assertEquals("Incorrect number of paths", 7, entry.getNumChangedNodes());
        assertTrue("Incorrect hasChanges", entry.hasChanges());

        entry.modified(Arrays.asList(p("/foo/bar4"), p("/foo5/bar5")));
        assertEquals("Incorrect number of paths", 10, entry.getNumChangedNodes());
        assertTrue("Incorrect hasChanges", entry.hasChanges());
    }

    @Test
    public void branchAdditionMarksChanges() {
        DocumentStore store = new MemoryDocumentStore();
        JournalEntry entry = JOURNAL.newDocument(store);

        assertFalse("Incorrect hasChanges", entry.hasChanges());

        entry.branchCommit(Collections.<Revision>emptyList());
        assertFalse("Incorrect hasChanges", entry.hasChanges());

        entry.branchCommit(Collections.singleton(Revision.fromString("r123-0-1")));
        assertTrue("Incorrect hasChanges", entry.hasChanges());
    }

    @Test
    public void invalidationPathsMarksChanges() {
        DocumentStore store = new MemoryDocumentStore();
        JournalEntry entry = JOURNAL.newDocument(store);

        assertFalse("Incorrect hasChanges", entry.hasChanges());

        entry.invalidate(Collections.emptyList());
        assertFalse("Incorrect hasChanges", entry.hasChanges());

        entry.invalidate(Collections.singleton(Revision.fromString("r123-0-1")));
        assertTrue("Incorrect hasChanges", entry.hasChanges());
    }

    @Test
    public void emptyBranchCommit() {
        DocumentStore store = new MemoryDocumentStore();
        JournalEntry entry = JOURNAL.newDocument(store);

        entry.branchCommit(Collections.<Revision>emptyList());
        assertFalse(entry.getBranchCommits().iterator().hasNext());
        assertNull(entry.get(JournalEntry.BRANCH_COMMITS));
    }

    private static void addRandomPaths(java.util.Collection<Path> paths) throws IOException {
        paths.add(Path.ROOT);
        Random random = new Random(42);
        for (int i = 0; i < 1000; i++) {
            Path path = Path.ROOT;
            int depth = random.nextInt(6);
            for (int j = 0; j < depth; j++) {
                char name = (char) ('a' + random.nextInt(26));
                path = new Path(path, String.valueOf(name));
                paths.add(path);
            }
        }
    }

    private static void add(StringSort sort, List<Path> paths)
            throws IOException {
        for (Path p : paths) {
            sort.add(p.toString());
        }
    }

    private static List<String> getChildren(String diff) {
        List<String> children = Lists.newArrayList();
        JsopTokenizer t = new JsopTokenizer(diff);
        for (;;) {
            int r = t.read();
            switch (r) {
                case '^': {
                    children.add(t.readString());
                    t.read(':');
                    t.read('{');
                    t.read('}');
                    break;
                }
                case JsopReader.END: {
                    return children;
                }
                default:
                    fail("Unexpected token: " + r);
            }
        }
    }

    private void validateCacheUsage(DiffCache cache,
                                    RevisionVector from,
                                    RevisionVector to,
                                    String path,
                                    boolean cacheExpected) {
        Path p = p(path);
        String nonLoaderDiff = cache.getChanges(from, to, p, null);
        final AtomicBoolean loaderCalled = new AtomicBoolean(false);
        cache.getChanges(from, to, p, new DiffCache.Loader() {
            @Override
            public String call() {
                loaderCalled.set(true);
                return "";
            }
        });

        if (cacheExpected) {
            assertNotNull(nonLoaderDiff);
            assertFalse(loaderCalled.get());
        } else {
            assertNull(nonLoaderDiff);
            assertTrue(loaderCalled.get());
        }
    }

    private static StringSort externalChanges(Revision from,
                                              Revision to,
                                              DocumentStore store)
            throws IOException {
        StringSort changes = JournalEntry.newSorter();
        StringSort invalidate = JournalEntry.newSorter();
        try {
            JournalEntry.fillExternalChanges(changes, invalidate, from, to, store);
        } finally {
            invalidate.close();
        } return changes;
    }
    
    private static Path p(String path) {
        return Path.fromString(path);
    }
}
