package org.springframework.security.access.expression.method;

import static org.junit.Assert.assertEquals;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

import org.aopalliance.intercept.MethodInvocation;
import org.junit.Test;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.expression.method.PreInvocationExpressionAttribute;
import org.springframework.security.access.prepost.PreInvocationAuthorizationAdviceVoter;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.util.SimpleMethodInvocation;

@SuppressWarnings("unchecked")
public class MethodExpressionVoterTests {
    private TestingAuthenticationToken joe = new TestingAuthenticationToken("joe", "joespass", "blah");
    private PreInvocationAuthorizationAdviceVoter am =
        new PreInvocationAuthorizationAdviceVoter(new ExpressionBasedPreInvocationAdvice());

    @Test
    public void hasRoleExpressionAllowsUserWithRole() throws Exception {
        MethodInvocation mi = new SimpleMethodInvocation(new TargetImpl(), methodTakingAnArray());
        assertEquals(AccessDecisionVoter.ACCESS_GRANTED, am.vote(joe, mi, createAttributes(new PreInvocationExpressionAttribute(null, null, "hasRole('blah')"))));
    }

    @Test
    public void hasRoleExpressionDeniesUserWithoutRole() throws Exception {
        List<ConfigAttribute> cad = new ArrayList<ConfigAttribute>(1);
        cad.add(new PreInvocationExpressionAttribute(null, null, "hasRole('joedoesnt')"));
        MethodInvocation mi = new SimpleMethodInvocation(new TargetImpl(), methodTakingAnArray());
        assertEquals(AccessDecisionVoter.ACCESS_DENIED, am.vote(joe, mi, cad));
    }

    @Test
    public void matchingArgAgainstAuthenticationNameIsSuccessful() throws Exception {
        MethodInvocation mi = new SimpleMethodInvocation(new TargetImpl(), methodTakingAString(), "joe");
        assertEquals(AccessDecisionVoter.ACCESS_GRANTED,
                am.vote(joe, mi, createAttributes(new PreInvocationExpressionAttribute(null, null, "(#argument == principal) and (principal == 'joe')"))));
    }

    @Test
    public void accessIsGrantedIfNoPreAuthorizeAttributeIsUsed() throws Exception {
        Collection arg = createCollectionArg("joe", "bob", "sam");
        MethodInvocation mi = new SimpleMethodInvocation(new TargetImpl(), methodTakingACollection(), arg);
        assertEquals(AccessDecisionVoter.ACCESS_GRANTED,
                am.vote(joe, mi, createAttributes(new PreInvocationExpressionAttribute("(filterObject == 'jim')", "collection", null))));
        // All objects should have been removed, because the expression is always false
        assertEquals(0, arg.size());
    }

    @Test
    public void collectionPreFilteringIsSuccessful() throws Exception {
        List arg = createCollectionArg("joe", "bob", "sam");
        MethodInvocation mi = new SimpleMethodInvocation(new TargetImpl(), methodTakingACollection(), arg);
        am.vote(joe, mi, createAttributes(new PreInvocationExpressionAttribute("(filterObject == 'joe' or filterObject == 'sam')", "collection", "permitAll")));
        assertEquals("joe and sam should still be in the list", 2, arg.size());
        assertEquals("joe", arg.get(0));
        assertEquals("sam", arg.get(1));
    }

    @Test(expected=IllegalArgumentException.class)
    public void arraysCannotBePrefiltered() throws Exception {
        MethodInvocation mi = new SimpleMethodInvocation(new TargetImpl(), methodTakingAnArray(), createArrayArg("sam","joe"));
        am.vote(joe, mi, createAttributes(new PreInvocationExpressionAttribute("(filterObject == 'jim')", "someArray", null)));
    }

    @Test(expected=IllegalArgumentException.class)
    public void incorrectFilterTargetNameIsRejected() throws Exception {
        MethodInvocation mi = new SimpleMethodInvocation(new TargetImpl(), methodTakingACollection(), createCollectionArg("joe", "bob"));
        am.vote(joe, mi, createAttributes(new PreInvocationExpressionAttribute("(filterObject == 'joe')", "collcetion", null)));
    }

    @Test(expected=IllegalArgumentException.class)
    public void nullNamedFilterTargetIsRejected() throws Exception {
        MethodInvocation mi = new SimpleMethodInvocation(new TargetImpl(), methodTakingACollection(), new Object[] {null});
        am.vote(joe, mi, createAttributes(new PreInvocationExpressionAttribute("(filterObject == 'joe')", "collection", null)));
    }

    @Test
    public void ruleDefinedInAClassMethodIsApplied() throws Exception {
        MethodInvocation mi = new SimpleMethodInvocation(new TargetImpl(), methodTakingAString(), "joe");
        assertEquals(AccessDecisionVoter.ACCESS_GRANTED, am.vote(joe, mi,
                createAttributes(new PreInvocationExpressionAttribute(null, null, "T(org.springframework.security.access.expression.method.SecurityRules).isJoe(#argument)"))));
    }

    private List<ConfigAttribute> createAttributes(ConfigAttribute... attributes) {
        return Arrays.asList(attributes);
    }

    private List createCollectionArg(Object... elts) {
        ArrayList result = new ArrayList(elts.length);
        result.addAll(Arrays.asList(elts));
        return result;
    }

    private Object createArrayArg(Object... elts) {
        ArrayList result = new ArrayList(elts.length);
        result.addAll(Arrays.asList(elts));
        return result.toArray(new Object[0]);
    }

    private Method methodTakingAnArray() throws Exception {
        return Target.class.getMethod("methodTakingAnArray", Object[].class);
    }

    private Method methodTakingAString() throws Exception {
        return Target.class.getMethod("methodTakingAString", String.class);
    }

    private Method methodTakingACollection() throws Exception {
        return Target.class.getMethod("methodTakingACollection", Collection.class);
    }


    //~ Inner Classes ==================================================================================================

    private interface Target {
        void methodTakingAnArray(Object[] args);

        void methodTakingAString(String argument);

        Collection methodTakingACollection(Collection collection);
    }

    private static class TargetImpl implements Target {
        public void methodTakingAnArray(Object[] args) {}

        public void methodTakingAString(String argument) {};

        public Collection methodTakingACollection(Collection collection) {return collection;}
    }
}
