/*
 * Copyright (c) 2016 Uber Technologies, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.uber.sdk.core.auth;

import com.google.api.client.auth.oauth2.BearerToken;
import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.auth.oauth2.StoredCredential;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpExecuteInterceptor;
import com.google.api.client.http.LowLevelHttpRequest;
import com.google.api.client.http.LowLevelHttpResponse;
import com.google.api.client.testing.http.MockLowLevelHttpRequest;
import com.google.api.client.testing.http.MockLowLevelHttpResponse;
import com.google.api.client.testing.json.MockJsonFactory;
import com.google.api.client.util.store.AbstractDataStoreFactory;
import com.google.api.client.util.store.DataStore;
import com.uber.sdk.core.auth.AuthException;
import com.uber.sdk.core.auth.Scope;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Mockito;

import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;

import static org.hamcrest.Matchers.any;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

/**
 * Tests for {@link OAuth2Credentials}.
 */
public class OAuth2CredentialsTest {

    private static final String TOKEN_REQUEST_URL = "https://login.uber.com/oauth/v2/token";

    @Rule public ExpectedException exception = ExpectedException.none();

    private MockHttpTransport mockHttpTransport;

    @Before
    public void setUp() {
        mockHttpTransport = new MockHttpTransport();
    }

    @Test
    public void getAuthorizationUrl() throws Exception {
        OAuth2Credentials oAuth2Credentials = new OAuth2Credentials.Builder()
                .setClientSecrets("CLIENT_ID", "CLIENT_SECRET")
                .setScopes(Arrays.asList(Scope.PROFILE, Scope.REQUEST,
                        Scope.HISTORY))
                .build();

        assertEquals("https://login.uber.com/oauth/v2/authorize?client_id=CLIENT_ID"
                        + "&response_type=code&scope=history%20profile%20request",
                oAuth2Credentials.getAuthorizationUrl());
    }

    @Test
    public void getAuthorizationUrl_whenThereAreNoScopes() throws Exception {
        OAuth2Credentials oAuth2Credentials = new OAuth2Credentials.Builder()
                .setClientSecrets("CLIENT_ID", "CLIENT_SECRET")
                .build();

        assertEquals("https://login.uber.com/oauth/v2/authorize?client_id=CLIENT_ID&response_type=code",
                oAuth2Credentials.getAuthorizationUrl());
    }

    @Test
    public void getAuthorizationUrl_whenThereIsAnEmptyScopeList() throws Exception {
        OAuth2Credentials oAuth2Credentials = new OAuth2Credentials.Builder()
                .setClientSecrets("CLIENT_ID", "CLIENT_SECRET")
                .setScopes(new ArrayList<Scope>())
                .build();

        assertEquals("https://login.uber.com/oauth/v2/authorize?client_id=CLIENT_ID&response_type=code",
                oAuth2Credentials.getAuthorizationUrl());
    }

    @Test
    public void getAuthorizationUrl_whenThereAreCustomScopes() throws Exception {
        OAuth2Credentials oAuth2Credentials = new OAuth2Credentials.Builder()
                .setClientSecrets("CLIENT_ID", "CLIENT_SECRET")
                .setScopes(Arrays.asList(Scope.PROFILE))
                .setCustomScopes(Arrays.asList("custom"))
                .build();

        assertTrue(
                "https://login.uber.com/oauth/v2/authorize?client_id=CLIENT_ID&response_type=code&scope=custom%20profile"
                        .equals(oAuth2Credentials.getAuthorizationUrl()) ||
                        "https://login.uber.com/oauth/v2/authorize?client_id=CLIENT_ID&response_type=code&scope=profile%20custom"
                                .equals(oAuth2Credentials.getAuthorizationUrl()));
    }

    @Test
    public void getAuthorizationUrl_whenThereAreDuplicateCustomScopes() throws Exception {
        OAuth2Credentials oAuth2Credentials = new OAuth2Credentials.Builder()
                .setClientSecrets("CLIENT_ID", "CLIENT_SECRET")
                .setScopes(Arrays.asList(Scope.PROFILE))
                .setCustomScopes(Arrays.asList("profile"))
                .build();

        assertEquals("https://login.uber.com/oauth/v2/authorize?client_id=CLIENT_ID&response_type=code&scope=profile",
                oAuth2Credentials.getAuthorizationUrl());
    }

    @Test
    public void getAuthorizationUrl_whenThereIsARedirectUri() throws Exception {
        OAuth2Credentials oAuth2Credentials = new OAuth2Credentials.Builder()
                .setClientSecrets("CLIENT_ID", "CLIENT_SECRET")
                .setRedirectUri("https://localhost:8181/OAuth2Callback")
                .setScopes(Arrays.asList(Scope.PROFILE))
                .setCustomScopes(Arrays.asList("profile"))
                .build();

        assertEquals("https://login.uber.com/oauth/v2/authorize?client_id=CLIENT_ID" +
                        "&response_type=code&scope=profile&redirect_uri=https%3A%2F%2Flocalhost%3A8181%2FOAuth2Callback",
                oAuth2Credentials.getAuthorizationUrl());
    }

    @Test
    public void build_whenClientIdIsNull() throws Exception {
        exception.expect(IllegalStateException.class);
        exception.expectMessage(containsString("Client ID and secret"));

        OAuth2Credentials oAuth2Credentials = new OAuth2Credentials.Builder()
                .setClientSecrets(null, "CLIENT_SECRET")
                .build();
    }

    @Test
    public void build_whenClientSecretIsNull() throws Exception {
        exception.expect(IllegalStateException.class);
        exception.expectMessage(containsString("Client ID and secret"));

        OAuth2Credentials oAuth2Credentials = new OAuth2Credentials.Builder()
                .setClientSecrets("CLIENT_ID", null)
                .build();
    }

    @Test
    public void build_whenClientSecretsAreNull() throws Exception {
        exception.expect(IllegalStateException.class);
        exception.expectMessage(containsString("Client ID and secret"));

        OAuth2Credentials oAuth2Credentials = new OAuth2Credentials.Builder()
                .setClientSecrets(null, null)
                .build();
    }

    @Test
    public void build_whenThereAreNoClientSecrets() throws Exception {
        exception.expect(IllegalStateException.class);
        exception.expectMessage(containsString("Client ID and secret"));

        OAuth2Credentials oAuth2Credentials = new OAuth2Credentials.Builder()
                .build();
    }

    @Test
    public void authenticate() throws Exception {
        String authorizationCode = "authorizationCode";

        String expectedRequestContent = "code=authorizationCode&grant_type=authorization_code" +
                "&redirect_uri=http%3A%2F%2Fredirect&scope=profile+request" +
                "&client_id=CLIENT_ID&client_secret=CLIENT_SECRET";

        OAuth2Credentials oAuth2Credentials = new OAuth2Credentials.Builder()
                .setClientSecrets("CLIENT_ID", "CLIENT_SECRET")
                .setRedirectUri("http://redirect")
                .setHttpTransport(mockHttpTransport)
                .setScopes(Arrays.asList(Scope.PROFILE, Scope.REQUEST))
                .build();

        Credential credential = oAuth2Credentials.authenticate(authorizationCode, "userId");

        assertEquals("Request URL did not match.", TOKEN_REQUEST_URL, mockHttpTransport.lastRequestUrl);
        assertEquals("Request content did not match.", expectedRequestContent, mockHttpTransport.lastRequestContent);

        assertEquals("Refresh token does not match.", "refreshToken", credential.getRefreshToken());
        assertTrue("Expected expires_in between 0 and 3600. Was actually: " + credential.getExpiresInSeconds(),
                credential.getExpiresInSeconds() > 0 && credential.getExpiresInSeconds() <= 3600);
        assertEquals("Access token does not match.", "accessToken", credential.getAccessToken());
        assertEquals("Access method (Bearer) does not match",
                BearerToken.authorizationHeaderAccessMethod().getClass(), credential.getMethod().getClass());
    }

    @Test
    public void authenticate_whenThereAreNoScopes() throws Exception {
        String authorizationCode = "authorizationCode";

        String expectedRequestContent = "code=authorizationCode&grant_type=authorization_code" +
                "&redirect_uri=http%3A%2F%2Fredirect" +
                "&client_id=CLIENT_ID&client_secret=CLIENT_SECRET";

        OAuth2Credentials oAuth2Credentials = new OAuth2Credentials.Builder()
                .setClientSecrets("CLIENT_ID", "CLIENT_SECRET")
                .setRedirectUri("http://redirect")
                .setHttpTransport(mockHttpTransport)
                .build();

        Credential credential = oAuth2Credentials.authenticate(authorizationCode, "userId");

        assertEquals("Request URL did not match.", TOKEN_REQUEST_URL, mockHttpTransport.lastRequestUrl);
        assertEquals("Request content did not match.", expectedRequestContent, mockHttpTransport.lastRequestContent);

        assertEquals("Refresh token does not match.", "refreshToken", credential.getRefreshToken());
        assertTrue("Expected expires_in between 0 and 3600. Was actually: " + credential.getExpiresInSeconds(),
                credential.getExpiresInSeconds() > 0 && credential.getExpiresInSeconds() <= 3600);
        assertEquals("Access token does not match.", "accessToken", credential.getAccessToken());
        assertEquals("Access method (Bearer) does not match",
                BearerToken.authorizationHeaderAccessMethod().getClass(), credential.getMethod().getClass());
    }

    @Test
    public void authenticate_whenCantAuthenticate_shouldThrowException() throws Exception {
        exception.expect(AuthException.class);
        exception.expectCause(any(IOException.class));

        String authorizationCode = "authorizationCode";

        mockHttpTransport.setHttpResponseContent("failed");
        mockHttpTransport.setHttpStatusCode(403);

        OAuth2Credentials oAuth2Credentials = new OAuth2Credentials.Builder()
                .setClientSecrets("CLIENT_ID", "CLIENT_SECRET")
                .setRedirectUri("http://redirect")
                .setHttpTransport(mockHttpTransport)
                .build();

        oAuth2Credentials.authenticate(authorizationCode, "userId");
    }

    @Test
    public void loadCredential() throws Exception {
        OAuth2Credentials oAuth2Credentials = new OAuth2Credentials.Builder()
                .setClientSecrets("CLIENT_ID", "CLIENT_SECRET")
                .setRedirectUri("http://redirect")
                .setHttpTransport(mockHttpTransport)
                .setScopes(Arrays.asList(Scope.PROFILE, Scope.REQUEST))
                .build();

        oAuth2Credentials.authenticate("authorizationCode", "userId");

        Credential credential = oAuth2Credentials.loadCredential("userId");

        assertEquals("Refresh token does not match.", "refreshToken", credential.getRefreshToken());
        assertTrue("Expected expires_in between 0 and 3600. Was actually: " + credential.getExpiresInSeconds(),
                credential.getExpiresInSeconds() > 0 && credential.getExpiresInSeconds() <= 3600);
        assertEquals("Access token does not match.", "accessToken", credential.getAccessToken());
        assertEquals("Access method (Bearer) does not match",
                BearerToken.authorizationHeaderAccessMethod().getClass(), credential.getMethod().getClass());
    }

    @Test
    public void clearCredential() throws Exception {
        OAuth2Credentials oAuth2Credentials = new OAuth2Credentials.Builder()
                .setClientSecrets("CLIENT_ID", "CLIENT_SECRET")
                .setRedirectUri("http://redirect")
                .setHttpTransport(mockHttpTransport)
                .setScopes(Arrays.asList(Scope.PROFILE, Scope.REQUEST))
                .build();

        oAuth2Credentials.authenticate("authorizationCode", "userId");

        Credential credential = oAuth2Credentials.loadCredential("userId");

        assertNotNull(credential);

        oAuth2Credentials.clearCredential("userId");

        credential = oAuth2Credentials.loadCredential("userId");
        assertNull(credential);
    }

    @Test
    public void useCustomDataStore() throws Exception {
        Credential credential = new Credential.Builder(BearerToken.authorizationHeaderAccessMethod())
                .setTransport(new MockHttpTransport())
                .setJsonFactory(new MockJsonFactory())
                .setClientAuthentication(Mockito.mock(HttpExecuteInterceptor.class))
                .setTokenServerUrl(new GenericUrl(TOKEN_REQUEST_URL))
                .build();

        credential.setAccessToken("accessToken2");
        credential.setRefreshToken("refreshToken2");
        credential.setExpiresInSeconds(1000L);

        DataStore mockDataStore = Mockito.mock(DataStore.class);
        MockDataStoreFactory mockDataStoreFactory = new MockDataStoreFactory(mockDataStore);
        Mockito.when(mockDataStore.get(eq("userId"))).thenReturn(new StoredCredential(credential));

        OAuth2Credentials oAuth2Credentials = new OAuth2Credentials.Builder()
                .setClientSecrets("CLIENT_ID", "CLIENT_SECRET")
                .setRedirectUri("http://redirect")
                .setHttpTransport(mockHttpTransport)
                .setCredentialDataStoreFactory(mockDataStoreFactory)
                .setScopes(Arrays.asList(Scope.PROFILE, Scope.REQUEST))
                .build();

        Credential storedCredential = oAuth2Credentials.authenticate("authorizationCode", "userId");
        Credential loadedCredential = oAuth2Credentials.loadCredential("userId");

        assertEquals("Refresh token does not match.", "refreshToken", storedCredential.getRefreshToken());
        assertTrue("Expected expires_in between 0 and 3600. Was actually: " + storedCredential.getExpiresInSeconds(),
                storedCredential.getExpiresInSeconds() > 0 && storedCredential.getExpiresInSeconds() <= 3600);
        assertEquals("Access token does not match.", "accessToken", storedCredential.getAccessToken());
        assertEquals("Access method (Bearer) does not match",
                BearerToken.authorizationHeaderAccessMethod().getClass(), storedCredential.getMethod().getClass());

        assertEquals("Refresh token does not match.", "refreshToken2", loadedCredential.getRefreshToken());
        assertTrue("Expected expires_in between 0 and 1000. Was actually: " + loadedCredential.getExpiresInSeconds(),
                loadedCredential.getExpiresInSeconds() > 0 && loadedCredential.getExpiresInSeconds() <= 1000L);
        assertEquals("Access token does not match.", "accessToken2", loadedCredential.getAccessToken());
        assertEquals("Access method (Bearer) does not match",
                BearerToken.authorizationHeaderAccessMethod().getClass(), loadedCredential.getMethod().getClass());
    }

    /**
     * Mock DataStoreFactory that returns a passed in datastore.
     */
    private static class MockDataStoreFactory extends AbstractDataStoreFactory {

        private final DataStore mockDataStore;

        public MockDataStoreFactory(DataStore mockDataStore) throws IOException {
            super();
            this.mockDataStore = mockDataStore;
        }

        @Override
        protected <V extends Serializable> DataStore<V> createDataStore(String id) throws IOException {
            @SuppressWarnings("unchecked")
            DataStore<V> dataStore = (DataStore<V>) mockDataStore;
            return dataStore;
        }
    }

    /**
     * Mock HttpTransport that captures last request URL and Content and relays a valid token.
     */
    private static class MockHttpTransport extends com.google.api.client.testing.http.MockHttpTransport {

        private String lastRequestUrl;
        private String lastRequestContent;

        private int httpStatusCode = 200;
        private String httpResponseContent = "{\n" +
                "  \"access_token\" : \"accessToken\",\n" +
                "  \"token_type\" : \"Bearer\",\n" +
                "  \"expires_in\" : " + 300L + ",\n" +
                "  \"refresh_token\" : \"refreshToken\"\n" +
                "}";

        public void setHttpStatusCode(int httpStatusCode) {
            this.httpStatusCode = httpStatusCode;
        }

        public void setHttpResponseContent(String httpResponseContent) {
            this.httpResponseContent = httpResponseContent;
        }

        @Override
        public LowLevelHttpRequest buildRequest(String method, final String url) throws IOException {
            return new MockLowLevelHttpRequest() {

                @Override
                public String getUrl() {
                    return url;
                }

                @Override
                public LowLevelHttpResponse execute() throws IOException {
                    lastRequestUrl = getUrl();
                    lastRequestContent = getContentAsString();

                    MockLowLevelHttpResponse mock = new MockLowLevelHttpResponse();
                    mock.setStatusCode(httpStatusCode);
                    mock.setContent(httpResponseContent);

                    return mock;
                }
            };
        }
    }
}