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

import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.commons.LazyValue;
import org.apache.jackrabbit.oak.commons.collections.IterableUtils;
import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeStore;
import org.apache.jackrabbit.oak.plugins.memory.PropertyStates;
import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
import org.apache.jackrabbit.oak.spi.security.authorization.permission.PermissionProvider;
import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.apache.jackrabbit.oak.spi.state.NodeStore;
import org.junit.Before;
import org.junit.Test;

import java.util.List;

import static org.apache.jackrabbit.oak.core.TestPermissionProvider.NAME_ACCESSIBLE;
import static org.apache.jackrabbit.oak.core.TestPermissionProvider.NAME_NON_ACCESSIBLE;
import static org.apache.jackrabbit.oak.core.TestPermissionProvider.NAME_NON_EXISTING;
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.assertSame;
import static org.junit.Assert.assertTrue;

public class SecureNodeBuilderTest {

    private final NodeStore store = new MemoryNodeStore();
    private final TestPermissionProvider permissionProvider = new TestPermissionProvider();

    private SecureNodeBuilder secureNodeBuilder;

    @Before
    public void before() throws Exception {
        NodeBuilder rootBuilder = store.getRoot().builder();
        rootBuilder.setProperty("prop", "value").setProperty(NAME_NON_ACCESSIBLE, "value");
        rootBuilder.child(NAME_ACCESSIBLE).setProperty("prop", "value").setProperty(NAME_NON_ACCESSIBLE, "value");
        rootBuilder.child(NAME_NON_ACCESSIBLE).setProperty("prop", "value").setProperty(NAME_NON_ACCESSIBLE, "value");
        store.merge(rootBuilder, new EmptyHook(), new CommitInfo("id", null));

        rootBuilder = store.getRoot().builder();
        secureNodeBuilder = new SecureNodeBuilder(rootBuilder, new LazyValue<PermissionProvider>() {
            @Override
            protected PermissionProvider createValue() {
                return permissionProvider;
            }
        });
    }

    @Test
    public void testGetChildNodeNonExisting() {
        NodeBuilder child = secureNodeBuilder.getChildNode(NAME_NON_EXISTING);
        assertTrue(child instanceof SecureNodeBuilder);
    }

    @Test
    public void testGetChildNode() {
        NodeBuilder child = secureNodeBuilder.getChildNode(NAME_ACCESSIBLE);
        assertTrue(child instanceof SecureNodeBuilder);
        assertTrue(child.exists());
    }

    @Test
    public void testGetChildNodeNonAccessible() {
        NodeBuilder child = secureNodeBuilder.getChildNode(NAME_NON_ACCESSIBLE);
        assertTrue(child instanceof SecureNodeBuilder);
    }

    @Test
    public void testExists() {
        assertTrue(secureNodeBuilder.exists());
    }

    @Test
    public void testExistsNonExisting() {
        assertFalse(secureNodeBuilder.getChildNode(NAME_NON_EXISTING).exists());
    }

    @Test
    public void testExistsAccessible() {
        assertTrue(secureNodeBuilder.getChildNode(NAME_ACCESSIBLE).exists());
    }

    @Test
    public void testExistsNonAccessible() {
        assertFalse(secureNodeBuilder.getChildNode(NAME_NON_ACCESSIBLE).exists());
    }

    @Test
    public void testGetBaseState() {
        NodeState ns = secureNodeBuilder.getBaseState();

        assertTrue(ns instanceof SecureNodeState);
    }

    @Test
    public void testGetBaseStateNonExisting() {
        NodeState ns = secureNodeBuilder.getChildNode(NAME_NON_EXISTING).getBaseState();

        assertTrue(ns instanceof SecureNodeState);
    }

    @Test
    public void testGetBaseStateNonAccessible() {
        NodeState ns = secureNodeBuilder.getChildNode(NAME_NON_ACCESSIBLE).getBaseState();

        assertTrue(ns instanceof SecureNodeState);
    }

    @Test
    public void testGetNodeState() {
        NodeState ns = secureNodeBuilder.getNodeState();

        assertTrue(ns instanceof SecureNodeState);
    }

    @Test
    public void testGetNodeStateNonExisting() {
        NodeState ns = secureNodeBuilder.getChildNode(NAME_NON_EXISTING).getNodeState();

        assertTrue(ns instanceof SecureNodeState);
    }

    @Test
    public void testGetNodeStateNonAccessible() {
        NodeState ns = secureNodeBuilder.getChildNode(NAME_NON_ACCESSIBLE).getNodeState();

        assertTrue(ns instanceof SecureNodeState);
    }

    /**
     * Illustrating usage of {@link SecureNodeBuilder#baseChanged()} as it is
     * currently present with {@link MutableRoot#commit()}, {@link MutableRoot#rebase()}
     * and {@link MutableRoot#refresh()}: baseChanged is call <strong>before</strong>
     * the permission provider is refreshed, which depending on the implementation
     * may lead to a stale {@code TreePermission} being obtained with the non-lazy
     * refresh as described in OAK-5355.
     *
     * @see <a href"https://issues.apache.org/jira/browse/OAK-5355">OAK-5355</a>
     */
    @Test
    public void testBaseChanged() {
        secureNodeBuilder.baseChanged();
        // change the behavior of the permission provider to assure that subsequent
        // calls are always reading the most up to date information
        try {
            permissionProvider.denyAll = true;
            assertFalse(secureNodeBuilder.exists());
            assertFalse(secureNodeBuilder.hasChildNode(NAME_ACCESSIBLE));
            assertFalse(secureNodeBuilder.hasProperty("prop"));
            assertFalse(secureNodeBuilder.isNew());
            assertFalse(secureNodeBuilder.getBaseState().exists());
            assertFalse(secureNodeBuilder.getNodeState().exists());
        } finally {
            permissionProvider.denyAll = false;
        }
    }

    @Test
    public void testHasProperty() {
        assertTrue(secureNodeBuilder.hasProperty("prop"));
        assertTrue(secureNodeBuilder.getChildNode(NAME_ACCESSIBLE).hasProperty("prop"));
        assertTrue(secureNodeBuilder.getChildNode(NAME_NON_ACCESSIBLE).hasProperty("prop"));
        assertFalse(secureNodeBuilder.getChildNode(NAME_NON_EXISTING).hasProperty("prop"));
    }

    @Test
    public void testHasPropertyNonAccessible() {
        assertFalse(secureNodeBuilder.hasProperty(NAME_NON_ACCESSIBLE));
        assertFalse(secureNodeBuilder.getChildNode(NAME_ACCESSIBLE).hasProperty(NAME_NON_ACCESSIBLE));
        assertFalse(secureNodeBuilder.getChildNode(NAME_NON_ACCESSIBLE).hasProperty(NAME_NON_ACCESSIBLE));
        assertFalse(secureNodeBuilder.getChildNode(NAME_NON_EXISTING).hasProperty(NAME_NON_ACCESSIBLE));
    }

    @Test
    public void testHasPropertyNonExisting() {
        assertFalse(secureNodeBuilder.hasProperty(NAME_NON_EXISTING));
        assertFalse(secureNodeBuilder.getChildNode(NAME_ACCESSIBLE).hasProperty(NAME_NON_EXISTING));
        assertFalse(secureNodeBuilder.getChildNode(NAME_NON_ACCESSIBLE).hasProperty(NAME_NON_EXISTING));
        assertFalse(secureNodeBuilder.getChildNode(NAME_NON_EXISTING).hasProperty(NAME_NON_EXISTING));
    }

    @Test
    public void testGetProperty() {
        assertNotNull(secureNodeBuilder.getProperty("prop"));
        assertNotNull(secureNodeBuilder.getChildNode(NAME_ACCESSIBLE).getProperty("prop"));
        assertNotNull(secureNodeBuilder.getChildNode(NAME_NON_ACCESSIBLE).getProperty("prop"));
        assertNull(secureNodeBuilder.getChildNode(NAME_NON_EXISTING).getProperty("prop"));
    }

    @Test
    public void testGetPropertyNonAccessible() {
        assertNull(secureNodeBuilder.getProperty(NAME_NON_ACCESSIBLE));
        assertNull(secureNodeBuilder.getChildNode(NAME_ACCESSIBLE).getProperty(NAME_NON_ACCESSIBLE));
        assertNull(secureNodeBuilder.getChildNode(NAME_NON_ACCESSIBLE).getProperty(NAME_NON_ACCESSIBLE));
        assertNull(secureNodeBuilder.getChildNode(NAME_NON_EXISTING).getProperty(NAME_NON_ACCESSIBLE));
    }

    @Test
    public void testGetPropertyNonExisting() {
        assertNull(secureNodeBuilder.getProperty(NAME_NON_EXISTING));
        assertNull(secureNodeBuilder.getChildNode(NAME_ACCESSIBLE).getProperty(NAME_NON_EXISTING));
        assertNull(secureNodeBuilder.getChildNode(NAME_NON_ACCESSIBLE).getProperty(NAME_NON_EXISTING));
        assertNull(secureNodeBuilder.getChildNode(NAME_NON_EXISTING).getProperty(NAME_NON_EXISTING));
    }

    @Test
    public void testGetPropertyCount() {
        assertEquals(1, secureNodeBuilder.getPropertyCount());
        assertEquals(1, secureNodeBuilder.getChildNode(NAME_ACCESSIBLE).getPropertyCount());
        assertEquals(1, secureNodeBuilder.getChildNode(NAME_NON_ACCESSIBLE).getPropertyCount());
        assertEquals(0, secureNodeBuilder.getChildNode(NAME_NON_EXISTING).getPropertyCount());
    }

    @Test
    public void testGetPropertyCountCanReadProperties() {
        try {
            permissionProvider.canReadProperties = true;
            assertEquals(2, secureNodeBuilder.getPropertyCount());
            assertEquals(2, secureNodeBuilder.getChildNode(NAME_ACCESSIBLE).getPropertyCount());
            assertEquals(2, secureNodeBuilder.getChildNode(NAME_NON_ACCESSIBLE).getPropertyCount());
            assertEquals(0, secureNodeBuilder.getChildNode(NAME_NON_EXISTING).getPropertyCount());
        } finally {
            permissionProvider.canReadProperties = false;
        }
    }

    @Test
    public void testGetProperties() {
        assertEquals(1, IterableUtils.size(secureNodeBuilder.getProperties()));
        assertEquals(1, IterableUtils.size(secureNodeBuilder.getChildNode(NAME_ACCESSIBLE).getProperties()));
        assertEquals(1, IterableUtils.size(secureNodeBuilder.getChildNode(NAME_NON_ACCESSIBLE).getProperties()));
        assertEquals(0, IterableUtils.size(secureNodeBuilder.getChildNode(NAME_NON_EXISTING).getProperties()));
    }

    @Test
    public void testGetPropertiesCanReadProperties() {
        try {
            permissionProvider.canReadProperties = true;
            assertEquals(2, IterableUtils.size(secureNodeBuilder.getProperties()));
            assertEquals(2, IterableUtils.size(secureNodeBuilder.getChildNode(NAME_ACCESSIBLE).getProperties()));
            assertEquals(2, IterableUtils.size(secureNodeBuilder.getChildNode(NAME_NON_ACCESSIBLE).getProperties()));
            assertEquals(0, IterableUtils.size(secureNodeBuilder.getChildNode(NAME_NON_EXISTING).getProperties()));
        } finally {
            permissionProvider.canReadProperties = false;
        }
    }

    @Test
    public void testGetPropertiesAfterSet() {
        secureNodeBuilder.setProperty(PropertyStates.createProperty("another", List.of("v", "v2"), Type.STRINGS));
        assertEquals(2, IterableUtils.size(secureNodeBuilder.getProperties()));
    }

    @Test
    public void testGetPropertiesAfterRemoval() {
        secureNodeBuilder.removeProperty("prop");
        assertEquals(0, IterableUtils.size(secureNodeBuilder.getProperties()));
    }

    @Test
    public void testGetBoolean() {
        assertFalse(secureNodeBuilder.getBoolean("prop"));
        assertFalse(secureNodeBuilder.getBoolean(NAME_NON_EXISTING));
        assertFalse(secureNodeBuilder.getBoolean(NAME_NON_ACCESSIBLE));
    }

    @Test
    public void testGetBooleanTypeBoolean() {
        secureNodeBuilder.setProperty("boolean", true, Type.BOOLEAN);
        assertTrue(secureNodeBuilder.getBoolean("boolean"));
    }

    @Test
    public void testGetBooleanTypeBooleans() {
        secureNodeBuilder.setProperty("booleans", List.of(true, false), Type.BOOLEANS);
        assertFalse(secureNodeBuilder.getBoolean("booleans"));
    }

    @Test
    public void testGetString() {
        assertEquals("value", secureNodeBuilder.getString("prop"));
        assertNull(secureNodeBuilder.getString(NAME_NON_EXISTING));
        assertNull(secureNodeBuilder.getString(NAME_NON_ACCESSIBLE));
    }

    @Test
    public void testGetStringTypeStrings() {
        secureNodeBuilder.setProperty("strings", List.of("a", "b"), Type.STRINGS);
        assertNull(secureNodeBuilder.getString("strings"));
    }

    @Test
    public void testGetStringTypeLong() {
        secureNodeBuilder.setProperty("long", Long.MAX_VALUE, Type.LONG);
        assertNull(secureNodeBuilder.getString("long"));
    }

    @Test
    public void testGetName() {
        assertNull(secureNodeBuilder.getName("prop"));
        assertNull(secureNodeBuilder.getName(NAME_NON_EXISTING));
        assertNull(secureNodeBuilder.getName(NAME_NON_ACCESSIBLE));
    }

    @Test
    public void testGetNameTypeNames() {
        secureNodeBuilder.setProperty("names", List.of("a", "b"), Type.NAMES);
        assertNull(secureNodeBuilder.getName("names"));
    }

    @Test
    public void testGetNameTypeName() {
        secureNodeBuilder.setProperty("name", "value", Type.NAME);
        assertEquals("value", secureNodeBuilder.getName("name"));
    }

    @Test
    public void testGetNames() {
        assertEquals(0, IterableUtils.size(secureNodeBuilder.getNames("prop")));
        assertEquals(0, IterableUtils.size(secureNodeBuilder.getNames(NAME_NON_EXISTING)));
        assertEquals(0, IterableUtils.size(secureNodeBuilder.getNames(NAME_NON_ACCESSIBLE)));
    }

    @Test
    public void testGetNamesTypeNames() {
        Iterable<String> names = List.of("a", "b");
        secureNodeBuilder.setProperty("names", names, Type.NAMES);
        assertTrue(IterableUtils.elementsEqual(names, secureNodeBuilder.getNames("names")));
    }

    @Test
    public void testGetNamesTypeName() {
        secureNodeBuilder.setProperty("name", "value", Type.NAME);
        assertEquals(0, IterableUtils.size(secureNodeBuilder.getNames("name")));
    }

    @Test
    public void testSetProperty() {
        secureNodeBuilder.setProperty("another", "value");
        assertTrue(secureNodeBuilder.hasProperty("another"));
    }

    @Test
    public void testSetPropertyReturnsSame() {
        assertSame(secureNodeBuilder, secureNodeBuilder.setProperty("another", "value"));
    }

    @Test
    public void testSetPropertyModifiedNonAccessible() {
        secureNodeBuilder.setProperty(NAME_NON_ACCESSIBLE, "anothervalue");
        // modified property must exist irrespective of permission eval
        assertTrue(secureNodeBuilder.hasProperty(NAME_NON_ACCESSIBLE));
    }

    @Test
    public void testSetPropertyNewNonAccessible() {
        String name = NAME_NON_ACCESSIBLE+"-new";
        secureNodeBuilder.setProperty(name, "value");
        // new property must exist irrespective of permission eval
        assertTrue(secureNodeBuilder.hasProperty(name));
    }

    @Test
    public void testRemoveProperty() {
        secureNodeBuilder.removeProperty("prop");
        assertFalse(secureNodeBuilder.hasProperty("prop"));
    }

    @Test
    public void testRemovePropertyNonAccessible() {
        secureNodeBuilder.removeProperty(NAME_NON_ACCESSIBLE);

        // verify that property has not been removed
        try {
            permissionProvider.canReadProperties = true;
            assertTrue(secureNodeBuilder.hasProperty(NAME_NON_ACCESSIBLE));
        } finally {
            permissionProvider.canReadProperties = false;
        }
    }

    @Test
    public void testRemovePropertyReturnsSame() {
        assertSame(secureNodeBuilder, secureNodeBuilder.removeProperty("prop"));
    }

    @Test
    public void testHasChildNode() {
        assertTrue(secureNodeBuilder.hasChildNode(NAME_ACCESSIBLE));
    }

    @Test
    public void testHasChildNodeNonAccessible() {
        assertFalse(secureNodeBuilder.hasChildNode(NAME_NON_ACCESSIBLE));
    }

    @Test
    public void testHasChildNodeNonExisting() {
        assertFalse(secureNodeBuilder.hasChildNode(NAME_NON_EXISTING));
    }

    @Test
    public void testGetChildNodeCount() {
        assertEquals(1, secureNodeBuilder.getChildNodeCount(Long.MAX_VALUE));
    }

    @Test
    public void testGetChildNodeCountCanReadAll() {
        try {
            permissionProvider.canReadAll = true;
            assertEquals(2, secureNodeBuilder.getChildNodeCount(Long.MAX_VALUE));
        } finally {
            permissionProvider.canReadAll = false;
        }
    }

}