/*
 * Copyright (c) 2016 Couchbase, Inc.
 *
 * 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 com.couchbase.client.core.config;

import com.couchbase.client.core.ClusterFacade;
import com.couchbase.client.core.config.loader.Loader;
import com.couchbase.client.core.config.refresher.CarrierRefresher;
import com.couchbase.client.core.config.refresher.HttpRefresher;
import com.couchbase.client.core.config.refresher.Refresher;
import com.couchbase.client.core.env.CoreEnvironment;
import com.couchbase.client.core.env.DefaultCoreEnvironment;
import com.couchbase.client.core.env.NetworkResolution;
import com.couchbase.client.core.lang.Tuple;
import com.couchbase.client.core.lang.Tuple2;
import com.couchbase.client.core.util.Resources;
import org.junit.AfterClass;
import org.junit.Test;
import org.mockito.internal.util.collections.Sets;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import rx.Observable;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.observers.TestSubscriber;
import rx.subjects.AsyncSubject;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * Verifies the correct functionality of the {@link DefaultConfigurationProvider}.
 *
 * @author Michael Nitschinger
 * @since 1.0
 */
public class DefaultConfigurationProviderTest {

    private static final CoreEnvironment environment = DefaultCoreEnvironment.create();

    @AfterClass
    public static void cleanup() {
        environment.shutdown();
    }

    @Test
    @SuppressWarnings("unchecked")
    public void shouldOpenBucket() {
        ClusterFacade cluster = mock(ClusterFacade.class);
        Loader loader = mock(Loader.class);
        BucketConfig bucketConfig = mock(BucketConfig.class);
        when(bucketConfig.name()).thenReturn("bucket");
        when(loader.loadConfig(any(String.class), anyString(), anyString(), anyString()))
            .thenReturn(Observable.just(Tuple.create(LoaderType.Carrier, bucketConfig)));

        final Refresher refresher = mock(Refresher.class);
        when(refresher.configs()).thenReturn(Observable.<ProposedBucketConfigContext>empty());
        when(refresher.registerBucket(anyString(), nullable(String.class), nullable(String.class))).thenReturn(Observable.just(true));

        ConfigurationProvider provider = new DefaultConfigurationProvider(
            cluster,
            environment,
            Arrays.asList(loader),
            new HashMap<LoaderType, Refresher>() {{
                put(LoaderType.Carrier, refresher);
            }}
        );

        provider.seedHosts(Sets.newSet("127.0.0.1"), true);
        Observable<ClusterConfig> configObservable = provider.openBucket("bucket", "password");
        ClusterConfig config = configObservable.toBlocking().first();
        assertTrue(config.hasBucket("bucket"));
        assertFalse(config.hasBucket("other"));
    }

    @Test
    @SuppressWarnings("unchecked")
    public void shouldDelegateLoadingToSecondProviderIfFirstFails() throws Exception {
        ClusterFacade cluster = mock(ClusterFacade.class);
        Loader successLoader = mock(Loader.class);
        Loader errorLoader = mock(Loader.class);
        BucketConfig bucketConfig = mock(BucketConfig.class);
        when(bucketConfig.name()).thenReturn("bucket");
        when(successLoader.loadConfig(any(String.class), anyString(), anyString(), anyString()))
            .thenReturn(Observable.just(Tuple.create(LoaderType.Carrier, bucketConfig)));
        AsyncSubject<BucketConfig> errorSubject = AsyncSubject.create();
        when(errorLoader.loadConfig(any(String.class), anyString(), anyString(), anyString())).thenReturn((Observable) errorSubject);
        errorSubject.onError(new IllegalStateException());

        final Refresher refresher = mock(Refresher.class);
        when(refresher.configs()).thenReturn(Observable.<ProposedBucketConfigContext>empty());
        when(refresher.registerBucket(anyString(), nullable(String.class), nullable(String.class))).thenReturn(Observable.just(true));

        ConfigurationProvider provider = new DefaultConfigurationProvider(
            cluster,
            environment,
            Arrays.asList(errorLoader, successLoader),
            new HashMap<LoaderType, Refresher>() {{
                put(LoaderType.Carrier, refresher);
                put(LoaderType.HTTP, refresher);
            }}
        );

        provider.seedHosts(Sets.newSet("127.0.0.1"), true);
        Observable<ClusterConfig> configObservable = provider.openBucket("bucket", "password");
        ClusterConfig config = configObservable.toBlocking().first();
        assertTrue(config.hasBucket("bucket"));
        assertFalse(config.hasBucket("other"));
    }

    @Test
    public void shouldOpenBucketIfSubsetOfNodesIsFailing() throws Exception {
        ClusterFacade cluster = mock(ClusterFacade.class);

        final Refresher refresher = mock(Refresher.class);
        when(refresher.configs()).thenReturn(Observable.<ProposedBucketConfigContext>empty());
        when(refresher.registerBucket(anyString(), nullable(String.class), nullable(String.class))).thenReturn(Observable.just(true));

        Loader carrierLoader = mock(Loader.class);
        Loader httpLoader = mock(Loader.class);

        final String goodNode = "5.6.7.8";
        String badNode = "1.2.3.4";


        when(carrierLoader.loadConfig(any(String.class), any(String.class), any(String.class), any(String.class)))
            .thenAnswer(new Answer<Observable<Tuple2<LoaderType, BucketConfig>>>() {
                @Override
                public Observable<Tuple2<LoaderType, BucketConfig>> answer(InvocationOnMock in) throws Throwable {
                    String target = (String) in.getArguments()[0];

                    if (target.equals(goodNode)) {
                        final BucketConfig bucketConfig = mock(BucketConfig.class);
                        when(bucketConfig.name()).thenReturn("bucket-carrier-"+target);
                        return Observable.just(Tuple.create(LoaderType.Carrier, bucketConfig));
                    } else {
                        return Observable.error(new Exception("Could not load config for some reason."));
                    }
                }
            });

        when(httpLoader.loadConfig(any(String.class), any(String.class), any(String.class), any(String.class)))
                .thenAnswer(new Answer<Observable<Tuple2<LoaderType, BucketConfig>>>() {
                    @Override
                    public Observable<Tuple2<LoaderType, BucketConfig>> answer(InvocationOnMock in) throws Throwable {
                        String target = (String) in.getArguments()[0];

                        if (target.equals(goodNode)) {
                            final BucketConfig bucketConfig = mock(BucketConfig.class);
                            when(bucketConfig.name()).thenReturn("bucket-http-"+target);
                            return Observable.just(Tuple.create(LoaderType.HTTP, bucketConfig));
                        } else {
                            return Observable.error(new Exception("Could not load config for some reason."));
                        }
                    }
                });

        ConfigurationProvider provider = new DefaultConfigurationProvider(
                cluster,
                environment,
                Arrays.asList(carrierLoader, httpLoader),
                new HashMap<LoaderType, Refresher>() {{
                    put(LoaderType.Carrier, refresher);
                    put(LoaderType.HTTP, refresher);
                }}
        );

        provider.seedHosts(Sets.newSet(badNode, goodNode), true);

        Observable<ClusterConfig> configObservable = provider.openBucket("bucket", "password");
        ClusterConfig config = configObservable.toBlocking().first();

        assertEquals(1, config.bucketConfigs().size());
        assertTrue(config.hasBucket("bucket-carrier-" + goodNode));
    }

    @Test
    public void shouldOpenBucketIfSubsetOfNodesIsFailingAndOnlyHttpAvailable() throws Exception {
        ClusterFacade cluster = mock(ClusterFacade.class);

        final Refresher refresher = mock(Refresher.class);
        when(refresher.configs()).thenReturn(Observable.<ProposedBucketConfigContext>empty());
        when(refresher.registerBucket(anyString(), nullable(String.class), nullable(String.class))).thenReturn(Observable.just(true));

        Loader carrierLoader = mock(Loader.class);
        Loader httpLoader = mock(Loader.class);

        final String goodNode = "5.6.7.8";
        String badNode = "1.2.3.4";

        when(carrierLoader.loadConfig(any(String.class), any(String.class), any(String.class), any(String.class)))
                .thenAnswer(new Answer<Observable<Tuple2<LoaderType, BucketConfig>>>() {
                    @Override
                    public Observable<Tuple2<LoaderType, BucketConfig>> answer(InvocationOnMock in) throws Throwable {
                        return Observable.error(new Exception("Could not load config for some reason."));
                    }
                });

        when(httpLoader.loadConfig(any(String.class), any(String.class), any(String.class), any(String.class)))
                .thenAnswer(new Answer<Observable<Tuple2<LoaderType, BucketConfig>>>() {
                    @Override
                    public Observable<Tuple2<LoaderType, BucketConfig>> answer(InvocationOnMock in) throws Throwable {
                        String target = (String) in.getArguments()[0];

                        if (target.equals(goodNode)) {
                            final BucketConfig bucketConfig = mock(BucketConfig.class);
                            when(bucketConfig.name()).thenReturn("bucket-http-"+target);
                            return Observable.just(Tuple.create(LoaderType.HTTP, bucketConfig));
                        } else {
                            return Observable.error(new Exception("Could not load config for some reason."));
                        }
                    }
                });

        ConfigurationProvider provider = new DefaultConfigurationProvider(
                cluster,
                environment,
                Arrays.asList(carrierLoader, httpLoader),
                new HashMap<LoaderType, Refresher>() {{
                    put(LoaderType.Carrier, refresher);
                    put(LoaderType.HTTP, refresher);
                }}
        );

        provider.seedHosts(Sets.newSet(badNode, goodNode), true);

        Observable<ClusterConfig> configObservable = provider.openBucket("bucket", "password");
        ClusterConfig config = configObservable.toBlocking().first();

        assertEquals(1, config.bucketConfigs().size());
        assertTrue(config.hasBucket("bucket-http-" + goodNode));
    }

    @Test
    public void shouldOpenBucketIfSubsetOfNodesIsNotResponding() throws Exception {
        ClusterFacade cluster = mock(ClusterFacade.class);

        final Refresher refresher = mock(Refresher.class);
        when(refresher.configs()).thenReturn(Observable.<ProposedBucketConfigContext>empty());
        when(refresher.registerBucket(anyString(), nullable(String.class), nullable(String.class))).thenReturn(Observable.just(true));

        Loader carrierLoader = mock(Loader.class);
        Loader httpLoader = mock(Loader.class);

        final String goodNode = "5.6.7.8";
        String badNode = "1.2.3.4";


        when(carrierLoader.loadConfig(any(String.class), any(String.class), any(String.class), any(String.class)))
                .thenAnswer(new Answer<Observable<Tuple2<LoaderType, BucketConfig>>>() {
                    @Override
                    public Observable<Tuple2<LoaderType, BucketConfig>> answer(InvocationOnMock in) throws Throwable {
                        String target = (String) in.getArguments()[0];

                        if (target.equals(goodNode)) {
                            final BucketConfig bucketConfig = mock(BucketConfig.class);
                            when(bucketConfig.name()).thenReturn("bucket-carrier-"+target);
                            return Observable.just(Tuple.create(LoaderType.Carrier, bucketConfig));
                        } else {
                            return Observable.timer(1, TimeUnit.MINUTES).map(new Func1<Long, Tuple2<LoaderType, BucketConfig>>() {
                                @Override
                                public Tuple2<LoaderType, BucketConfig> call(Long aLong) {
                                    throw new RuntimeException("Could not load config for some reason.");
                                }
                            });
                        }
                    }
                });

        when(httpLoader.loadConfig(any(String.class), any(String.class), any(String.class), any(String.class)))
                .thenAnswer(new Answer<Observable<Tuple2<LoaderType, BucketConfig>>>() {
                    @Override
                    public Observable<Tuple2<LoaderType, BucketConfig>> answer(InvocationOnMock in) throws Throwable {
                        String target = (String) in.getArguments()[0];

                        if (target.equals(goodNode)) {
                            final BucketConfig bucketConfig = mock(BucketConfig.class);
                            when(bucketConfig.name()).thenReturn("bucket-http-"+target);
                            return Observable.just(Tuple.create(LoaderType.HTTP, bucketConfig));
                        } else {
                            return Observable.timer(1, TimeUnit.MINUTES).map(new Func1<Long, Tuple2<LoaderType, BucketConfig>>() {
                                @Override
                                public Tuple2<LoaderType, BucketConfig> call(Long aLong) {
                                    throw new RuntimeException("Could not load config for some reason.");
                                }
                            });
                        }
                    }
                });

        ConfigurationProvider provider = new DefaultConfigurationProvider(
                cluster,
                environment,
                Arrays.asList(carrierLoader, httpLoader),
                new HashMap<LoaderType, Refresher>() {{
                    put(LoaderType.Carrier, refresher);
                    put(LoaderType.HTTP, refresher);
                }}
        );

        provider.seedHosts(Sets.newSet(badNode, goodNode), true);

        Observable<ClusterConfig> configObservable = provider.openBucket("bucket", "password");
        ClusterConfig config = configObservable.toBlocking().first();

        assertEquals(1, config.bucketConfigs().size());
        assertTrue(config.hasBucket("bucket-carrier-" + goodNode));
    }

    @Test
    public void shouldEmitNewClusterConfig() throws Exception {
        final ClusterFacade cluster = mock(ClusterFacade.class);
        Loader loader = mock(Loader.class);
        BucketConfig bucketConfig = mock(BucketConfig.class);
        when(bucketConfig.name()).thenReturn("bucket");
        when(loader.loadConfig(any(String.class), anyString(), anyString(), anyString()))
            .thenReturn(Observable.just(Tuple.create(LoaderType.Carrier, bucketConfig)));


        final Refresher refresher = mock(Refresher.class);
        when(refresher.configs()).thenReturn(Observable.<ProposedBucketConfigContext>empty());
        when(refresher.registerBucket(anyString(), nullable(String.class), nullable(String.class))).thenReturn(Observable.just(true));

        ConfigurationProvider provider = new DefaultConfigurationProvider(
            cluster,
            environment,
            Arrays.asList(loader),
            new HashMap<LoaderType, Refresher>() {{
                put(LoaderType.Carrier, refresher);
            }}
        );
        final CountDownLatch latch = new CountDownLatch(1);
        final AtomicReference<ClusterConfig> configReference = new AtomicReference<ClusterConfig>();
        provider.configs().subscribe(new Action1<ClusterConfig>() {
            @Override
            public void call(ClusterConfig clusterConfig) {
                configReference.set(clusterConfig);
                latch.countDown();
            }
        });

        provider.seedHosts(Sets.newSet("127.0.0.1"), true);
        Observable<ClusterConfig> configObservable = provider.openBucket("bucket", "password");
        ClusterConfig config = configObservable.toBlocking().first();
        assertTrue(config.hasBucket("bucket"));
        assertFalse(config.hasBucket("other"));
        assertTrue(latch.await(2, TimeUnit.SECONDS));
        assertEquals(configReference.get(), config);
    }

    @SuppressWarnings("unchecked")
    @Test
    public void shouldFailOpeningBucketIfNoConfigLoaded() throws Exception {
        ClusterFacade cluster = mock(ClusterFacade.class);
        Loader errorLoader = mock(Loader.class);
        AsyncSubject<Tuple2<LoaderType, BucketConfig>> errorSubject = AsyncSubject.create();
        when(errorLoader.loadConfig(any(String.class), anyString(), anyString(), anyString())).thenReturn(errorSubject);
        errorSubject.onError(new IllegalStateException());

        final Refresher refresher = mock(Refresher.class);
        when(refresher.configs()).thenReturn(Observable.<ProposedBucketConfigContext>empty());
        when(refresher.registerBucket(anyString(), anyString(), anyString())).thenReturn(Observable.just(true));

        ConfigurationProvider provider = new DefaultConfigurationProvider(
            cluster,
            environment,
            Arrays.asList(errorLoader),
            new HashMap<LoaderType, Refresher>() {{
                put(LoaderType.Carrier, refresher);
            }}
        );

        provider.seedHosts(Sets.newSet("127.0.0.1"), true);
        Observable<ClusterConfig> configObservable = provider.openBucket("bucket", "password");
        try {
            configObservable.toBlocking().single();
            assertTrue(false);
        } catch(ConfigurationException ex) {
            assertEquals("Could not open bucket.", ex.getMessage());
        } catch(Exception ex) {
            assertTrue(false);
        }
    }

    @Test
    public void shouldAcceptProposedConfigIfNoneExists() {
        DefaultConfigurationProvider provider = new DefaultConfigurationProvider(
            mock(ClusterFacade.class),
            environment
        );

        Set<String> seeds = new HashSet<String>(Arrays.asList("127.0.0.1"));
        provider.seedHosts(seeds, true);

        assertTrue(provider.config().bucketConfigs().isEmpty());

        String raw = Resources.read("config_with_rev_placeholder.json", getClass());
        raw = raw.replace("$REV", "1");
        provider.proposeBucketConfig(new ProposedBucketConfigContext("default", raw, null));

        assertFalse(provider.config().bucketConfigs().isEmpty());
        assertEquals(1, provider.config().bucketConfig("default").rev());
    }

    @Test
    public void shouldAcceptProposedConfigIfNewer() {
        DefaultConfigurationProvider provider = new DefaultConfigurationProvider(
            mock(ClusterFacade.class),
            environment
        );

        Set<String> seeds = new HashSet<String>(Arrays.asList("127.0.0.1"));
        provider.seedHosts(seeds, true);
        String raw = Resources.read("config_with_rev_placeholder.json", getClass());
        String v1 = raw.replace("$REV", "1");
        provider.proposeBucketConfig(new ProposedBucketConfigContext("default", v1, null));

        assertFalse(provider.config().bucketConfigs().isEmpty());
        assertEquals(1, provider.config().bucketConfig("default").rev());

        String v2 = raw.replace("$REV", "2");
        provider.proposeBucketConfig(new ProposedBucketConfigContext("default", v2, null));

        assertFalse(provider.config().bucketConfigs().isEmpty());
        assertEquals(2, provider.config().bucketConfig("default").rev());
    }

    @Test
    public void shouldIgnoreConfigIfInvalid() {
        DefaultConfigurationProvider provider = new DefaultConfigurationProvider(
            mock(ClusterFacade.class),
            environment
        );

        Set<String> seeds = new HashSet<String>(Arrays.asList("127.0.0.1"));
        provider.seedHosts(seeds, true);

        assertTrue(provider.config().bucketConfigs().isEmpty());


        String raw = Resources.read("config_with_rev_placeholder.json", getClass());
        provider.proposeBucketConfig(new ProposedBucketConfigContext("default", raw, null));
        assertTrue(provider.config().bucketConfigs().isEmpty());

        String v1 = raw.replace("$REV", "1");
        provider.proposeBucketConfig(new ProposedBucketConfigContext("default", v1, null));
        assertFalse(provider.config().bucketConfigs().isEmpty());

        provider.proposeBucketConfig(new ProposedBucketConfigContext("default", raw, null));
        assertFalse(provider.config().bucketConfigs().isEmpty());

        String v2 = raw.replace("$REV", "2");
        provider.proposeBucketConfig(new ProposedBucketConfigContext("default", v2, null));
        assertFalse(provider.config().bucketConfigs().isEmpty());

        assertEquals(2, provider.config().bucketConfig("default").rev());
    }

    @Test
    public void shouldIgnoreConfigIfOlder() {
        DefaultConfigurationProvider provider = new DefaultConfigurationProvider(
            mock(ClusterFacade.class),
            environment
        );

        Set<String> seeds = new HashSet<String>(Arrays.asList("127.0.0.1"));
        provider.seedHosts(seeds, true);

        String raw = Resources.read("config_with_rev_placeholder.json", getClass());
        String v2 = raw.replace("$REV", "2");
        provider.proposeBucketConfig(new ProposedBucketConfigContext("default", v2, null));

        assertFalse(provider.config().bucketConfigs().isEmpty());
        assertEquals(2, provider.config().bucketConfig("default").rev());

        String v1 = raw.replace("$REV", "1");
        provider.proposeBucketConfig(new ProposedBucketConfigContext("default", v1, null));

        assertFalse(provider.config().bucketConfigs().isEmpty());
        assertEquals(2, provider.config().bucketConfig("default").rev());
    }

    @Test
    public void shouldIgnoreConfigIfSameRev() throws Exception {
        DefaultConfigurationProvider provider = new DefaultConfigurationProvider(
            mock(ClusterFacade.class),
            environment
        );

        Set<String> seeds = new HashSet<String>(Arrays.asList("127.0.0.1"));
        provider.seedHosts(seeds, true);

        TestSubscriber<ClusterConfig> subscriber = new TestSubscriber<ClusterConfig>();
        provider.configs().subscribe(subscriber);

        String raw = Resources.read("config_with_rev_placeholder.json", getClass());
        String v1 = raw.replace("$REV", "1");
        provider.proposeBucketConfig(new ProposedBucketConfigContext("default", v1, null));

        assertFalse(provider.config().bucketConfigs().isEmpty());
        assertEquals(1, provider.config().bucketConfig("default").rev());

        String v2 = raw.replace("$REV", "1");
        provider.proposeBucketConfig(new ProposedBucketConfigContext("default", v2, null));

        assertFalse(provider.config().bucketConfigs().isEmpty());
        assertEquals(1, provider.config().bucketConfig("default").rev());

        String v3 = raw.replace("$REV", "2");
        provider.proposeBucketConfig(new ProposedBucketConfigContext("default", v3, null));

        assertFalse(provider.config().bucketConfigs().isEmpty());
        assertEquals(2, provider.config().bucketConfig("default").rev());

        Thread.sleep(100);

        assertEquals(2, subscriber.getOnNextEvents().size());
    }

    @Test
    public void shouldForcePickServerDefault() {
        ClusterFacade cluster = mock(ClusterFacade.class);
        final Loader loader = mock(Loader.class);
        final Refresher refresher = mock(Refresher.class);
        final CoreEnvironment env = mock(CoreEnvironment.class);
        when(env.networkResolution()).thenReturn(NetworkResolution.DEFAULT);
        ConfigurationProvider provider = new DefaultConfigurationProvider(
            cluster,
            env,
            Arrays.asList(loader),
            new HashMap<LoaderType, Refresher>() {{
                put(LoaderType.Carrier, refresher);
            }}
        );

        // No configs available
        assertTrue(provider.config().bucketConfigs().isEmpty());

        String raw = Resources.read("config_with_external.json", getClass());
        provider.proposeBucketConfig(new ProposedBucketConfigContext("bucket", raw, null));

        assertNull(provider.config().bucketConfig("default").useAlternateNetwork());
    }

    @Test
    public void shouldForcePickExternal() {
        ClusterFacade cluster = mock(ClusterFacade.class);
        final Loader loader = mock(Loader.class);
        final Refresher refresher = mock(Refresher.class);
        final CoreEnvironment env = mock(CoreEnvironment.class);
        when(env.networkResolution()).thenReturn(NetworkResolution.EXTERNAL);
        ConfigurationProvider provider = new DefaultConfigurationProvider(
            cluster,
            env,
            Arrays.asList(loader),
            new HashMap<LoaderType, Refresher>() {{
                put(LoaderType.Carrier, refresher);
            }}
        );

        // No configs available
        assertTrue(provider.config().bucketConfigs().isEmpty());

        String raw = Resources.read("config_with_external.json", getClass());
        provider.proposeBucketConfig(new ProposedBucketConfigContext("bucket", raw, null));

        assertNotNull(provider.config().bucketConfig("default").useAlternateNetwork());
    }

    @Test
    public void shouldAutoPickExternal() {
        ClusterFacade cluster = mock(ClusterFacade.class);
        final Loader loader = mock(Loader.class);
        final Refresher refresher = mock(Refresher.class);
        final CoreEnvironment env = mock(CoreEnvironment.class);
        when(env.networkResolution()).thenReturn(NetworkResolution.AUTO);
        ConfigurationProvider provider = new DefaultConfigurationProvider(
            cluster,
            env,
            Arrays.asList(loader),
            new HashMap<LoaderType, Refresher>() {{
                put(LoaderType.Carrier, refresher);
            }}
        );

        Set<String> seeds = new HashSet<String>();
        seeds.add("192.168.132.234");
        provider.seedHosts(seeds, true);

        // No configs available
        assertTrue(provider.config().bucketConfigs().isEmpty());

        String raw = Resources.read("config_with_external.json", getClass());
        provider.proposeBucketConfig(new ProposedBucketConfigContext("bucket", raw, null));

        assertNotNull(provider.config().bucketConfig("default").useAlternateNetwork());
    }

    @Test
    public void shouldAutoPickServerDefault() {
        ClusterFacade cluster = mock(ClusterFacade.class);
        final Loader loader = mock(Loader.class);
        final Refresher refresher = mock(Refresher.class);
        final CoreEnvironment env = mock(CoreEnvironment.class);
        when(env.networkResolution()).thenReturn(NetworkResolution.AUTO);
        ConfigurationProvider provider = new DefaultConfigurationProvider(
            cluster,
            env,
            Arrays.asList(loader),
            new HashMap<LoaderType, Refresher>() {{
                put(LoaderType.Carrier, refresher);
            }}
        );

        Set<String> seeds = new HashSet<String>(Arrays.asList("172.17.0.3"));
        provider.seedHosts(seeds, true);

        // No configs available
        assertTrue(provider.config().bucketConfigs().isEmpty());

        String raw = Resources.read("config_with_external.json", getClass());
        provider.proposeBucketConfig(new ProposedBucketConfigContext("bucket", raw, null));

        assertNull(provider.config().bucketConfig("default").useAlternateNetwork());
    }

    @Test
    public void shouldSelectCarrierRefresherIfCarrierLoader() {
        Refresher carrierRefresher = mock(CarrierRefresher.class);
        when(carrierRefresher.registerBucket(anyString(), anyString(), anyString())).thenReturn(Observable.just(true));
        Refresher httpRefresher = mock(HttpRefresher.class);
        when(httpRefresher.registerBucket(anyString(), anyString(), anyString())).thenReturn(Observable.just(true));
        Map<LoaderType, Refresher> refreshers = new HashMap<>();
        refreshers.put(LoaderType.Carrier, carrierRefresher);
        refreshers.put(LoaderType.HTTP, httpRefresher);

        LoaderType loaderType = LoaderType.Carrier;

        BucketConfig config = mock(CouchbaseBucketConfig.class);
        when(config.name()).thenReturn("default");
        when(config.username()).thenReturn("user");
        when(config.password()).thenReturn("pass");

        DefaultConfigurationProvider.registerBucketForRefresh(refreshers, loaderType, config);

        verify(carrierRefresher, times(1))
            .registerBucket("default", "user", "pass");

        verify(httpRefresher, never())
            .registerBucket("default", "user", "pass");
    }

    @Test
    public void shouldSelectCarrierRefresherIfHttpLoader() {
        Refresher carrierRefresher = mock(CarrierRefresher.class);
        when(carrierRefresher.registerBucket(anyString(), anyString(), anyString())).thenReturn(Observable.just(true));
        Refresher httpRefresher = mock(HttpRefresher.class);
        when(httpRefresher.registerBucket(anyString(), anyString(), anyString())).thenReturn(Observable.just(true));
        Map<LoaderType, Refresher> refreshers = new HashMap<>();
        refreshers.put(LoaderType.Carrier, carrierRefresher);
        refreshers.put(LoaderType.HTTP, httpRefresher);

        LoaderType loaderType = LoaderType.HTTP;

        BucketConfig config = mock(CouchbaseBucketConfig.class);
        when(config.name()).thenReturn("default");
        when(config.username()).thenReturn("user");
        when(config.password()).thenReturn("pass");
        when(config.capabilities()).thenReturn(Collections.singletonList(BucketCapabilities.NODES_EXT));

        DefaultConfigurationProvider.registerBucketForRefresh(refreshers, loaderType, config);

        verify(carrierRefresher, times(1))
            .registerBucket("default", "user", "pass");

        verify(httpRefresher, never())
            .registerBucket("default", "user", "pass");
    }

    @Test
    public void shouldSelectHttpRefresherForMemcacheBucket() {
        Refresher carrierRefresher = mock(CarrierRefresher.class);
        when(carrierRefresher.registerBucket(anyString(), anyString(), anyString())).thenReturn(Observable.just(true));
        Refresher httpRefresher = mock(HttpRefresher.class);
        when(httpRefresher.registerBucket(anyString(), anyString(), anyString())).thenReturn(Observable.just(true));
        Map<LoaderType, Refresher> refreshers = new HashMap<>();
        refreshers.put(LoaderType.Carrier, carrierRefresher);
        refreshers.put(LoaderType.HTTP, httpRefresher);

        LoaderType loaderType = LoaderType.HTTP;

        BucketConfig config = mock(MemcachedBucketConfig.class);
        when(config.name()).thenReturn("default");
        when(config.username()).thenReturn("user");
        when(config.password()).thenReturn("pass");
        when(config.capabilities()).thenReturn(Collections.singletonList(BucketCapabilities.NODES_EXT));

        DefaultConfigurationProvider.registerBucketForRefresh(refreshers, loaderType, config);

        verify(carrierRefresher, never())
            .registerBucket("default", "user", "pass");

        verify(httpRefresher, times(1))
            .registerBucket("default", "user", "pass");
    }

    @Test
    public void shouldSelectHttpRefresherForOldCluster() {
        Refresher carrierRefresher = mock(CarrierRefresher.class);
        when(carrierRefresher.registerBucket(anyString(), anyString(), anyString())).thenReturn(Observable.just(true));
        Refresher httpRefresher = mock(HttpRefresher.class);
        when(httpRefresher.registerBucket(anyString(), anyString(), anyString())).thenReturn(Observable.just(true));
        Map<LoaderType, Refresher> refreshers = new HashMap<>();
        refreshers.put(LoaderType.Carrier, carrierRefresher);
        refreshers.put(LoaderType.HTTP, httpRefresher);

        LoaderType loaderType = LoaderType.HTTP;

        BucketConfig config = mock(CouchbaseBucketConfig.class);
        when(config.name()).thenReturn("default");
        when(config.username()).thenReturn("user");
        when(config.password()).thenReturn("pass");
        when(config.capabilities()).thenReturn(Collections.<BucketCapabilities>emptyList());

        DefaultConfigurationProvider.registerBucketForRefresh(refreshers, loaderType, config);

        verify(carrierRefresher, never())
            .registerBucket("default", "user", "pass");

        verify(httpRefresher, times(1))
            .registerBucket("default", "user", "pass");
    }

}