/*
 * Copyright 2016 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed 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.uberfire.security.impl.authz;

import java.util.Collection;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.jboss.errai.security.shared.api.Role;
import org.jboss.errai.security.shared.api.RoleImpl;
import org.jboss.errai.security.shared.api.identity.User;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.runners.MockitoJUnitRunner;
import org.uberfire.security.ResourceAction;
import org.uberfire.security.ResourceRef;
import org.uberfire.security.ResourceType;
import org.uberfire.security.authz.AuthorizationPolicy;
import org.uberfire.security.authz.AuthorizationResult;
import org.uberfire.security.authz.Permission;
import org.uberfire.security.authz.PermissionCollection;
import org.uberfire.security.authz.PermissionManager;
import org.uberfire.security.authz.VotingAlgorithm;
import org.uberfire.security.authz.VotingStrategy;

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import static org.uberfire.security.authz.AuthorizationResult.ACCESS_ABSTAIN;
import static org.uberfire.security.authz.AuthorizationResult.ACCESS_DENIED;
import static org.uberfire.security.authz.AuthorizationResult.ACCESS_GRANTED;

@RunWith(MockitoJUnitRunner.class)
public class PermissionManagerTest {

    PermissionManager permissionManager;
    DefaultAuthzResultCache authzResultCache;
    AuthorizationPolicy authorizationPolicy;
    Permission viewAll = new DotNamedPermission("resource.read",
                                                true);
    Permission denyAll = new DotNamedPermission("resource.read",
                                                false);
    Permission view1 = new DotNamedPermission("resource.read.1",
                                              true);
    Permission noView1 = new DotNamedPermission("resource.read.1",
                                                false);
    Permission view2 = new DotNamedPermission("resource.read.2",
                                              true);
    Permission view12 = new DotNamedPermission("resource.read.1.2",
                                               true);

    protected User createUserMock(String... roles) {
        User user = mock(User.class);
        Set<Role> roleSet = Stream.of(roles).map(RoleImpl::new).collect(Collectors.toSet());
        when(user.getIdentifier()).thenReturn(Integer.toString(user.hashCode()));
        when(user.getRoles()).thenReturn(roleSet);
        when(user.getGroups()).thenReturn(null);
        return user;
    }

    @Before
    public void setUp() {
        authzResultCache = spy(new DefaultAuthzResultCache());
        permissionManager = spy(new DefaultPermissionManager(new DefaultPermissionTypeRegistry(),
                                                             authzResultCache));
        permissionManager.setAuthorizationPolicy(
                authorizationPolicy = spy(permissionManager.newAuthorizationPolicy()
                                                  .role("viewAll").permission("resource.read",
                                                                              true)
                                                  .role("noViewAll").permission("resource.read",
                                                                                false)
                                                  .role("onlyView1",
                                                        5).permission("resource.read",
                                                                      false).permission("resource.read.1",
                                                                                        true)
                                                  .role("noView1").permission("resource.read.1",
                                                                              false)
                                                  .role("onlyView12").permission("resource.read.1.2",
                                                                                 true)
                                                  .build()));
    }

    @Test
    public void testSetNullPolicy() {
        permissionManager.setAuthorizationPolicy(null);
        AuthorizationPolicy policy = permissionManager.getAuthorizationPolicy();
        assertNotNull(policy);
        assertTrue(policy.getRoles().isEmpty());
        assertTrue(policy.getGroups().isEmpty());
    }

    @Test
    public void testCreateGlobalPermissions() {
        ResourceType type = () -> "type";
        Permission p = permissionManager.createPermission(type,
                                                          null,
                                                          true);
        assertEquals(p.getName(),
                     "type.read");

        p = permissionManager.createPermission(type,
                                               () -> "edit",
                                               true);
        assertEquals(p.getName(),
                     "type.edit");
    }

    @Test
    public void testCreateTypedPermissions() {
        ResourceType type = () -> "type";
        ResourceRef r = new ResourceRef("r1",
                                        type,
                                        null);
        Permission p = permissionManager.createPermission(r,
                                                          null,
                                                          true);
        assertEquals(p.getName(),
                     "type.read.r1");

        p = permissionManager.createPermission(r,
                                               ResourceAction.READ,
                                               true);
        assertEquals(p.getName(),
                     "type.read.r1");
    }

    @Test
    public void testUnknownTypePermissions() {
        ResourceRef r = new ResourceRef("r1",
                                        ResourceType.UNKNOWN,
                                        null);
        Permission p = permissionManager.createPermission(r,
                                                          null,
                                                          true);
        assertEquals(p.getName(),
                     "r1");

        p = permissionManager.createPermission(r,
                                               ResourceAction.READ,
                                               true);
        assertEquals(p.getName(),
                     "r1");
    }

    @Test
    public void testCreateNonTypedPermissions() {
        ResourceRef r = new ResourceRef("r1",
                                        null,
                                        null);
        Permission p = permissionManager.createPermission(r,
                                                          null,
                                                          true);
        assertEquals(p.getName(),
                     "r1");

        p = permissionManager.createPermission(r,
                                               ResourceAction.READ,
                                               true);
        assertEquals(p.getName(),
                     "r1");
    }

    @Test
    public void testResolveResourceId() {
        ResourceType type = () -> "type";
        ResourceRef r = new ResourceRef("r1",
                                        type,
                                        null);
        Permission p = permissionManager.createPermission(r,
                                                          null,
                                                          true);
        assertEquals(p.getName(),
                     "type.read.r1");

        String id = permissionManager.resolveResourceId(p);
        assertEquals(id,
                     "r1");
    }

    @Test
    public void testResolveResourceNull() {
        Permission p = permissionManager.createPermission("feature",
                                                          true);
        assertEquals(p.getName(),
                     "feature");

        String id = permissionManager.resolveResourceId(p);
        assertNull(id);
    }

    @Test
    public void testCheckPermission1() {
        User user = createUserMock("viewAll");
        assertEquals(permissionManager.checkPermission(viewAll,
                                                       user),
                     ACCESS_GRANTED);
        assertEquals(permissionManager.checkPermission(view1,
                                                       user),
                     ACCESS_GRANTED);
        assertEquals(permissionManager.checkPermission(view2,
                                                       user),
                     ACCESS_GRANTED);
        assertEquals(permissionManager.checkPermission(noView1,
                                                       user),
                     ACCESS_DENIED);
    }

    @Test
    public void testCheckPermission2() {
        User user = createUserMock("viewAll",
                                   "onlyView1");
        assertEquals(permissionManager.checkPermission(viewAll,
                                                       user),
                     ACCESS_DENIED);
        assertEquals(permissionManager.checkPermission(view1,
                                                       user),
                     ACCESS_GRANTED);
        assertEquals(permissionManager.checkPermission(view2,
                                                       user),
                     ACCESS_DENIED);
        assertEquals(permissionManager.checkPermission(noView1,
                                                       user),
                     ACCESS_DENIED);
    }

    @Test
    public void testCheckPermission3() {
        User user = createUserMock("viewAll",
                                   "onlyView1",
                                   "noView1");
        assertEquals(permissionManager.checkPermission(viewAll,
                                                       user),
                     ACCESS_DENIED);
        assertEquals(permissionManager.checkPermission(view1,
                                                       user),
                     ACCESS_GRANTED);
        assertEquals(permissionManager.checkPermission(view2,
                                                       user),
                     ACCESS_DENIED);
        assertEquals(permissionManager.checkPermission(noView1,
                                                       user),
                     ACCESS_DENIED);
    }

    @Test
    public void testCheckPermission4() {
        User user = createUserMock("viewAll",
                                   "noView1");
        assertEquals(permissionManager.checkPermission(viewAll,
                                                       user),
                     ACCESS_GRANTED);
        assertEquals(permissionManager.checkPermission(view1,
                                                       user),
                     ACCESS_GRANTED);
        assertEquals(permissionManager.checkPermission(view2,
                                                       user),
                     ACCESS_GRANTED);
        assertEquals(permissionManager.checkPermission(noView1,
                                                       user),
                     ACCESS_DENIED);
    }

    @Test
    public void testCheckPermission5() {
        User user = createUserMock("onlyView1");
        assertEquals(permissionManager.checkPermission(viewAll,
                                                       user),
                     ACCESS_DENIED);
        assertEquals(permissionManager.checkPermission(view1,
                                                       user),
                     ACCESS_GRANTED);
        assertEquals(permissionManager.checkPermission(view2,
                                                       user),
                     ACCESS_DENIED);
        assertEquals(permissionManager.checkPermission(noView1,
                                                       user),
                     ACCESS_DENIED);
    }

    @Test
    public void testCheckPermission6() {
        User user = createUserMock("noView1");
        assertEquals(permissionManager.checkPermission(viewAll,
                                                       user),
                     ACCESS_ABSTAIN);
        assertEquals(permissionManager.checkPermission(view1,
                                                       user),
                     ACCESS_DENIED);
        assertEquals(permissionManager.checkPermission(view2,
                                                       user),
                     ACCESS_ABSTAIN);
        assertEquals(permissionManager.checkPermission(noView1,
                                                       user),
                     ACCESS_GRANTED);
    }

    @Test
    public void testCheckPermission7() {
        User user = createUserMock("onlyView1",
                                   "noView1");
        assertEquals(permissionManager.checkPermission(viewAll,
                                                       user),
                     ACCESS_DENIED);
        assertEquals(permissionManager.checkPermission(view1,
                                                       user),
                     ACCESS_GRANTED);
        assertEquals(permissionManager.checkPermission(view2,
                                                       user),
                     ACCESS_DENIED);
        assertEquals(permissionManager.checkPermission(noView1,
                                                       user),
                     ACCESS_DENIED);
    }

    @Test
    public void testCheckPermission8() {
        User user = createUserMock("noView1",
                                   "onlyView12");
        assertEquals(permissionManager.checkPermission(viewAll,
                                                       user),
                     ACCESS_ABSTAIN);
        assertEquals(permissionManager.checkPermission(view1,
                                                       user),
                     ACCESS_DENIED);
        assertEquals(permissionManager.checkPermission(view2,
                                                       user),
                     ACCESS_ABSTAIN);
        assertEquals(permissionManager.checkPermission(view12,
                                                       user),
                     ACCESS_GRANTED);
        assertEquals(permissionManager.checkPermission(noView1,
                                                       user),
                     ACCESS_GRANTED);
    }

    @Test
    public void testCacheHits() {
        User user = createUserMock("viewAll");
        permissionManager.checkPermission(viewAll,
                                          user);
        permissionManager.checkPermission(viewAll,
                                          user);
        permissionManager.checkPermission(viewAll,
                                          user);
        permissionManager.checkPermission(viewAll,
                                          user);
        verify(permissionManager,
               times(1)).resolvePermissions(user,
                                            VotingStrategy.PRIORITY);
        verify(authzResultCache,
               times(1)).put(user,
                             viewAll,
                             AuthorizationResult.ACCESS_GRANTED);
        verify(authzResultCache,
               times(4)).get(user,
                             viewAll);
        assertEquals(authzResultCache.size(user),
                     1);
        assertEquals(authzResultCache.size(createUserMock()),
                     0);
    }

    @Test
    public void testDefaultVotingStrategy() {
        User user = createUserMock("role1");
        assertEquals(permissionManager.getDefaultVotingStrategy(),
                     VotingStrategy.PRIORITY);

        VotingAlgorithm unanimousVoter = mock(VotingAlgorithm.class);
        when(unanimousVoter.vote(any())).thenReturn(AuthorizationResult.ACCESS_GRANTED);

        permissionManager.setDefaultVotingStrategy(VotingStrategy.UNANIMOUS);
        permissionManager.setVotingAlgorithm(VotingStrategy.UNANIMOUS,
                                             unanimousVoter);
        permissionManager.checkPermission(viewAll,
                                          user);
        verify(unanimousVoter).vote(any());

        permissionManager.checkPermission(viewAll,
                                          user,
                                          null);
        verify(unanimousVoter).vote(any());

        VotingAlgorithm affirmativeVoter = mock(VotingAlgorithm.class);
        when(affirmativeVoter.vote(any())).thenReturn(AuthorizationResult.ACCESS_GRANTED);
        authzResultCache.clear();
        permissionManager.setDefaultVotingStrategy(VotingStrategy.AFFIRMATIVE);
        permissionManager.setVotingAlgorithm(VotingStrategy.AFFIRMATIVE,
                                             affirmativeVoter);
        permissionManager.checkPermission(viewAll,
                                          user);
        verify(affirmativeVoter).vote(any());

        VotingAlgorithm consensusVoter = mock(VotingAlgorithm.class);
        when(consensusVoter.vote(any())).thenReturn(AuthorizationResult.ACCESS_GRANTED);
        authzResultCache.clear();
        permissionManager.setDefaultVotingStrategy(VotingStrategy.CONSENSUS);
        permissionManager.setVotingAlgorithm(VotingStrategy.CONSENSUS,
                                             consensusVoter);
        permissionManager.checkPermission(viewAll,
                                          user);
        verify(consensusVoter).vote(any());
    }

    @Test
    public void testPriorityVoting1() {
        User user = createUserMock("role1",
                                   "role2",
                                   "role3");
        AuthorizationPolicy policy = permissionManager.newAuthorizationPolicy()
                .role("role1",
                      1).permission("resource.read",
                                    true)
                .role("role2",
                      2).permission("resource.read",
                                    false)
                .role("role3",
                      3).permission("resource.read.1",
                                    true)
                .build();

        permissionManager.setAuthorizationPolicy(policy);
        assertEquals(permissionManager.checkPermission(view1,
                                                       user),
                     ACCESS_GRANTED);

        PermissionCollection pc = permissionManager.resolvePermissions(user,
                                                                       VotingStrategy.PRIORITY);
        Collection<Permission> permissions = pc.collection();
        assertEquals(permissions.size(),
                     2);
        assertTrue(permissions.contains(denyAll));
        assertTrue(permissions.contains(view1));
    }

    @Test
    public void testPriorityVoting2() {
        User user = createUserMock("role1",
                                   "role2",
                                   "role3");
        AuthorizationPolicy policy = permissionManager.newAuthorizationPolicy()
                .role("role1",
                      3).permission("resource.read",
                                    true)
                .role("role2",
                      2).permission("resource.read",
                                    false)
                .role("role3",
                      1).permission("resource.read.1",
                                    true)
                .build();

        permissionManager.setAuthorizationPolicy(policy);
        assertEquals(permissionManager.checkPermission(view1,
                                                       user),
                     ACCESS_GRANTED);

        PermissionCollection pc = permissionManager.resolvePermissions(user,
                                                                       VotingStrategy.PRIORITY);
        Collection<Permission> permissions = pc.collection();
        assertEquals(permissions.size(),
                     1);
        assertTrue(permissions.contains(viewAll));
    }

    @Test
    public void testPriorityVoting3() {
        User user = createUserMock("role1",
                                   "role2",
                                   "role3");
        AuthorizationPolicy policy = permissionManager.newAuthorizationPolicy()
                .role("role1",
                      1).permission("resource.read",
                                    true)
                .role("role2",
                      2).permission("resource.read",
                                    false)
                .role("role3",
                      1).permission("resource.read.1",
                                    true)
                .build();

        permissionManager.setAuthorizationPolicy(policy);
        assertEquals(permissionManager.checkPermission(view1,
                                                       user),
                     ACCESS_DENIED);

        PermissionCollection pc = permissionManager.resolvePermissions(user,
                                                                       VotingStrategy.PRIORITY);
        Collection<Permission> permissions = pc.collection();
        assertEquals(permissions.size(),
                     1);
        assertTrue(permissions.contains(denyAll));
    }

    @Test
    public void testUnanimousVoting() {
        permissionManager.setAuthorizationPolicy(permissionManager.newAuthorizationPolicy()
                                                         .role("role1").permission("resource.read",
                                                                                   true)
                                                         .role("role2").permission("resource.read",
                                                                                   false)
                                                         .role("role3").permission("resource.read",
                                                                                   true)
                                                         .build());

        User user = createUserMock("role1",
                                   "role2",
                                   "role3");
        assertEquals(permissionManager.checkPermission(viewAll,
                                                       user,
                                                       VotingStrategy.UNANIMOUS),
                     ACCESS_DENIED);

        user = createUserMock("role1",
                              "role3");
        assertEquals(permissionManager.checkPermission(viewAll,
                                                       user,
                                                       VotingStrategy.UNANIMOUS),
                     ACCESS_GRANTED);
    }

    @Test
    public void testConsensusVoting() {
        permissionManager.setAuthorizationPolicy(permissionManager.newAuthorizationPolicy()
                                                         .role("role1").permission("resource.read",
                                                                                   true)
                                                         .role("role2").permission("resource.read",
                                                                                   false)
                                                         .role("role3").permission("resource.read",
                                                                                   true)
                                                         .build());

        User user = createUserMock("role1",
                                   "role2",
                                   "role3");
        assertEquals(permissionManager.checkPermission(viewAll,
                                                       user,
                                                       VotingStrategy.CONSENSUS),
                     ACCESS_DENIED);

        user = createUserMock("role1",
                              "role3");
        assertEquals(permissionManager.checkPermission(viewAll,
                                                       user,
                                                       VotingStrategy.CONSENSUS),
                     ACCESS_GRANTED);
    }

    @Test
    public void testAffirmativeVoting() {
        permissionManager.setAuthorizationPolicy(permissionManager.newAuthorizationPolicy()
                                                         .role("role1").permission("resource.read",
                                                                                   true)
                                                         .role("role2").permission("resource.read",
                                                                                   false)
                                                         .role("role3").permission("resource.read",
                                                                                   true)
                                                         .build());

        User user = createUserMock("role1",
                                   "role2",
                                   "role3");
        assertEquals(permissionManager.checkPermission(viewAll,
                                                       user,
                                                       VotingStrategy.AFFIRMATIVE),
                     ACCESS_GRANTED);

        user = createUserMock("role1",
                              "role3");
        assertEquals(permissionManager.checkPermission(viewAll,
                                                       user,
                                                       VotingStrategy.AFFIRMATIVE),
                     ACCESS_GRANTED);
    }
}
