/*
 * 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.security.authorization.permission;

import java.lang.reflect.Field;
import java.util.Set;

import javax.jcr.security.AccessControlList;
import javax.jcr.security.AccessControlManager;

import org.apache.jackrabbit.commons.jackrabbit.authorization.AccessControlUtils;
import org.apache.jackrabbit.oak.AbstractSecurityTest;
import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.api.Root;
import org.apache.jackrabbit.oak.api.Tree;
import org.apache.jackrabbit.oak.commons.PathUtils;
import org.apache.jackrabbit.oak.namepath.NamePathMapper;
import org.apache.jackrabbit.oak.plugins.memory.PropertyStates;
import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
import org.apache.jackrabbit.oak.plugins.version.ReadOnlyVersionManager;
import org.apache.jackrabbit.oak.spi.security.authorization.AuthorizationConfiguration;
import org.apache.jackrabbit.oak.spi.security.authorization.permission.PermissionProvider;
import org.apache.jackrabbit.oak.spi.security.authorization.permission.Permissions;
import org.apache.jackrabbit.oak.spi.security.authorization.permission.TreePermission;
import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal;
import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeConstants;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.apache.jackrabbit.oak.plugins.tree.TreeUtil;
import org.jetbrains.annotations.NotNull;
import org.junit.Test;

import static java.util.Objects.requireNonNull;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

public class VersionTreePermissionTest extends AbstractSecurityTest implements NodeTypeConstants {

    private ReadOnlyVersionManager vMgr;
    private PermissionProvider pp;

    private Tree testTree;

    private Field vpField;
    private Field tpImplTree;

    @Override
    public void before() throws Exception {
        super.before();

        testTree = TreeUtil.addChild(root.getTree("/"),"test", NT_OAK_UNSTRUCTURED);
        Tree a = TreeUtil.addChild(testTree, "a", NT_OAK_UNSTRUCTURED);
        Tree b = TreeUtil.addChild(a, "b", NT_OAK_UNSTRUCTURED);
        TreeUtil.addChild(b, "c", NT_OAK_UNSTRUCTURED);
        TreeUtil.addMixin(testTree, MIX_VERSIONABLE, root.getTree(NODE_TYPES_PATH), null);

        AccessControlManager acMgr = getAccessControlManager(root);
        AccessControlList acl = AccessControlUtils.getAccessControlList(acMgr, "/test");
        acl.addAccessControlEntry(EveryonePrincipal.getInstance(), AccessControlUtils.privilegesFromNames(acMgr, PrivilegeConstants.JCR_READ));
        acMgr.setPolicy("/test", acl);
        root.commit();

        // create a structure in the version storage
        testTree.setProperty(JCR_ISCHECKEDOUT, false);
        root.commit();
        testTree.setProperty(JCR_ISCHECKEDOUT, true);
        root.commit();

        vMgr = ReadOnlyVersionManager.getInstance(root, NamePathMapper.DEFAULT);
        pp = getConfig(AuthorizationConfiguration.class).getPermissionProvider(root, root.getContentSession().getWorkspaceName(), Set.of(EveryonePrincipal.getInstance()));

        assertTrue(pp instanceof PermissionProviderImpl);

        vpField = VersionTreePermission.class.getDeclaredField("versionablePermission");
        vpField.setAccessible(true);
        Class cls = Class.forName(CompiledPermissionImpl.class.getName() + "$TreePermissionImpl");
        tpImplTree = cls.getDeclaredField("tree");
        tpImplTree.setAccessible(true);
    }

    @Override
    public void after() throws Exception {
        try {
            root.refresh();
            Tree t = root.getTree("/test");
            if (t.exists()) {
                t.remove();
                root.commit();
            }
        } finally {
            super.after();
        }
    }

    private static TreePermission getVersionPermission(Root root, PermissionProvider pp, String path) {
        Tree t = root.getTree("/");
        TreePermission tp = pp.getTreePermission(t, TreePermission.EMPTY);
        for (String name : PathUtils.elements(path)) {
            t = t.getChild(name);
            tp = pp.getTreePermission(t, tp);
        }
        return tp;
    }

    private void assertVersionPermission(@NotNull TreePermission tp, @NotNull String expectedPath, boolean canRead) throws Exception {
        assertTrue(tp instanceof VersionTreePermission);
        assertEquals(canRead, tp.canRead());
        assertEquals(canRead, tp.canRead(PropertyStates.createProperty("any", "Value")));
        assertEquals(canRead, tp.isGranted(Permissions.READ));
        assertEquals(canRead, tp.isGranted(Permissions.READ, PropertyStates.createProperty("any", "Value")));
        assertEquals(canRead, tp.canReadProperties());
        assertFalse(tp.canReadAll());

        VersionTreePermission vtp = (VersionTreePermission) tp;
        TreePermission delegatee = (TreePermission) vpField.get(vtp);
        Tree delegateeTree = (Tree) tpImplTree.get(delegatee);

        assertEquals(expectedPath, delegateeTree.getPath());
    }

    @Test
    public void testGetTreePermission() throws Exception {
        Tree versionHistory = requireNonNull(vMgr.getVersionHistory(testTree));

        String expectedPath = "/test";

        TreePermission tp = getVersionPermission(root, pp, versionHistory.getPath());
        assertVersionPermission(tp, expectedPath, true);

        Tree vTree = versionHistory.getChild("1.0");
        assertTrue(vTree.exists());
        tp = pp.getTreePermission(vTree, tp);
        assertVersionPermission(tp, expectedPath, true);

        Tree frozen = vTree.getChild(JCR_FROZENNODE);
        assertTrue(frozen.exists());
        tp = pp.getTreePermission(frozen, tp);
        assertVersionPermission(tp, expectedPath, true);

        Tree t = frozen;
        for (String name : new String[] {"a", "b", "c"}) {
            t = t.getChild(name);
            expectedPath = PathUtils.concat(expectedPath, name);
            tp = pp.getTreePermission(t, tp);
            assertVersionPermission(tp, expectedPath, true);
        }
    }

    @Test
    public void testGetChild() throws Exception {
        Tree versionHistory = requireNonNull(vMgr.getVersionHistory(testTree));

        Tree t = getRootProvider().createReadOnlyRoot(root).getTree("/");
        TreePermission tp = pp.getTreePermission(t, TreePermission.EMPTY);
        for (String name : PathUtils.elements(versionHistory.getPath())) {
            t = t.getChild(name);
            tp = tp.getChildPermission(name, getTreeProvider().asNodeState(t));
        }

        String expectedPath = "/test";
        assertVersionPermission(tp, "/test", true);

        NodeState ns = getTreeProvider().asNodeState(t.getChild("1.0"));
        tp = tp.getChildPermission("1.0", ns);
        assertVersionPermission(tp, "/test", true);

        ns = ns.getChildNode(JCR_FROZENNODE);
        tp = tp.getChildPermission(JCR_FROZENNODE, ns);
        assertVersionPermission(tp, "/test", true);

        for (String name : new String[] {"a", "b", "c"}) {
            ns = ns.getChildNode(name);
            expectedPath = PathUtils.concat(expectedPath, name);

            tp = tp.getChildPermission(name, ns);
            assertVersionPermission(tp, expectedPath, true);
        }
    }

    @Test
    public void testVersionableRemoved() throws Exception {
        Tree versionHistory = requireNonNull(vMgr.getVersionHistory(testTree));

        testTree.remove();
        root.commit();
        pp.refresh();

        TreePermission tp = getVersionPermission(root, pp, versionHistory.getPath());
        assertVersionPermission(tp, "/", false);

        Tree vTree = versionHistory.getChild("1.0");
        tp = pp.getTreePermission(vTree, tp);
        assertVersionPermission(tp, "/", false);

        Tree frozen = vTree.getChild(JCR_FROZENNODE);
        assertTrue(frozen.exists());
        tp = pp.getTreePermission(frozen, tp);
        assertVersionPermission(tp, "/", false);

        Tree t = frozen;
        String expectedPath = "/";
        for (String name : new String[] {"a", "b", "c"}) {
            t = t.getChild(name);
            expectedPath = PathUtils.concat(expectedPath, name);
            tp = pp.getTreePermission(t, tp);
            assertVersionPermission(tp, expectedPath, false);
        }
    }

    @Test
    public void testVersionableChildRemoved() throws Exception {
        root.getTree("/test/a/b/c").remove();
        root.commit();
        pp.refresh();

        Tree versionHistory = requireNonNull(vMgr.getVersionHistory(testTree));
        String frozenCPath = PathUtils.concat(versionHistory.getPath(), "1.0", JCR_FROZENNODE, "a/b/c");

        TreePermission tp = getVersionPermission(root, pp, frozenCPath);
        assertVersionPermission(tp, "/test/a/b/c", true);

        root.getTree("/test/a").remove();
        root.commit();
        pp.refresh();

        tp = getVersionPermission(root, pp, frozenCPath);
        assertVersionPermission(tp, "/test/a/b/c", true);
    }

    @Test
    public void testVersionableChildRemoved2() throws Exception {
        root.getTree("/test/a/b").remove();
        root.commit();
        pp.refresh();

        Tree versionHistory = requireNonNull(vMgr.getVersionHistory(testTree));

        String frozenAPath = PathUtils.concat(versionHistory.getPath(), "1.0", JCR_FROZENNODE, "a");
        TreePermission tp = getVersionPermission(root, pp, frozenAPath);
        assertVersionPermission(tp, "/test/a", true);

        Tree frozenB = root.getTree(frozenAPath).getChild("b");
        tp = pp.getTreePermission(frozenB, tp);
        assertVersionPermission(tp, "/test/a/b", true);

        Tree frozenC = frozenB.getChild("c");
        tp = pp.getTreePermission(frozenC, tp);
        assertVersionPermission(tp, "/test/a/b/c", true);
    }

    @Test
    public void testCanRead() {
        TreePermission versionableTreePermission = mock(TreePermission.class);
        VersionTreePermission vtp = new VersionTreePermission(mock(Tree.class), versionableTreePermission, getTreeProvider());

        vtp.canRead();
        verify(versionableTreePermission, times(1)).canRead();
    }

    @Test
    public void testCanReadProperty() {
        TreePermission versionableTreePermission = mock(TreePermission.class);
        VersionTreePermission vtp = new VersionTreePermission(mock(Tree.class), versionableTreePermission, getTreeProvider());

        PropertyState ps = mock(PropertyState.class);
        vtp.canRead(ps);
        verify(versionableTreePermission, times(1)).canRead(ps);
    }

    @Test
    public void testCanReadAll() {
        TreePermission versionableTreePermission = mock(TreePermission.class);
        VersionTreePermission vtp = new VersionTreePermission(mock(Tree.class), versionableTreePermission, getTreeProvider());

        vtp.canReadAll();
        verify(versionableTreePermission, times(1)).canReadAll();
    }

    @Test
    public void testCanReadProperties() {
        TreePermission versionableTreePermission = mock(TreePermission.class);
        VersionTreePermission vtp = new VersionTreePermission(mock(Tree.class), versionableTreePermission, getTreeProvider());

        vtp.canReadProperties();
        verify(versionableTreePermission, times(1)).canReadProperties();
    }

    @Test
    public void testIsGranted() {
        TreePermission versionableTreePermission = mock(TreePermission.class);
        VersionTreePermission vtp = new VersionTreePermission(mock(Tree.class), versionableTreePermission, getTreeProvider());

        vtp.isGranted(Permissions.ALL);
        verify(versionableTreePermission, times(1)).isGranted(Permissions.ALL);
    }

    @Test
    public void testIsGrantedProperty() {
        TreePermission versionableTreePermission = mock(TreePermission.class);
        VersionTreePermission vtp = new VersionTreePermission(mock(Tree.class), versionableTreePermission, getTreeProvider());

        PropertyState ps = mock(PropertyState.class);

        vtp.isGranted(Permissions.ALL, ps);
        verify(versionableTreePermission, times(1)).isGranted(Permissions.ALL, ps);
    }
}
