/*
 * Copyright 2011-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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 com.amazonaws.client.builder;

import com.amazonaws.AmazonClientException;
import com.amazonaws.AmazonWebServiceClient;
import com.amazonaws.ClientConfiguration;
import com.amazonaws.ClientConfigurationFactory;
import com.amazonaws.PredefinedClientConfigurations;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
import com.amazonaws.client.AwsAsyncClientParams;
import com.amazonaws.client.AwsSyncClientParams;
import com.amazonaws.client.builder.AwsClientBuilder.EndpointConfiguration;
import com.amazonaws.handlers.RequestHandler2;
import com.amazonaws.internal.StaticCredentialsProvider;
import com.amazonaws.metrics.RequestMetricCollector;
import com.amazonaws.regions.AwsRegionProvider;
import com.amazonaws.regions.Regions;

import org.junit.Test;

import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

import utils.builder.StaticExecutorFactory;

import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.sameInstance;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class AwsClientBuilderTest {

    // Note that the tests rely on the socket timeout being set to some arbitrary unique value
    private static final ClientConfiguration DEFAULT_CLIENT_CONFIG = new ClientConfiguration()
            .withSocketTimeout(9001);

    private static class ConcreteRequestHandler extends RequestHandler2 {
    }

    private static class MockClientConfigurationFactory extends ClientConfigurationFactory {
        @Override
        protected ClientConfiguration getDefaultConfig() {
            return DEFAULT_CLIENT_CONFIG;
        }
    }

    private static class ConcreteAsyncBuilder extends
                                              AwsAsyncClientBuilder<ConcreteAsyncBuilder, AmazonConcreteClient> {
        private ConcreteAsyncBuilder() {
            super(new MockClientConfigurationFactory());
        }

        private ConcreteAsyncBuilder(AwsRegionProvider mockRegionProvider) {
            super(new MockClientConfigurationFactory(), mockRegionProvider);
        }

        @Override
        protected AmazonConcreteClient build(AwsAsyncClientParams asyncClientParams) {
            return new AmazonConcreteClient(asyncClientParams);
        }
    }

    private static class ConcreteSyncBuilder extends
                                             AwsSyncClientBuilder<ConcreteSyncBuilder, AmazonConcreteClient> {
        private ConcreteSyncBuilder() {
            super(new MockClientConfigurationFactory());
        }

        @Override
        protected AmazonConcreteClient build(AwsSyncClientParams asyncClientParams) {
            return new AmazonConcreteClient(asyncClientParams);
        }
    }

    /**
     * Dummy client used by both the {@link ConcreteSyncBuilder} and {@link ConcreteAsyncBuilder}.
     * Captures the param object the client was created for for verification in tests.
     */
    private static class AmazonConcreteClient extends AmazonWebServiceClient {

        private AwsAsyncClientParams asyncParams;
        private AwsSyncClientParams syncParams;

        private AmazonConcreteClient(AwsAsyncClientParams asyncParams) {
            super(new ClientConfiguration());
            this.asyncParams = asyncParams;
        }

        private AmazonConcreteClient(AwsSyncClientParams syncParams) {
            super(new ClientConfiguration());
            this.syncParams = syncParams;
        }

        @Override
        public String getServiceNameIntern() {
            return "mockservice";
        }

        @Override
        public String getEndpointPrefix() {
            return "mockprefix";
        }

        public URI getEndpoint() {
            return this.endpoint;
        }

        public AwsAsyncClientParams getAsyncParams() {
            return asyncParams;
        }

        public AwsSyncClientParams getSyncParams() {
            return syncParams;
        }
    }

    /**
     * The sync client is tested less thoroughly then the async client primarily because the async
     * client exercises most of the same code paths so a bug introduced in the sync client builder
     * should be exposed via tests written against the async builder. This test is mainly for
     * additional coverage of the sync builder in case there is a regression specific to sync
     * builders.
     */
    @Test
    public void syncClientBuilder() {
        final List<RequestHandler2> requestHandlers = createRequestHandlerList(
                new ConcreteRequestHandler(), new ConcreteRequestHandler());
        final AWSCredentialsProvider credentials = mock(AWSCredentialsProvider.class);
        final RequestMetricCollector metrics = mock(RequestMetricCollector.class);

        //@formatter:off
        AmazonConcreteClient client = new ConcreteSyncBuilder()
                .withRegion(Regions.EU_CENTRAL_1)
                .withClientConfiguration(new ClientConfiguration().withSocketTimeout(1234))
                .withCredentials(credentials)
                .withMetricsCollector(metrics)
                .withRequestHandlers(requestHandlers.toArray(new RequestHandler2[requestHandlers.size()]))
                .build();
        //@formatter:on

        assertEquals(URI.create("https://mockprefix.eu-central-1.amazonaws.com"),
                     client.getEndpoint());
        assertEquals(1234, client.getSyncParams().getClientConfiguration().getSocketTimeout());
        assertEquals(requestHandlers, client.getSyncParams().getRequestHandlers());
        assertEquals(credentials, client.getSyncParams().getCredentialsProvider());
        assertEquals(metrics, client.getSyncParams().getRequestMetricCollector());
    }


    @Test
    public void credentialsNotExplicitlySet_UsesDefaultCredentialChain() throws Exception {
        AwsAsyncClientParams params = builderWithRegion().build().getAsyncParams();
        assertThat(params.getCredentialsProvider(),
                   instanceOf(DefaultAWSCredentialsProviderChain.class));
    }

    @Test
    public void credentialsExplicitlySet_UsesExplicitCredentials() throws Exception {
        AWSCredentialsProvider provider = new StaticCredentialsProvider(
                new BasicAWSCredentials("akid", "skid"));
        AwsAsyncClientParams params = builderWithRegion().withCredentials(provider).build()
                .getAsyncParams();
        assertEquals(provider, params.getCredentialsProvider());
    }

    @Test
    public void metricCollectorNotExplicitlySet_UsesNullMetricsCollector() throws Exception {
        assertNull(builderWithRegion().build().getAsyncParams().getRequestMetricCollector());
    }

    @Test
    public void metricsCollectorExplicitlySet_UsesExplicitMetricsCollector() throws Exception {
        RequestMetricCollector metricCollector = RequestMetricCollector.NONE;
        AwsAsyncClientParams params = builderWithRegion().withMetricsCollector(metricCollector)
                .build().getAsyncParams();
        assertEquals(metricCollector, params.getRequestMetricCollector());
    }

    @Test
    public void clientConfigurationNotExplicitlySet_UsesServiceDefaultClientConfiguration() {
        AwsAsyncClientParams params = builderWithRegion().build().getAsyncParams();
        ClientConfiguration actualConfig = params.getClientConfiguration();
        assertEquals(DEFAULT_CLIENT_CONFIG.getSocketTimeout(), actualConfig.getSocketTimeout());
    }

    @Test
    public void clientConfigurationExplicitlySet_UsesExplicitConfiguration() {
        ClientConfiguration config = new ClientConfiguration().withSocketTimeout(1000);
        AwsAsyncClientParams params = builderWithRegion().withClientConfiguration(config).build()
                .getAsyncParams();
        assertEquals(config.getSocketTimeout(), params.getClientConfiguration().getSocketTimeout());
    }

    @Test
    public void explicitRegionIsSet_UsesRegionToConstructEndpoint() {
        URI actualUri = new ConcreteAsyncBuilder().withRegion(Regions.US_WEST_2).build()
                .getEndpoint();
        assertEquals(URI.create("https://mockprefix.us-west-2.amazonaws.com"), actualUri);
    }

    /**
     * If no region is explicitly given and no region can be found from the {@link
     * AwsRegionProvider} implementation then the builder should fail to build clients. We mock the
     * provider to yield consistent results for the tests.
     */
    @Test(expected = AmazonClientException.class)
    public void noRegionProvidedExplicitlyOrImplicitly_ThrowsException() {
        AwsRegionProvider mockRegionProvider = mock(AwsRegionProvider.class);
        when(mockRegionProvider.getRegion()).thenReturn(null);
        new ConcreteAsyncBuilder(mockRegionProvider).build();
    }

    /**
     * Customers may not need to explicitly configure a builder with a region if one can be found
     * from the {@link AwsRegionProvider} implementation. We mock the provider to yield consistent
     * results for the tests.
     */
    @Test
    public void regionImplicitlyProvided_UsesRegionToConstructEndpoint() {
        AwsRegionProvider mockRegionProvider = mock(AwsRegionProvider.class);
        when(mockRegionProvider.getRegion()).thenReturn("ap-southeast-2");
        final URI actualUri = new ConcreteAsyncBuilder(mockRegionProvider).build().getEndpoint();
        assertEquals(URI.create("https://mockprefix.ap-southeast-2.amazonaws.com"), actualUri);
    }

    @Test
    public void endpointAndSigningRegionCanBeUsedInPlaceOfSetRegion() {
        AmazonConcreteClient client = new ConcreteSyncBuilder()
                .withEndpointConfiguration(new EndpointConfiguration("https://mockprefix.ap-southeast-2.amazonaws.com", "us-east-1"))
                .build();
        assertEquals("us-east-1", client.getSignerRegionOverride());
        assertEquals(URI.create("https://mockprefix.ap-southeast-2.amazonaws.com"), client.getEndpoint());
    }

    @Test(expected = IllegalStateException.class)
    public void cannotSetBothEndpointConfigurationAndRegionOnBuilder() {
        new ConcreteSyncBuilder()
                .withEndpointConfiguration(new EndpointConfiguration("http://localhost:3030", "us-west-2"))
                .withRegion("us-east-1")
                .build();
    }

    @Test
    public void defaultClientConfigAndNoExplicitExecutor_UsesDefaultExecutorBasedOnMaxConns() {
        ExecutorService executor = builderWithRegion().build().getAsyncParams().getExecutor();
        assertThat(executor, instanceOf(ThreadPoolExecutor.class));
        assertEquals(PredefinedClientConfigurations.defaultConfig().getMaxConnections(),
                     ((ThreadPoolExecutor) executor).getMaximumPoolSize());
    }

    @Test
    public void customMaxConnsAndNoExplicitExecutor_UsesDefaultExecutorBasedOnMaxConns() {
        final int maxConns = 10;
        ExecutorService executor = builderWithRegion()
                .withClientConfiguration(new ClientConfiguration().withMaxConnections(maxConns))
                .build().getAsyncParams().getExecutor();
        assertThat(executor, instanceOf(ThreadPoolExecutor.class));
        assertEquals(maxConns, ((ThreadPoolExecutor) executor).getMaximumPoolSize());
    }

    /**
     * If a custom executor is set then the Max Connections in Client Configuration should be
     * ignored and the executor should be used as is.
     */
    @Test
    public void customMaxConnsAndExplicitExecutor_UsesExplicitExecutor() throws Exception {
        final int clientConfigMaxConns = 10;
        final int customExecutorThreadCount = 15;
        final ExecutorService customExecutor = Executors
                .newFixedThreadPool(customExecutorThreadCount);
        ExecutorService actualExecutor = builderWithRegion().withClientConfiguration(
                new ClientConfiguration().withMaxConnections(clientConfigMaxConns))
                .withExecutorFactory(new StaticExecutorFactory(customExecutor)).build()
                .getAsyncParams().getExecutor();
        assertThat(actualExecutor, instanceOf(ThreadPoolExecutor.class));
        assertEquals(customExecutor, actualExecutor);
        assertEquals(customExecutorThreadCount,
                     ((ThreadPoolExecutor) actualExecutor).getMaximumPoolSize());

    }

    @Test
    public void noRequestHandlersExplicitlySet_UsesEmptyRequestHandlerList() throws Exception {
        List<RequestHandler2> requestHandlers = builderWithRegion().build().getAsyncParams()
                .getRequestHandlers();
        assertThat(requestHandlers, empty());
    }

    @Test
    public void requestHandlersExplicitlySet_UsesClonedListOfExplicitRequestHandlers() throws
                                                                                       Exception {
        List<RequestHandler2> expectedHandlers = createRequestHandlerList(
                new ConcreteRequestHandler(), new ConcreteRequestHandler());
        List<RequestHandler2> actualHandlers = builderWithRegion()
                .withRequestHandlers(expectedHandlers.toArray(new RequestHandler2[0])).build()
                .getAsyncParams().getRequestHandlers();
        assertEquals(expectedHandlers, actualHandlers);
        // List should be copied or cloned
        assertThat(actualHandlers, not(sameInstance(expectedHandlers)));
    }

    @Test
    public void requestHandlersExplicitlySetWithVarArgs_UsesExplicitRequestHandlers() throws
                                                                                      Exception {
        RequestHandler2 handlerOne = new ConcreteRequestHandler();
        RequestHandler2 handlerTwo = new ConcreteRequestHandler();
        RequestHandler2 handlerThree = new ConcreteRequestHandler();
        List<RequestHandler2> actualHandlers = builderWithRegion()
                .withRequestHandlers(handlerOne, handlerTwo, handlerThree).build().getAsyncParams()
                .getRequestHandlers();
        assertEquals(createRequestHandlerList(handlerOne, handlerTwo, handlerThree),
                     actualHandlers);
    }

    /**
     * @return A {@link ConcreteAsyncBuilder} instance with an explicitly configured region.
     */
    private ConcreteAsyncBuilder builderWithRegion() {
        return new ConcreteAsyncBuilder().withRegion(Regions.AP_NORTHEAST_1);
    }

    private List<RequestHandler2> createRequestHandlerList(RequestHandler2... handlers) {
        List<RequestHandler2> requestHandlers = new ArrayList<RequestHandler2>();
        Collections.addAll(requestHandlers, handlers);
        return requestHandlers;
    }
}