/*
 * 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 static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.equalTo;
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.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.util.Collections;
import java.util.List;
import java.util.Random;

import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.commons.json.JsopBuilder;
import org.apache.jackrabbit.oak.plugins.document.DocumentNodeState.Children;
import org.apache.jackrabbit.oak.plugins.document.util.MongoConnection;
import org.apache.jackrabbit.oak.plugins.document.util.Utils;
import org.junit.Rule;
import org.junit.Test;

/**
 * A set of simple tests.
 */
public class SimpleTest {

    @Rule
    public DocumentMKBuilderProvider builderProvider = new DocumentMKBuilderProvider();

    @Rule
    public MongoConnectionFactory connectionFactory = new MongoConnectionFactory();

    private static final boolean MONGO_DB = false;
    // private static final boolean MONGO_DB = true;

    @Test
    public void pathToId() {
        assertEquals("0:/", Utils.getIdFromPath("/"));
        assertEquals("/", Utils.getPathFromId("0:/"));
        assertEquals("1:/test", Utils.getIdFromPath("/test"));
        assertEquals("/test", Utils.getPathFromId("1:/test"));
        assertEquals("10:/1/2/3/3/4/6/7/8/9/a", Utils.getIdFromPath("/1/2/3/3/4/6/7/8/9/a"));
        assertEquals("/1/2/3/3/4/6/7/8/9/a", Utils.getPathFromId("10:/1/2/3/3/4/6/7/8/9/a"));
    }

    @Test
    public void pathDepth() {
        assertEquals(0, Utils.pathDepth(""));
        assertEquals(0, Utils.pathDepth("/"));
        assertEquals(1, Utils.pathDepth("1/"));
        assertEquals(2, Utils.pathDepth("/a/"));
        assertEquals(2, Utils.pathDepth("/a/b"));
        assertEquals(3, Utils.pathDepth("/a/b/c"));
        assertEquals(2, Utils.pathDepth("a/b/c"));
    }

    @Test
    public void addNodeGetNode() {
        DocumentMK mk = builderProvider.newBuilder().open();
        DocumentStore s = mk.getDocumentStore();
        DocumentNodeStore ns = mk.getNodeStore();
        RevisionVector rev = RevisionVector.fromString(mk.getHeadRevision());
        DocumentNodeState n = new DocumentNodeState(ns, Path.fromString("/test"), rev,
                Collections.singleton(ns.createPropertyState("name", "\"Hello\"")), false, null);
        UpdateOp op = n.asOperation(rev.getRevision(ns.getClusterId()));
        // mark as commit root
        NodeDocument.setRevision(op, rev.getRevision(ns.getClusterId()), "c");
        assertTrue(s.create(Collection.NODES, List.of(op)));
        DocumentNodeState n2 = ns.getNode(Path.fromString("/test"), rev);
        assertNotNull(n2);
        PropertyState p = n2.getProperty("name");
        assertNotNull(p);
        assertEquals("Hello", p.getValue(Type.STRING));
    }

    @Test
    public void nodeIdentifier() {
        DocumentMK mk = createMK(true);

        String rev0 = mk.getHeadRevision();
        String rev1 = mk.commit("/", "+\"test\":{}", null, null);
        String rev2 = mk.commit("/test", "+\"a\":{}", null, null);
        String rev3 = mk.commit("/test", "+\"b\":{}", null, null);
        String rev4 = mk.commit("/test", "^\"a/x\":1", null, null);

        String r0 = mk.getNodes("/", rev0, 0, 0, Integer.MAX_VALUE, ":id");
        assertEquals("{\":id\":\"/@r1-0-1\",\":childNodeCount\":0}", r0);
        String r1 = mk.getNodes("/", rev1, 0, 0, Integer.MAX_VALUE, ":id");
        assertEquals("{\":id\":\"/@r2-0-1\",\"test\":{},\":childNodeCount\":1}", r1);
        String r2 = mk.getNodes("/", rev2, 0, 0, Integer.MAX_VALUE, ":id");
        assertEquals("{\":id\":\"/@r3-0-1\",\"test\":{},\":childNodeCount\":1}", r2);
        String r3;
        r3 = mk.getNodes("/", rev3, 0, 0, Integer.MAX_VALUE, ":id");
        assertEquals("{\":id\":\"/@r4-0-1\",\"test\":{},\":childNodeCount\":1}", r3);
        r3 = mk.getNodes("/test", rev3, 0, 0, Integer.MAX_VALUE, ":id");
        assertEquals("{\":id\":\"/test@r4-0-1\",\"a\":{},\"b\":{},\":childNodeCount\":2}", r3);
        String r4;
        r4 = mk.getNodes("/", rev4, 0, 0, Integer.MAX_VALUE, ":id");
        assertEquals("{\":id\":\"/@r5-0-1\",\"test\":{},\":childNodeCount\":1}", r4);
        r4 = mk.getNodes("/test", rev4, 0, 0, Integer.MAX_VALUE, ":id");
        assertEquals("{\":id\":\"/test@r5-0-1\",\"a\":{},\"b\":{},\":childNodeCount\":2}", r4);
        r4 = mk.getNodes("/test/a", rev4, 0, 0, Integer.MAX_VALUE, ":id");
        assertEquals("{\":id\":\"/test/a@r5-0-1\",\"x\":1,\":childNodeCount\":0}", r4);
        r4 = mk.getNodes("/test/b", rev4, 0, 0, Integer.MAX_VALUE, ":id");
        assertEquals("{\":id\":\"/test/b@r4-0-1\",\":childNodeCount\":0}", r4);
    }

    @Test
    public void conflict() {
        DocumentMK mk = createMK();
        mk.commit("/", "+\"a\": {}", null, null);
        try {
            mk.commit("/", "+\"b\": {}  +\"a\": {}", null, null);
            fail();
        } catch (DocumentStoreException e) {
            // expected
        }
        // the previous commit should be rolled back now,
        // so this should work
        mk.commit("/", "+\"b\": {}", null, null);
    }

    @Test
    public void diff() {
        DocumentMK mk = createMK();

        String rev0 = mk.getHeadRevision();
        String rev1 = mk.commit("/", "+\"t1\":{}", null, null);
        String rev2 = mk.commit("/", "+\"t2\":{}", null, null);
        String rev3 = mk.commit("/", "+\"t3\":{}", null, null);
        String rev4 = mk.commit("/", "^\"t3/x\":1", null, null);

        String r0 = mk.getNodes("/", rev0, 0, 0, Integer.MAX_VALUE, null);
        assertEquals("{\":childNodeCount\":0}", r0);
        String r1 = mk.getNodes("/", rev1, 0, 0, Integer.MAX_VALUE, null);
        assertEquals("{\"t1\":{},\":childNodeCount\":1}", r1);
        String r2 = mk.getNodes("/", rev2, 0, 0, Integer.MAX_VALUE, null);
        assertEquals("{\"t1\":{},\"t2\":{},\":childNodeCount\":2}", r2);
        String r3 = mk.getNodes("/", rev3, 0, 0, Integer.MAX_VALUE, null);
        assertEquals("{\"t1\":{},\"t2\":{},\"t3\":{},\":childNodeCount\":3}", r3);

        String diff01 = mk.diff(rev0, rev1, "/", 0).trim();
        assertEquals("+\"/t1\":{}", diff01);
        String diff12 = mk.diff(rev1, rev2, "/", 0).trim();
        assertEquals("+\"/t2\":{}", diff12);
        String diff23 = mk.diff(rev2, rev3, "/", 0).trim();
        assertEquals("+\"/t3\":{}", diff23);
        String diff13 = mk.diff(rev1, rev3, "/", 0).trim();
        assertThat(diff13, anyOf(equalTo("+\"/t2\":{}+\"/t3\":{}"), equalTo("+\"/t3\":{}+\"/t2\":{}")));
        String diff34 = mk.diff(rev3, rev4, "/", 0).trim();
        assertEquals("^\"/t3\":{}", diff34);
    }

    @Test
    public void reAddDeleted() {
        DocumentMK mk = createMK();
        String rev0 = mk.getHeadRevision();
        String rev1 = mk.commit("/", "+\"test\":{\"name\": \"Hello\"} ^ \"x\": 1", null, null);
        String rev2 = mk.commit("/", "-\"test\" ^ \"x\": 2", null, null);
        String rev3 = mk.commit("/", "+\"test\":{\"name\": \"Hallo\"} ^ \"x\": 3", null, null);
        String test0 = mk.getNodes("/test", rev0, 0, 0, Integer.MAX_VALUE, null);
        assertNull(null, test0);
        String test1 = mk.getNodes("/test", rev1, 0, 0, Integer.MAX_VALUE, null);
        assertEquals("{\"name\":\"Hello\",\":childNodeCount\":0}", test1);
        String test2 = mk.getNodes("/test", rev2, 0, 0, Integer.MAX_VALUE, null);
        assertNull(null, test2);
        String test3 = mk.getNodes("/test", rev3, 0, 0, Integer.MAX_VALUE, null);
        assertEquals("{\"name\":\"Hallo\",\":childNodeCount\":0}", test3);
        mk.dispose();
    }

    @Test
    public void reAddDeleted2() {
        DocumentMK mk = createMK();
        String rev = mk.commit("/", "+\"test\":{\"x\":\"1\",\"child\": {}}", null, null);
        rev = mk.commit("/", "-\"test\"", rev, null);
        rev = mk.commit("/", "+\"test\":{}  +\"test2\": {}", null, null);
        String test = mk.getNodes("/test", rev, 0, 0, Integer.MAX_VALUE, null);
        assertEquals("{\":childNodeCount\":0}", test);
        String test2 = mk.getNodes("/test2", rev, 0, 0, Integer.MAX_VALUE, null);
        assertEquals("{\":childNodeCount\":0}", test2);
    }

    @Test
    public void move() {
        DocumentMK mk = createMK();
        String rev = mk.commit("/", "+\"test\":{\"x\":\"1\",\"child\": {}}", null, null);
        rev = mk.commit("/", ">\"test\": \"/test2\"", rev, null);
        String test = mk.getNodes("/test2", rev, 0, 0, Integer.MAX_VALUE, null);
        assertEquals("{\"x\":\"1\",\"child\":{},\":childNodeCount\":1}", test);
        test = mk.getNodes("/test", rev, 0, 0, Integer.MAX_VALUE, null);
        assertNull(test);
    }

    @Test
    public void copy() {
        DocumentMK mk = createMK();
        String rev = mk.commit("/", "+\"test\":{\"x\":\"1\",\"child\": {}}", null, null);
        rev = mk.commit("/", "*\"test\": \"/test2\"", rev, null);
        String test = mk.getNodes("/test2", rev, 0, 0, Integer.MAX_VALUE, null);
        assertEquals("{\"x\":\"1\",\"child\":{},\":childNodeCount\":1}", test);
        test = mk.getNodes("/test", rev, 0, 0, Integer.MAX_VALUE, null);
        assertEquals("{\"x\":\"1\",\"child\":{},\":childNodeCount\":1}", test);
    }

    @Test
    public void escapePropertyName() {
        DocumentMK mk = createMK();
        String rev = mk.commit(
                "/", "+\"test1\":{\"name.first\": \"Hello\"} +\"test2\":{\"_id\": \"a\"} +\"test3\":{\"$x\": \"1\"}", null, null);
        String test1 = mk.getNodes("/test1", rev, 0, 0, Integer.MAX_VALUE, null);
        assertEquals("{\"name.first\":\"Hello\",\":childNodeCount\":0}", test1);
        String test2 = mk.getNodes("/test2", rev, 0, 0, Integer.MAX_VALUE, null);
        assertEquals("{\"_id\":\"a\",\":childNodeCount\":0}", test2);
        String test3 = mk.getNodes("/test3", rev, 0, 0, Integer.MAX_VALUE, null);
        assertEquals("{\"$x\":\"1\",\":childNodeCount\":0}", test3);
        mk.dispose();
    }

    @Test
    public void commit() {
        DocumentMK mk = createMK();
        DocumentNodeStore ns = mk.getNodeStore();

        String rev = mk.commit("/", "+\"test\":{\"name\": \"Hello\"}", null, null);
        String test = mk.getNodes("/test", rev, 0, 0, Integer.MAX_VALUE, null);
        assertEquals("{\"name\":\"Hello\",\":childNodeCount\":0}", test);

        String r0 = mk.commit("/test", "+\"a\":{\"name\": \"World\"}", null, null);
        String r1 = mk.commit("/test", "+\"b\":{\"name\": \"!\"}", null, null);
        test = mk.getNodes("/test", r0, 0, 0, Integer.MAX_VALUE, null);
        DocumentNodeState n = ns.getNode(Path.ROOT, RevisionVector.fromString(r0));
        assertNotNull(n);
        Children c = ns.getChildren(n, "", Integer.MAX_VALUE);
        assertEquals("[test]", c.toString());
        n = ns.getNode(Path.fromString("/test"), RevisionVector.fromString(r1));
        assertNotNull(n);
        c = ns.getChildren(n, "", Integer.MAX_VALUE);
        assertEquals("[a, b]", c.toString());

        rev = mk.commit("", "^\"/test\":1", null, null);
        test = mk.getNodes("/", rev, 0, 0, Integer.MAX_VALUE, null);
        assertEquals("{\"test\":1,\"test\":{},\":childNodeCount\":1}", test);

        // System.out.println(test);
    }

    @Test
    public void delete() {
        DocumentMK mk = createMK();
        DocumentNodeStore ns = mk.getNodeStore();

        mk.commit("/", "+\"testDel\":{\"name\": \"Hello\"}", null, null);
        mk.commit("/testDel", "+\"a\":{\"name\": \"World\"}", null, null);
        mk.commit("/testDel", "+\"b\":{\"name\": \"!\"}", null, null);
        String r1 = mk.commit("/testDel", "+\"c\":{\"name\": \"!\"}", null, null);

        DocumentNodeState n = ns.getNode(Path.fromString("/testDel"), RevisionVector.fromString(r1));
        assertNotNull(n);
        Children c = ns.getChildren(n, "", Integer.MAX_VALUE);
        assertEquals(3, c.children.size());

        String r2 = mk.commit("/testDel", "-\"c\"", null, null);
        n = ns.getNode(Path.fromString("/testDel"), RevisionVector.fromString(r2));
        assertNotNull(n);
        c = ns.getChildren(n, "", Integer.MAX_VALUE);
        assertEquals(2, c.children.size());

        String r3 = mk.commit("/", "-\"testDel\"", null, null);
        n = ns.getNode(Path.fromString("/testDel"), RevisionVector.fromString(r3));
        assertNull(n);
    }

    @Test
    public void escapeUnescape() {
        DocumentMK mk = createMK();
        String rev;
        String nodes;
        Random r = new Random(1);
        for (int i = 0; i < 20; i++) {
            int len = 1 + r.nextInt(5);
            StringBuilder buff = new StringBuilder();
            for (int j = 0; j < len; j++) {
                buff.append((char) (32 + r.nextInt(128)));
            }
            String s = buff.toString();
            String x2 = Utils.escapePropertyName(s);
            String s2 = Utils.unescapePropertyName(x2);
            if (!s.equals(s2)) {
                assertEquals(s, s2);
            }
            if (s.indexOf('/') >= 0) {
                continue;
            }
            JsopBuilder jsop = new JsopBuilder();
            jsop.tag('+').key(s).object().key(s).value("x").endObject();
            rev = mk.commit("/", jsop.toString(),
                    null, null);
            nodes = mk.getNodes("/" + s, rev, 0, 0, 100, null);
            jsop = new JsopBuilder();
            jsop.object().key(s).value("x").
                    key(":childNodeCount").value(0).endObject();
            String n = jsop.toString();
            assertEquals(n, nodes);
            nodes = mk.getNodes("/", rev, 0, 0, 100, null);
            jsop = new JsopBuilder();
            jsop.object().key(s).object().endObject().
            key(":childNodeCount").value(1).endObject();
            n = jsop.toString();
            assertEquals(n, nodes);
            jsop = new JsopBuilder();
            jsop.tag('-').value(s);
            rev = mk.commit("/", jsop.toString(), rev, null);

        }
    }

    @Test
    public void nodeAndPropertyNames() {
        DocumentMK mk = createMK();
        String rev;
        String nodes;
        for (String s : new String[] { "_", "$", "__", "_id", "$x", ".", ".\\", "x\\", "\\x", "first.name" }) {
            String x2 = Utils.escapePropertyName(s);
            String s2 = Utils.unescapePropertyName(x2);
            if (!s.equals(s2)) {
                assertEquals(s, s2);
            }
            JsopBuilder jsop = new JsopBuilder();
            jsop.tag('+').key(s).object().key(s).value("x").endObject();
            rev = mk.commit("/", jsop.toString(),
                    null, null);
            nodes = mk.getNodes("/" + s, rev, 0, 0, 10, null);
            jsop = new JsopBuilder();
            jsop.object().key(s).value("x").
                    key(":childNodeCount").value(0).endObject();
            String n = jsop.toString();
            assertEquals(n, nodes);
            nodes = mk.getNodes("/", rev, 0, 0, 10, null);
            jsop = new JsopBuilder();
            jsop.object().key(s).object().endObject().
            key(":childNodeCount").value(1).endObject();
            n = jsop.toString();
            assertEquals(n, nodes);
            jsop = new JsopBuilder();
            jsop.tag('-').value(s);
            rev = mk.commit("/", jsop.toString(), rev, null);
        }
    }

    @Test
    public void addAndMove() {
        DocumentMK mk = createMK();

        String head = mk.getHeadRevision();
        head = mk.commit("",
                "+\"/root\":{}\n" +
                        "+\"/root/a\":{}\n"+
                        "+\"/root/a/b\":{}\n",
                head, "");

        head = mk.commit("",
                ">\"/root/a\":\"/root/c\"\n",
                head, "");

        assertFalse(mk.nodeExists("/root/a", head));
        assertTrue(mk.nodeExists("/root/c/b", head));
    }

    @Test
    public void commitRoot() {
        DocumentMK mk = createMK();
        DocumentStore store = mk.getDocumentStore();
        Revision head = Revision.fromString(mk.getHeadRevision());
        head = Revision.fromString(mk.commit("", "+\"/test\":{\"foo\":{}}", head.toString(), null));

        // root node must not have the revision
        NodeDocument rootDoc = store.find(Collection.NODES, "0:/");

        //As we update the childStatus flag the commit root would shift
        //one layer above
        // assertNotNull(rootDoc);
        // assertFalse(rootDoc.containsRevision(head));

        // test node must have head in revisions
        NodeDocument node = store.find(Collection.NODES, "1:/test");
        //assertNotNull(node);
        //assertTrue(node.containsRevision(head));

        // foo must not have head in revisions and must refer to test
        // as commit root (depth = 1)
        NodeDocument foo = store.find(Collection.NODES, "2:/test/foo");
        assertNotNull(foo);
        assertFalse(foo.containsRevision(head));
        assertEquals(Path.ROOT, foo.getCommitRootPath(head));

        head = Revision.fromString(mk.commit("", "+\"/bar\":{}+\"/test/foo/bar\":{}", head.toString(), null));

        // root node is root of commit
        rootDoc = store.find(Collection.NODES, "0:/");
        assertNotNull(rootDoc);
        assertTrue(rootDoc.containsRevision(head));

        // /bar refers to root nodes a commit root
        NodeDocument bar = store.find(Collection.NODES, "1:/bar");
        assertNotNull(bar);
        assertEquals(Path.ROOT, bar.getCommitRootPath(head));

        // /test/foo/bar refers to root nodes a commit root
        bar = store.find(Collection.NODES, "3:/test/foo/bar");
        assertNotNull(bar);
        assertEquals(Path.ROOT, bar.getCommitRootPath(head));

    }

    private DocumentMK createMK() {
        return createMK(false);
    }

    private DocumentMK createMK(boolean useSimpleRevision) {
        DocumentMK.Builder builder = builderProvider.newBuilder();

        if (MONGO_DB) {
            MongoConnection c = connectionFactory.getConnection();
            MongoUtils.dropCollections(c.getDBName());
            builder.setMongoDB(c.getMongoClient(), c.getDBName());
        }

        builder.setUseSimpleRevision(useSimpleRevision);

        return builder.open();
    }

}
