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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Value;
import javax.jcr.security.Privilege;

import org.apache.jackrabbit.guava.common.collect.ImmutableList;
import org.apache.jackrabbit.guava.common.collect.Lists;
import org.apache.jackrabbit.api.security.JackrabbitAccessControlEntry;
import org.apache.jackrabbit.oak.api.Tree;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.namepath.NamePathMapper;
import org.apache.jackrabbit.oak.spi.security.authorization.restriction.AbstractRestrictionProvider;
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.RestrictionDefinitionImpl;
import org.apache.jackrabbit.oak.spi.security.authorization.restriction.RestrictionPattern;
import org.apache.jackrabbit.oak.spi.security.authorization.restriction.RestrictionProvider;
import org.apache.jackrabbit.oak.spi.security.principal.PrincipalImpl;
import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeBits;
import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeConstants;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.junit.Test;
import org.mockito.Mockito;

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.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * Tests for functionality provided by {@link AbstractAccessControlList}.
 */
public class AbstractAccessControlListTest extends AbstractAccessControlTest {

    protected String getTestPath() {
        return "/testPath";
    }

    protected NamePathMapper getNamePathMapper() {
        return NamePathMapper.DEFAULT;
    }

    protected RestrictionProvider getRestrictionProvider() {
        Map<String, RestrictionDefinition> rDefs = new HashMap<>();
        rDefs.put("r1", new RestrictionDefinitionImpl("r1", Type.STRING, true));
        rDefs.put("r2", new RestrictionDefinitionImpl("r2", Type.LONGS, false));

        return new AbstractRestrictionProvider(rDefs) {
            @NotNull
            @Override
            public RestrictionPattern getPattern(@Nullable String oakPath, @NotNull Tree tree) {
                throw new UnsupportedOperationException();
            }

            @NotNull
            @Override
            public RestrictionPattern getPattern(@Nullable String oakPath, @NotNull Set<Restriction> restrictions) {
                throw new UnsupportedOperationException();
            }
        };
    }

    protected AbstractAccessControlList createEmptyACL() {
        return createACL(getTestPath(), Collections.<JackrabbitAccessControlEntry>emptyList(), getNamePathMapper());
    }

    protected AbstractAccessControlList createACL(@NotNull JackrabbitAccessControlEntry... entries) {
        return createACL(getTestPath(), Lists.newArrayList(entries), getNamePathMapper());
    }

    protected AbstractAccessControlList createACL(@NotNull List<JackrabbitAccessControlEntry> entries) {
        return createACL(getTestPath(), entries, getNamePathMapper());
    }

    protected AbstractAccessControlList createACL(@Nullable String jcrPath,
                                                  @NotNull ACE... entries) {
        return createACL(jcrPath, Lists.newArrayList(entries), getNamePathMapper());
    }

    protected AbstractAccessControlList createACL(@Nullable String jcrPath,
                                                  @NotNull List<JackrabbitAccessControlEntry> entries,
                                                  @NotNull NamePathMapper namePathMapper) {
        return createACL(jcrPath, entries, namePathMapper, getRestrictionProvider());
    }

    protected AbstractAccessControlList createACL(@Nullable String jcrPath,
                                                  @NotNull List<JackrabbitAccessControlEntry> entries,
                                                  @NotNull NamePathMapper namePathMapper,
                                                  @NotNull RestrictionProvider restrictionProvider) {
        return new TestACL(jcrPath, restrictionProvider, namePathMapper, entries);
    }

    protected List<JackrabbitAccessControlEntry> createTestEntries() throws RepositoryException {
        List<JackrabbitAccessControlEntry> entries = new ArrayList<>(3);
        for (int i = 0; i < 3; i++) {
            entries.add(createEntry(
                    new PrincipalImpl("testPrincipal" + i), PrivilegeBits.BUILT_IN.get(PrivilegeConstants.JCR_READ), true));
        }
        return entries;
    }

    @Test
    public void testGetNamePathMapper() {
        assertSame(getNamePathMapper(), createEmptyACL().getNamePathMapper());
        assertSame(NamePathMapper.DEFAULT, createACL(getTestPath(), ImmutableList.<JackrabbitAccessControlEntry>of(), NamePathMapper.DEFAULT).getNamePathMapper());
    }

    @Test
    public void testGetPath() {
        NamePathMapper npMapper = mockNamePathMapper(getTestPath());

        // map of jcr-path to standard jcr-path
        Map<String, String> paths = new HashMap<>();
        paths.put(null, null);
        paths.put(getTestPath(), getTestPath());
        paths.put("/", "/");
        paths.put("/jr:testPath", "/jr:testPath");
        paths.put("/{http://jackrabbit.apache.org}testPath", "/jr:testPath");

        paths.forEach((key, value) -> {
            AbstractAccessControlList acl = createACL(key, Collections.<JackrabbitAccessControlEntry>emptyList(), npMapper);
            assertEquals(value, acl.getPath());
        });
    }

    @Test
    public void testGetOakPath() {
        NamePathMapper npMapper = mockNamePathMapper(getTestPath());

        // map of jcr-path to oak path
        Map<String, String> paths = new HashMap<>();
        paths.put(null, null);
        paths.put(getTestPath(), getTestPath());
        paths.put("/", "/");
        String oakPath = "/oak:testPath";
        paths.put("/jr:testPath", oakPath);
        paths.put("/{http://jackrabbit.apache.org}testPath", oakPath);

        // test if oak-path is properly set.
        paths.forEach((key, value) -> {
            AbstractAccessControlList acl = createACL(key, Collections.<JackrabbitAccessControlEntry>emptyList(), npMapper);
            assertEquals(value, acl.getOakPath());
        });
    }

    private static NamePathMapper mockNamePathMapper(String testPath) {
        NamePathMapper npMapper = Mockito.mock(NamePathMapper.class);
        when(npMapper.getOakPath("/")).thenReturn("/");
        when(npMapper.getOakPath(null)).thenReturn(null);
        when(npMapper.getOakPath(testPath)).thenReturn(testPath);
        when(npMapper.getOakPath("/jr:testPath")).thenReturn("/oak:testPath");
        when(npMapper.getOakPath("/{http://jackrabbit.apache.org}testPath")).thenReturn("/oak:testPath");
        when(npMapper.getJcrPath("/")).thenReturn("/");
        when(npMapper.getJcrPath(null)).thenReturn(null);
        when(npMapper.getJcrPath(testPath)).thenReturn(testPath);
        when(npMapper.getJcrPath("/oak:testPath")).thenReturn("/jr:testPath");
        when(npMapper.getJcrPath("/{http://jackrabbit.apache.org}testPath")).thenReturn("/jr:testPath");
        return npMapper;
    }

    @Test
    public void testEmptyAcl() {
        AbstractAccessControlList acl = createEmptyACL();

        assertNotNull(acl.getAccessControlEntries());
        assertNotNull(acl.getEntries());

        assertEquals(0, acl.getAccessControlEntries().length);
        assertEquals(acl.getAccessControlEntries().length, acl.getEntries().size());
        assertEquals(0, acl.size());
        assertTrue(acl.isEmpty());
    }

    @Test
    public void testSize() throws RepositoryException {
        AbstractAccessControlList acl = createACL(createTestEntries());
        assertEquals(3, acl.size());
    }

    @Test
    public void testIsEmpty() throws RepositoryException {
        AbstractAccessControlList acl = createACL(createTestEntries());
        assertFalse(acl.isEmpty());
    }

    @Test
    public void testGetEntries() throws RepositoryException {
        List<JackrabbitAccessControlEntry> aces = createTestEntries();
        AbstractAccessControlList acl = createACL(aces);

        assertNotNull(acl.getEntries());
        assertNotNull(acl.getAccessControlEntries());

        assertEquals(aces.size(), acl.getEntries().size());
        assertEquals(aces.size(), acl.getAccessControlEntries().length);
        assertTrue(acl.getEntries().containsAll(aces));
        assertTrue(Arrays.asList(acl.getAccessControlEntries()).containsAll(aces));
    }

    @Test
    public void testGetRestrictionNames() {
        AbstractAccessControlList acl = createEmptyACL();

        String[] restrNames = acl.getRestrictionNames();
        assertNotNull(restrNames);
        List<String> names = Lists.newArrayList(restrNames);
        for (RestrictionDefinition def : getRestrictionProvider().getSupportedRestrictions(getTestPath())) {
            assertTrue(names.remove(getNamePathMapper().getJcrName(def.getName())));
        }
        assertTrue(names.isEmpty());
    }

    @Test
    public void testGetRestrictionType() {
        AbstractAccessControlList acl = createEmptyACL();
        for (RestrictionDefinition def : getRestrictionProvider().getSupportedRestrictions(getTestPath())) {
            int reqType = acl.getRestrictionType(getNamePathMapper().getJcrName(def.getName()));

            assertTrue(reqType > PropertyType.UNDEFINED);
            assertEquals(def.getRequiredType().tag(), reqType);
        }
    }

    @Test
    public void testGetRestrictionTypeForUnknownName() {
        AbstractAccessControlList acl = createEmptyACL();
        // for backwards compatibility getRestrictionType(String) must return
        // UNDEFINED for a unknown restriction name:
        assertEquals(PropertyType.UNDEFINED, acl.getRestrictionType("unknownRestrictionName"));
    }

    @Test
    public void testIsMultiValueRestriction() {
        AbstractAccessControlList acl = createEmptyACL();
        for (RestrictionDefinition def : getRestrictionProvider().getSupportedRestrictions(getTestPath())) {
            boolean isMv = acl.isMultiValueRestriction(getNamePathMapper().getJcrName(def.getName()));

            assertEquals(def.getRequiredType().isArray(), isMv);
        }
    }

    @Test
    public void testIsMultiValueRestrictionForUnknownName() {
        assertFalse(createEmptyACL().isMultiValueRestriction("unknownRestrictionName"));
    }

    @Test
    public void testAddAccessControlEntry() throws Exception {
        AbstractAccessControlList acl = spy(createEmptyACL());

        Privilege[] privs =  new Privilege[0];
        acl.addAccessControlEntry(testPrincipal, privs);

        verify(acl, never()).addEntry(testPrincipal, privs, true);
        verify(acl, times(1)).addEntry(testPrincipal, privs, true, Collections.emptyMap());
    }

    @Test
    public void testAddEntry() throws Exception {
        AbstractAccessControlList acl = spy(createEmptyACL());

        Privilege[] privs = new Privilege[0];
        acl.addEntry(testPrincipal, privs, false);

        verify(acl, times(1)).addEntry(testPrincipal, privs, false, Collections.emptyMap());
    }

    @Test
    public void testAddEntryWithRestrictions() throws Exception {
        AbstractAccessControlList acl = spy(createEmptyACL());

        Privilege[] privs =  new Privilege[0];
        Map<String, Value> restrictions = Collections.singletonMap("name", mock(Value.class));
        acl.addEntry(testPrincipal, privs, false, restrictions);

        verify(acl, times(1)).addEntry(testPrincipal, privs, false, restrictions, null);
    }
}
