/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2016 ForgeRock AS.
 */

package org.forgerock.oauth2.core;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.*;
import static org.testng.Assert.*;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.StringReader;
import java.net.URI;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import com.iplanet.sso.SSOException;
import com.iplanet.sso.SSOToken;
import com.iplanet.sso.SSOTokenManager;
import com.sun.identity.authentication.AuthContext;
import freemarker.template.Configuration;
import freemarker.template.Template;
import org.forgerock.oauth2.core.AuthenticationMethod;
import org.forgerock.oauth2.core.OAuth2ProviderSettings;
import org.forgerock.oauth2.core.OAuth2ProviderSettingsFactory;
import org.forgerock.oauth2.core.OAuth2Request;
import org.forgerock.oauth2.core.ResourceOwnerSessionValidator;
import org.forgerock.oauth2.core.exceptions.BadRequestException;
import org.forgerock.oauth2.core.exceptions.InteractionRequiredException;
import org.forgerock.oauth2.core.exceptions.ResourceOwnerAuthenticationRequired;
import org.forgerock.oauth2.core.exceptions.ServerException;
import org.forgerock.openam.core.guice.CoreGuiceModule;
import org.forgerock.openam.oauth2.ClientCredentialsReader;
import org.forgerock.openam.oauth2.OpenAMAuthenticationMethod;
import org.forgerock.openidconnect.ClientDAO;
import org.restlet.Request;
import org.restlet.data.Reference;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

/**
 * @since 12.0.0
 */
public class ResourceOwnerSessionValidatorTest {

    private static final SSOToken ACTIVE_SESSION_TOKEN = mock(SSOToken.class);
    private static final SSOToken NO_SESSION_TOKEN = null;

    private ResourceOwnerSessionValidator resourceOwnerSessionValidator;

    private SSOTokenManager mockSSOTokenManager;
    private OAuth2ProviderSettingsFactory mockProviderSettingsFactory;
    private OAuth2ProviderSettings providerSettings;
    private OAuth2Request mockOAuth2Request;
    private Request restletRequest;
    private HttpServletRequest mockHttpServletRequest;

    @BeforeMethod
    public void setUp() throws Exception {

        mockSSOTokenManager = mock(SSOTokenManager.class);
        mockProviderSettingsFactory = mock(OAuth2ProviderSettingsFactory.class);
        providerSettings = mock(OAuth2ProviderSettings.class);
        mockOAuth2Request = mock(OAuth2Request.class);
        restletRequest = new Request();
        mockHttpServletRequest = mock(HttpServletRequest.class);
        CoreGuiceModule.DNWrapper dnWrapper = mock(CoreGuiceModule.DNWrapper.class);
        ClientDAO mockClientDAO = mock(ClientDAO.class);
        ClientCredentialsReader mockClientCredentialsReader = mock(ClientCredentialsReader.class);

        given(mockOAuth2Request.getRequest()).willReturn(restletRequest);

        given(mockHttpServletRequest.getRequestURI()).willReturn("/openam/oauth2/authorize");
        given(mockHttpServletRequest.getScheme()).willReturn("http");
        given(mockHttpServletRequest.getServerName()).willReturn("openam.example.com");
        given(mockHttpServletRequest.getServerPort()).willReturn(8080);
        given(ACTIVE_SESSION_TOKEN.getProperty("Organization")).willReturn("/");

        given(mockProviderSettingsFactory.get(any(OAuth2Request.class))).willReturn(providerSettings);

        resourceOwnerSessionValidator =
                new ResourceOwnerSessionValidator(dnWrapper, mockSSOTokenManager, mockProviderSettingsFactory,
                        mockClientDAO, mockClientCredentialsReader) {
            @Override
            HttpServletRequest getHttpServletRequest(Request req) {
                return mockHttpServletRequest;
            }
        };

        when(dnWrapper.orgNameToDN("/")).thenReturn("/");
    }

    @Test (expectedExceptions = BadRequestException.class)
    public void shouldFailIfInvalidCombinationOfPromptsArePresent() throws Exception {

        //Given
        mockPrompt("login none");
        mockSSOToken(NO_SESSION_TOKEN);

        //When
        resourceOwnerSessionValidator.validate(mockOAuth2Request);

        // Then
        // BadRequestException
    }

    @Test
    public void shouldForceAuthentication() throws Exception {

        //Given
        mockPrompt("");
        mockSSOToken(NO_SESSION_TOKEN);

        try {
            //When
            resourceOwnerSessionValidator.validate(mockOAuth2Request);
            fail();

        } catch (ResourceOwnerAuthenticationRequired ex) {
            // Then
            assertEquals("goto=http://openam.example.com:8080/openam/oauth2/authorize?prompt=",
                    ex.getRedirectUri().getQuery());
        }
    }

    @Test
    public void shouldForceReauthenticationWhenLoginPromptIsPresent() throws Exception {

        //Given
        mockPrompt("login");
        mockSSOToken(ACTIVE_SESSION_TOKEN);

        try {
            //When
            resourceOwnerSessionValidator.validate(mockOAuth2Request);
            fail();

        } catch (ResourceOwnerAuthenticationRequired ex) {
            // Then
            assertEquals("goto=http://openam.example.com:8080/openam/oauth2/authorize?prompt=",
                    ex.getRedirectUri().getQuery());
            verify(mockSSOTokenManager).destroyToken(ACTIVE_SESSION_TOKEN);
        }
    }

    @Test
    public void shouldForceReauthenticationWhenLoginAndConsentPromptsArePresent() throws Exception {

        //Given
        mockPrompt("login consent");
        mockSSOToken(ACTIVE_SESSION_TOKEN);

        try {
            //When
            resourceOwnerSessionValidator.validate(mockOAuth2Request);
            fail();

        } catch (ResourceOwnerAuthenticationRequired ex) {
            // Then
            assertEquals("goto=http://openam.example.com:8080/openam/oauth2/authorize?prompt=consent",
                    ex.getRedirectUri().getQuery());
            verify(mockSSOTokenManager).destroyToken(ACTIVE_SESSION_TOKEN);
        }
    }

    @Test (expectedExceptions = InteractionRequiredException.class)
    public void shouldFailIfUserIsNotAuthenticatedAndNonePromptIsPresent() throws Exception {

        //Given
        mockPrompt("none");
        mockSSOToken(NO_SESSION_TOKEN);

        //When
        resourceOwnerSessionValidator.validate(mockOAuth2Request);

        // Then
        // InteractionRequiredException
    }

    // OPENAM-4092: When the user has no SSO token and specifies prompt=consent the
    // user should be presented with the OpenAM login page
    //
    @Test (expectedExceptions = ResourceOwnerAuthenticationRequired.class)
    public void shouldFailIfUserIsNotAuthenticatedAndOnlyConsentPromptIsPresent() throws Exception {

        //Given
        mockPrompt("consent");
        mockSSOToken(NO_SESSION_TOKEN);

        //When
        resourceOwnerSessionValidator.validate(mockOAuth2Request);

        // Then
        // LoginRequiredException
    }

    @Test
    public void shouldUseAcrValuesIfSpecified() throws Exception {
        // Given
        String acrValues = "1 2 3";
        String service = "myAuthService";
        mockPrompt("login");
        mockSSOToken(NO_SESSION_TOKEN);
        mockRequestAcrValues(acrValues);
        mockAcrValuesMap(Collections.<String, AuthenticationMethod>singletonMap("2",
                new OpenAMAuthenticationMethod(service, AuthContext.IndexType.SERVICE)));

        // When
        URI loginUri = null;
        try {
            resourceOwnerSessionValidator.validate(mockOAuth2Request);
            fail();
        } catch (ResourceOwnerAuthenticationRequired ex) {
            loginUri = ex.getRedirectUri();
        }

        // Then
        assertTrue(loginUri.getQuery().contains("service=" + service));
    }

    @Test
    public void shouldUseFirstAcrValueThatIsSupported() throws Exception {
        // Given
        String acrValues = "1 2 3";
        mockPrompt("login");
        mockSSOToken(NO_SESSION_TOKEN);
        mockRequestAcrValues(acrValues);
        final Map<String, AuthenticationMethod> acrMap = new HashMap<>();
        acrMap.put("2", new OpenAMAuthenticationMethod("service2", AuthContext.IndexType.SERVICE));
        acrMap.put("3", new OpenAMAuthenticationMethod("service3", AuthContext.IndexType.SERVICE));

        mockAcrValuesMap(acrMap);

        // When
        URI loginUri = null;
        try {
            resourceOwnerSessionValidator.validate(mockOAuth2Request);
            fail();
        } catch (ResourceOwnerAuthenticationRequired ex) {
            loginUri = ex.getRedirectUri();
        }

        // Then
        assertTrue(loginUri.getQuery().contains("service=service2"));
    }

    @Test
    public void shouldUseDefaultAuthChainIfNoAcrValuesSpecified() throws Exception {
        // Given
        mockPrompt("login");
        mockSSOToken(NO_SESSION_TOKEN);

        // When
        URI loginUri = null;
        try {
            resourceOwnerSessionValidator.validate(mockOAuth2Request);
            fail();
        } catch (ResourceOwnerAuthenticationRequired ex) {
            loginUri = ex.getRedirectUri();
        }

        // Then
        assertFalse(loginUri.getQuery().contains("service="));
    }

    @Test
    public void shouldUseDefaultAuthChainWhenNoSupportedAcrValue() throws Exception {
        // Given
        mockPrompt("login");
        mockSSOToken(NO_SESSION_TOKEN);
        mockRequestAcrValues("not_supported");
        mockAcrValuesMap(Collections.<String, AuthenticationMethod>emptyMap());

        // When
        URI loginUri = null;
        try {
            resourceOwnerSessionValidator.validate(mockOAuth2Request);
            fail();
        } catch (ResourceOwnerAuthenticationRequired ex) {
            loginUri = ex.getRedirectUri();
        }

        // Then
        assertFalse(loginUri.getQuery().contains("service="));
    }

    @Test
    public void shouldRedirectToCustomLoginUrl() throws Exception {

        //Given
        mockCustomLoginUrlTemplate("http://mylogin.com/login?goto=${goto}"
                + "<#if acrValues??>&acr_values=${acrValues}</#if><#if realm??>&realm=${realm}</#if>"
                + "<#if module??>&module=${module}</#if><#if service??>&service=${service}</#if>"
                + "<#if locale??>&locale=${locale}</#if>");

        mockRequestRef();
        mockSSOToken(NO_SESSION_TOKEN);
        mockRequestAcrValues("1 2 3");
        mockRequestRealm("SUB_REALM");
        mockRequestLocale("LOCALE");
        mockRequestModule("AUTHENTICATION_MODULE");
        mockRequestService("AUTHENTICATION_CHAIN");

        //When
        try {
            resourceOwnerSessionValidator.validate(mockOAuth2Request);
            fail();
        } catch (ResourceOwnerAuthenticationRequired e) {
            //Then
            assertThat(e.getRedirectUri().toString()).isEqualTo("http://mylogin.com/login?goto=http%3A%2F%2Fopenam.example.com%3A8080%2Fopenam%2Foauth2%2Fauthorize&acr_values=1+2+3&realm=SUB_REALM&module=AUTHENTICATION_MODULE&service=AUTHENTICATION_CHAIN&locale=LOCALE");
        }
    }

    @Test
    public void shouldRedirectToCustomLoginUrlWithNoQueryParameters() throws Exception {

        //Given
        mockCustomLoginUrlTemplate("http://mylogin.com/login?goto=${goto}"
                + "<#if acrValues??>&acr_values=${acrValues}</#if><#if realm??>&realm=${realm}</#if>"
                + "<#if module??>&module=${module}</#if><#if service??>&service=${service}</#if>"
                + "<#if locale??>&locale=${locale}</#if>");

        mockRequestRef();
        mockSSOToken(NO_SESSION_TOKEN);

        //When
        try {
            resourceOwnerSessionValidator.validate(mockOAuth2Request);
            fail();
        } catch (ResourceOwnerAuthenticationRequired e) {
            //Then
            assertThat(e.getRedirectUri().toString()).isEqualTo("http://mylogin.com/login?goto=http%3A%2F%2Fopenam.example.com%3A8080%2Fopenam%2Foauth2%2Fauthorize");
        }
    }

    private void mockRequestRef() {
        restletRequest.setResourceRef(new Reference("http://openam.example.com:8080/openam/oauth2/authorize"));
    }

    private void mockPrompt(String prompt) {
        restletRequest.setResourceRef(new Reference(
                "http://openam.example.com:8080/openam/oauth2/authorize?prompt=" + prompt));
        given(mockOAuth2Request.getParameter("prompt")).willReturn(prompt);
    }

    private void mockRequestRealm(String realm) {
        given(mockOAuth2Request.getParameter("realm")).willReturn(realm);
    }

    private void mockRequestLocale(String locale) {
        given(mockOAuth2Request.getParameter("locale")).willReturn(locale);
    }

    private void mockRequestModule(String module) {
        given(mockOAuth2Request.getParameter("module")).willReturn(module);
    }

    private void mockRequestService(String service) {
        given(mockOAuth2Request.getParameter("service")).willReturn(service);
    }

    private void mockSSOToken(SSOToken ssoToken) throws SSOException {
        given(mockSSOTokenManager.createSSOToken(mockHttpServletRequest)).willReturn(ssoToken);
    }

    private void mockAcrValuesMap(Map<String, AuthenticationMethod> mapping) throws Exception {
        final OAuth2ProviderSettings mockSettings = mock(OAuth2ProviderSettings.class);
        given(mockProviderSettingsFactory.get(mockOAuth2Request)).willReturn(mockSettings);
        given(mockSettings.getAcrMapping()).willReturn(mapping);
    }

    private void mockCustomLoginUrlTemplate(String customLoginUrlTemplate) throws ServerException, IOException {
        Template template = new Template("", new StringReader(customLoginUrlTemplate), new Configuration());
        given(providerSettings.getCustomLoginUrlTemplate()).willReturn(template);
    }

    private void mockRequestAcrValues(String acrValues) {
        given(mockOAuth2Request.getParameter("acr_values")).willReturn(acrValues);
    }
}
