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

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.jcr.security.AccessControlException;
import javax.jcr.security.AccessControlManager;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import org.apache.jackrabbit.JcrConstants;
import org.apache.jackrabbit.api.security.JackrabbitAccessControlList;
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.Tree;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.plugins.memory.PropertyStates;
import org.apache.jackrabbit.oak.plugins.tree.TreeUtil;
import org.apache.jackrabbit.oak.spi.security.authorization.accesscontrol.AccessControlConstants;
import org.apache.jackrabbit.oak.spi.security.authorization.restriction.CompositePattern;
import org.apache.jackrabbit.oak.spi.security.authorization.restriction.Restriction;
import org.apache.jackrabbit.oak.spi.security.authorization.restriction.RestrictionDefinition;
import org.apache.jackrabbit.oak.spi.security.authorization.restriction.RestrictionPattern;
import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeConstants;
import org.junit.Before;
import org.junit.Test;

import static com.google.common.collect.Maps.newHashMap;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;

/**
 * Tests for {@link RestrictionProviderImpl}
 */
public class RestrictionProviderImplTest extends AbstractSecurityTest implements AccessControlConstants {

    private RestrictionProviderImpl provider;

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

        provider = new RestrictionProviderImpl();
    }

    @Test
    public void testGetSupportedDefinitions() {
        assertTrue(provider.getSupportedRestrictions(null).isEmpty());

        Set<RestrictionDefinition> defs = provider.getSupportedRestrictions("/testPath");
        assertNotNull(defs);
        assertEquals(5, defs.size());

        for (RestrictionDefinition def : defs) {
            if (REP_GLOB.equals(def.getName())) {
                assertEquals(Type.STRING, def.getRequiredType());
                assertFalse(def.isMandatory());
            } else if (REP_NT_NAMES.equals(def.getName())) {
                assertEquals(Type.NAMES, def.getRequiredType());
                assertFalse(def.isMandatory());
            } else if (REP_PREFIXES.equals(def.getName())) {
                assertEquals(Type.STRINGS, def.getRequiredType());
                assertFalse(def.isMandatory());
            } else if (REP_ITEM_NAMES.equals(def.getName())) {
                assertEquals(Type.NAMES, def.getRequiredType());
                assertFalse(def.isMandatory());
            } else if (REP_CURRENT.equals(def.getName())) {
                assertEquals(Type.STRINGS, def.getRequiredType());
                assertFalse(def.isMandatory());
            } else {
                fail("unexpected restriction " + def.getName());
            }
        }
    }

    @Test
    public void testGetRestrictionPattern() throws Exception {
        Map<PropertyState, RestrictionPattern> map = newHashMap();
        map.put(PropertyStates.createProperty(REP_GLOB, "/*/jcr:content"), GlobPattern.create("/testPath", "/*/jcr:content"));
        List<String> ntNames = ImmutableList.of(JcrConstants.NT_FOLDER, JcrConstants.NT_LINKEDFILE);
        map.put(PropertyStates.createProperty(REP_NT_NAMES, ntNames, Type.NAMES), new NodeTypePattern(ntNames));

        Tree tree = TreeUtil.getOrAddChild(root.getTree("/"), "testPath", JcrConstants.NT_UNSTRUCTURED);
        Tree restrictions = TreeUtil.addChild(tree, REP_RESTRICTIONS, NT_REP_RESTRICTIONS);

        // test restrictions individually
        for (Map.Entry<PropertyState, RestrictionPattern> entry : map.entrySet()) {
            restrictions.setProperty(entry.getKey());

            RestrictionPattern pattern = provider.getPattern("/testPath", restrictions);
            assertEquals(entry.getValue(), pattern);

            restrictions.removeProperty(entry.getKey().getName());
        }

        // test combination on multiple restrictions
        for (Map.Entry<PropertyState, RestrictionPattern> entry : map.entrySet()) {
            restrictions.setProperty(entry.getKey());
        }
        RestrictionPattern pattern = provider.getPattern("/testPath", restrictions);
        assertTrue(pattern instanceof CompositePattern);
    }

    @Test
    public void testGetPatternForAllSupported() throws Exception {
        Map<PropertyState, RestrictionPattern> map = newHashMap();
        map.put(PropertyStates.createProperty(REP_GLOB, "/*/jcr:content"), GlobPattern.create("/testPath", "/*/jcr:content"));
        List<String> ntNames = ImmutableList.of(JcrConstants.NT_FOLDER, JcrConstants.NT_LINKEDFILE);
        map.put(PropertyStates.createProperty(REP_NT_NAMES, ntNames, Type.NAMES), new NodeTypePattern(ntNames));
        List<String> prefixes = ImmutableList.of("rep", "jcr");
        map.put(PropertyStates.createProperty(REP_PREFIXES, prefixes, Type.STRINGS), new PrefixPattern(prefixes));
        List<String> itemNames = ImmutableList.of("abc", "jcr:primaryType");
        map.put(PropertyStates.createProperty(REP_ITEM_NAMES, prefixes, Type.NAMES), new ItemNamePattern(itemNames));
        List<String> propNames = ImmutableList.of("jcr:mixinTypes", "jcr:primaryType");
        map.put(PropertyStates.createProperty(REP_CURRENT, prefixes, Type.STRINGS), new CurrentPattern("/testPatg", propNames));

        Tree tree = TreeUtil.getOrAddChild(root.getTree("/"), "testPath", JcrConstants.NT_UNSTRUCTURED);
        Tree restrictions = TreeUtil.addChild(tree, REP_RESTRICTIONS, NT_REP_RESTRICTIONS);
        for (Map.Entry<PropertyState, RestrictionPattern> entry : map.entrySet()) {
            restrictions.setProperty(entry.getKey());
        }

        RestrictionPattern pattern = provider.getPattern("/testPath", restrictions);
        assertTrue(pattern instanceof CompositePattern);
    }

    @Test
    public void testGetPatternFromRestrictions() throws Exception {
        Map<PropertyState, RestrictionPattern> map = newHashMap();
        map.put(PropertyStates.createProperty(REP_GLOB, "/*/jcr:content"), GlobPattern.create("/testPath", "/*/jcr:content"));

        List<String> ntNames = ImmutableList.of(JcrConstants.NT_FOLDER, JcrConstants.NT_LINKEDFILE);
        map.put(PropertyStates.createProperty(REP_NT_NAMES, ntNames, Type.NAMES), new NodeTypePattern(ntNames));

        List<String> prefixes = ImmutableList.of("rep", "jcr");
        map.put(PropertyStates.createProperty(REP_PREFIXES, prefixes, Type.STRINGS), new PrefixPattern(prefixes));

        List<String> itemNames = ImmutableList.of("abc", "jcr:primaryType");
        map.put(PropertyStates.createProperty(REP_ITEM_NAMES, itemNames, Type.NAMES), new ItemNamePattern(itemNames));

        List<String> propNames = ImmutableList.of("*");
        map.put(PropertyStates.createProperty(REP_CURRENT, propNames, Type.STRINGS), new CurrentPattern("/testPath", propNames));
        
        Tree tree = TreeUtil.getOrAddChild(root.getTree("/"), "testPath", JcrConstants.NT_UNSTRUCTURED);
        Tree restrictions = TreeUtil.addChild(tree, REP_RESTRICTIONS, NT_REP_RESTRICTIONS);
        
        // test restrictions individually
        for (Map.Entry<PropertyState, RestrictionPattern> entry : map.entrySet()) {
            restrictions.setProperty(entry.getKey());

            RestrictionPattern pattern = provider.getPattern("/testPath", provider.readRestrictions("/testPath", tree));
            assertEquals(entry.getValue(), pattern);
            restrictions.removeProperty(entry.getKey().getName());
        }

        // test combination on multiple restrictions
        for (Map.Entry<PropertyState, RestrictionPattern> entry : map.entrySet()) {
            restrictions.setProperty(entry.getKey());
        }
        RestrictionPattern pattern = provider.getPattern("/testPath", provider.readRestrictions("/testPath", tree));
        assertTrue(pattern instanceof CompositePattern);
    }

    @Test
    public void testGetPatternFromTreeNullPath() {
        assertSame(RestrictionPattern.EMPTY, provider.getPattern(null, mock(Tree.class)));
    }

    @Test
    public void testGetPatternFromRestrictionsNullPath() {
        assertSame(RestrictionPattern.EMPTY, provider.getPattern(null, ImmutableSet.of(mock(Restriction.class))));
    }

    @Test
    public void testGetPatternFromEmptyRestrictions() {
        assertSame(RestrictionPattern.EMPTY, provider.getPattern("/testPath", ImmutableSet.of()));
    }

    @Test(expected = AccessControlException.class)
    public void testValidateGlobRestriction() throws Exception {
        Tree t = TreeUtil.getOrAddChild(root.getTree("/"), "testTree", JcrConstants.NT_UNSTRUCTURED);
        String path = t.getPath();

        AccessControlManager acMgr = getAccessControlManager(root);

        List<String> globs = ImmutableList.of(
                "/1*/2*/3*/4*/5*/6*/7*/8*/9*/10*/11*/12*/13*/14*/15*/16*/17*/18*/19*/20*/21*",
                "*********************");
        for (String glob : globs) {
            JackrabbitAccessControlList acl = AccessControlUtils.getAccessControlList(acMgr, path);
            acl.addEntry(getTestUser().getPrincipal(),
                    AccessControlUtils.privilegesFromNames(acMgr, PrivilegeConstants.JCR_READ),
                    true, Collections.singletonMap(REP_GLOB, getValueFactory().createValue(glob)));
            acMgr.setPolicy(path, acl);

            try {
                provider.validateRestrictions(path, t.getChild(REP_POLICY).getChild("allow"));
            } finally {
                acMgr.removePolicy(path, acl);
            }
        }
    }
}