/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
 *
 * 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.springframework.security.cas.authentication;

import static org.mockito.Mockito.*;
import static org.junit.Assert.*;

import org.jasig.cas.client.validation.Assertion;
import org.jasig.cas.client.validation.AssertionImpl;
import org.jasig.cas.client.validation.TicketValidationException;
import org.jasig.cas.client.validation.TicketValidator;
import org.junit.*;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.web.CasAuthenticationFilter;
import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetails;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.WebAuthenticationDetails;

import java.util.*;


/**
 * Tests {@link CasAuthenticationProvider}.
 *
 * @author Ben Alex
 * @author Scott Battaglia
 */
@SuppressWarnings("unchecked")
public class CasAuthenticationProviderTests {
    //~ Methods ========================================================================================================

    private UserDetails makeUserDetails() {
        return new User("user", "password", true, true, true, true,
                AuthorityUtils.createAuthorityList("ROLE_ONE", "ROLE_TWO"));
    }

    private UserDetails makeUserDetailsFromAuthoritiesPopulator() {
        return new User("user", "password", true, true, true, true,
                AuthorityUtils.createAuthorityList("ROLE_A", "ROLE_B"));
    }

    private ServiceProperties makeServiceProperties() {
        final ServiceProperties serviceProperties = new ServiceProperties();
        serviceProperties.setSendRenew(false);
        serviceProperties.setService("http://test.com");

        return serviceProperties;
    }

    @Test
    public void statefulAuthenticationIsSuccessful() throws Exception {
        CasAuthenticationProvider cap = new CasAuthenticationProvider();
        cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator());
        cap.setKey("qwerty");

        StatelessTicketCache cache = new MockStatelessTicketCache();
        cap.setStatelessTicketCache(cache);
        cap.setServiceProperties(makeServiceProperties());

        cap.setTicketValidator(new MockTicketValidator(true));
        cap.afterPropertiesSet();

        UsernamePasswordAuthenticationToken token =
            new UsernamePasswordAuthenticationToken(CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER, "ST-123");
        token.setDetails("details");

        Authentication result = cap.authenticate(token);

        // Confirm ST-123 was NOT added to the cache
        assertTrue(cache.getByTicketId("ST-456") == null);

        if (!(result instanceof CasAuthenticationToken)) {
            fail("Should have returned a CasAuthenticationToken");
        }

        CasAuthenticationToken casResult = (CasAuthenticationToken) result;
        assertEquals(makeUserDetailsFromAuthoritiesPopulator(), casResult.getPrincipal());
        assertEquals("ST-123", casResult.getCredentials());
        assertTrue(casResult.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_A")));
        assertTrue(casResult.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_B")));
        assertEquals(cap.getKey().hashCode(), casResult.getKeyHash());
        assertEquals("details", casResult.getDetails());

        // Now confirm the CasAuthenticationToken is automatically re-accepted.
        // To ensure TicketValidator not called again, set it to deliver an exception...
        cap.setTicketValidator(new MockTicketValidator(false));

        Authentication laterResult = cap.authenticate(result);
        assertEquals(result, laterResult);
    }

    @Test
    public void statelessAuthenticationIsSuccessful() throws Exception {
        CasAuthenticationProvider cap = new CasAuthenticationProvider();
        cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator());
        cap.setKey("qwerty");

        StatelessTicketCache cache = new MockStatelessTicketCache();
        cap.setStatelessTicketCache(cache);
        cap.setTicketValidator(new MockTicketValidator(true));
        cap.setServiceProperties(makeServiceProperties());
        cap.afterPropertiesSet();

        UsernamePasswordAuthenticationToken token =
            new UsernamePasswordAuthenticationToken(CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER, "ST-456");
        token.setDetails("details");

        Authentication result = cap.authenticate(token);

        // Confirm ST-456 was added to the cache
        assertTrue(cache.getByTicketId("ST-456") != null);

        if (!(result instanceof CasAuthenticationToken)) {
            fail("Should have returned a CasAuthenticationToken");
        }

        assertEquals(makeUserDetailsFromAuthoritiesPopulator(), result.getPrincipal());
        assertEquals("ST-456", result.getCredentials());
        assertEquals("details", result.getDetails());

        // Now try to authenticate again. To ensure TicketValidator not
        // called again, set it to deliver an exception...
        cap.setTicketValidator(new MockTicketValidator(false));

        // Previously created UsernamePasswordAuthenticationToken is OK
        Authentication newResult = cap.authenticate(token);
        assertEquals(makeUserDetailsFromAuthoritiesPopulator(), newResult.getPrincipal());
        assertEquals("ST-456", newResult.getCredentials());
    }

    @Test
    public void authenticateAllNullService() throws Exception {
        String serviceUrl = "https://service/context";
        ServiceAuthenticationDetails details = mock(ServiceAuthenticationDetails.class);
        when(details.getServiceUrl()).thenReturn(serviceUrl);
        TicketValidator validator = mock(TicketValidator.class);
        when(validator.validate(any(String.class),any(String.class))).thenReturn(new AssertionImpl("rod"));

        ServiceProperties serviceProperties = makeServiceProperties();
        serviceProperties.setAuthenticateAllArtifacts(true);

        CasAuthenticationProvider cap = new CasAuthenticationProvider();
        cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator());
        cap.setKey("qwerty");

        cap.setTicketValidator(validator);
        cap.setServiceProperties(serviceProperties);
        cap.afterPropertiesSet();

        String ticket = "ST-456";
        UsernamePasswordAuthenticationToken token =
            new UsernamePasswordAuthenticationToken(CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER, ticket);

        Authentication result = cap.authenticate(token);
    }

    @Test
    public void authenticateAllAuthenticationIsSuccessful() throws Exception {
        String serviceUrl = "https://service/context";
        ServiceAuthenticationDetails details = mock(ServiceAuthenticationDetails.class);
        when(details.getServiceUrl()).thenReturn(serviceUrl);
        TicketValidator validator = mock(TicketValidator.class);
        when(validator.validate(any(String.class),any(String.class))).thenReturn(new AssertionImpl("rod"));

        ServiceProperties serviceProperties = makeServiceProperties();
        serviceProperties.setAuthenticateAllArtifacts(true);

        CasAuthenticationProvider cap = new CasAuthenticationProvider();
        cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator());
        cap.setKey("qwerty");

        cap.setTicketValidator(validator);
        cap.setServiceProperties(serviceProperties);
        cap.afterPropertiesSet();

        String ticket = "ST-456";
        UsernamePasswordAuthenticationToken token =
            new UsernamePasswordAuthenticationToken(CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER, ticket);

        Authentication result = cap.authenticate(token);
        verify(validator).validate(ticket, serviceProperties.getService());

        serviceProperties.setAuthenticateAllArtifacts(true);
        result = cap.authenticate(token);
        verify(validator,times(2)).validate(ticket, serviceProperties.getService());

        token.setDetails(details);
        result = cap.authenticate(token);
        verify(validator).validate(ticket, serviceUrl);

        serviceProperties.setAuthenticateAllArtifacts(false);
        serviceProperties.setService(null);
        cap.setServiceProperties(serviceProperties);
        cap.afterPropertiesSet();
        result = cap.authenticate(token);
        verify(validator,times(2)).validate(ticket, serviceUrl);

        token.setDetails(new WebAuthenticationDetails(new MockHttpServletRequest()));
        try {
            cap.authenticate(token);
            fail("Expected Exception");
        }catch(IllegalStateException success) {}

        cap.setServiceProperties(null);
        cap.afterPropertiesSet();
        try {
            cap.authenticate(token);
            fail("Expected Exception");
        }catch(IllegalStateException success) {}
    }

    @Test(expected = BadCredentialsException.class)
    public void missingTicketIdIsDetected() throws Exception {
        CasAuthenticationProvider cap = new CasAuthenticationProvider();
        cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator());
        cap.setKey("qwerty");

        StatelessTicketCache cache = new MockStatelessTicketCache();
        cap.setStatelessTicketCache(cache);
        cap.setTicketValidator(new MockTicketValidator(true));
        cap.setServiceProperties(makeServiceProperties());
        cap.afterPropertiesSet();

        UsernamePasswordAuthenticationToken token =
                new UsernamePasswordAuthenticationToken(CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER, "");

        cap.authenticate(token);
    }

    @Test(expected = BadCredentialsException.class)
    public void invalidKeyIsDetected() throws Exception {
        final Assertion assertion = new AssertionImpl("test");
        CasAuthenticationProvider cap = new CasAuthenticationProvider();
        cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator());
        cap.setKey("qwerty");

        StatelessTicketCache cache = new MockStatelessTicketCache();
        cap.setStatelessTicketCache(cache);
        cap.setTicketValidator(new MockTicketValidator(true));
        cap.setServiceProperties(makeServiceProperties());
        cap.afterPropertiesSet();

        CasAuthenticationToken token = new CasAuthenticationToken("WRONG_KEY", makeUserDetails(), "credentials",
                AuthorityUtils.createAuthorityList("XX"), makeUserDetails(), assertion);

        cap.authenticate(token);
    }

    @Test(expected = IllegalArgumentException.class)
    public void detectsMissingAuthoritiesPopulator() throws Exception {
        CasAuthenticationProvider cap = new CasAuthenticationProvider();
        cap.setKey("qwerty");
        cap.setStatelessTicketCache(new MockStatelessTicketCache());
        cap.setTicketValidator(new MockTicketValidator(true));
        cap.setServiceProperties(makeServiceProperties());
        cap.afterPropertiesSet();
    }

    @Test(expected = IllegalArgumentException.class)
    public void detectsMissingKey() throws Exception {
        CasAuthenticationProvider cap = new CasAuthenticationProvider();
        cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator());
        cap.setStatelessTicketCache(new MockStatelessTicketCache());
        cap.setTicketValidator(new MockTicketValidator(true));
        cap.setServiceProperties(makeServiceProperties());
        cap.afterPropertiesSet();
    }

    @Test(expected = IllegalArgumentException.class)
    public void detectsMissingStatelessTicketCache() throws Exception {
        CasAuthenticationProvider cap = new CasAuthenticationProvider();
        // set this explicitly to null to test failure
        cap.setStatelessTicketCache(null);
        cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator());
        cap.setKey("qwerty");
        cap.setTicketValidator(new MockTicketValidator(true));
        cap.setServiceProperties(makeServiceProperties());
        cap.afterPropertiesSet();
    }

    @Test(expected = IllegalArgumentException.class)
    public void detectsMissingTicketValidator() throws Exception {
        CasAuthenticationProvider cap = new CasAuthenticationProvider();
        cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator());
        cap.setKey("qwerty");
        cap.setStatelessTicketCache(new MockStatelessTicketCache());
        cap.setServiceProperties(makeServiceProperties());
        cap.afterPropertiesSet();
    }

    @Test
    public void gettersAndSettersMatch() throws Exception {
        CasAuthenticationProvider cap = new CasAuthenticationProvider();
        cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator());
        cap.setKey("qwerty");
        cap.setStatelessTicketCache(new MockStatelessTicketCache());
        cap.setTicketValidator(new MockTicketValidator(true));
        cap.setServiceProperties(makeServiceProperties());
        cap.afterPropertiesSet();

// TODO disabled because why do we need to expose this?
//        assertTrue(cap.getUserDetailsService() != null);
        assertEquals("qwerty", cap.getKey());
        assertTrue(cap.getStatelessTicketCache() != null);
        assertTrue(cap.getTicketValidator() != null);
    }

    @Test
    public void ignoresClassesItDoesNotSupport() throws Exception {
        CasAuthenticationProvider cap = new CasAuthenticationProvider();
        cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator());
        cap.setKey("qwerty");
        cap.setStatelessTicketCache(new MockStatelessTicketCache());
        cap.setTicketValidator(new MockTicketValidator(true));
        cap.setServiceProperties(makeServiceProperties());
        cap.afterPropertiesSet();

        TestingAuthenticationToken token = new TestingAuthenticationToken("user", "password", "ROLE_A");
        assertFalse(cap.supports(TestingAuthenticationToken.class));

        // Try it anyway
        assertEquals(null, cap.authenticate(token));
    }

    @Test
    public void ignoresUsernamePasswordAuthenticationTokensWithoutCasIdentifiersAsPrincipal() throws Exception {
        CasAuthenticationProvider cap = new CasAuthenticationProvider();
        cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator());
        cap.setKey("qwerty");
        cap.setStatelessTicketCache(new MockStatelessTicketCache());
        cap.setTicketValidator(new MockTicketValidator(true));
        cap.setServiceProperties(makeServiceProperties());
        cap.afterPropertiesSet();

        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("some_normal_user",
                "password", AuthorityUtils.createAuthorityList("ROLE_A"));
        assertEquals(null, cap.authenticate(token));
    }

    @Test
    public void supportsRequiredTokens() {
        CasAuthenticationProvider cap = new CasAuthenticationProvider();
        assertTrue(cap.supports(UsernamePasswordAuthenticationToken.class));
        assertTrue(cap.supports(CasAuthenticationToken.class));
    }

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

    private class MockAuthoritiesPopulator implements AuthenticationUserDetailsService {

        public UserDetails loadUserDetails(final Authentication token) throws UsernameNotFoundException {
            return makeUserDetailsFromAuthoritiesPopulator();
        }
    }

    private class MockStatelessTicketCache implements StatelessTicketCache {
        private Map<String, CasAuthenticationToken> cache = new HashMap<String, CasAuthenticationToken>();

        public CasAuthenticationToken getByTicketId(String serviceTicket) {
            return cache.get(serviceTicket);
        }

        public void putTicketInCache(CasAuthenticationToken token) {
            cache.put(token.getCredentials().toString(), token);
        }

        public void removeTicketFromCache(CasAuthenticationToken token) {
            throw new UnsupportedOperationException("mock method not implemented");
        }

        public void removeTicketFromCache(String serviceTicket) {
            throw new UnsupportedOperationException("mock method not implemented");
        }
    }

    private class MockTicketValidator implements TicketValidator {
        private boolean returnTicket;

        public MockTicketValidator(boolean returnTicket) {
            this.returnTicket = returnTicket;
        }

        public Assertion validate(final String ticket, final String service)
                throws TicketValidationException {
            if (returnTicket) {
                return new AssertionImpl("rod");
            }
            throw new BadCredentialsException("As requested from mock");
        }
    }
}
