// Copyright 2018 Google LLC
//
// 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.google.firebase.remoteconfig;

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.firebase.remoteconfig.AbtExperimentHelper.createAbtExperiment;
import static com.google.firebase.remoteconfig.FirebaseRemoteConfig.DEFAULT_VALUE_FOR_BOOLEAN;
import static com.google.firebase.remoteconfig.FirebaseRemoteConfig.DEFAULT_VALUE_FOR_BYTE_ARRAY;
import static com.google.firebase.remoteconfig.FirebaseRemoteConfig.DEFAULT_VALUE_FOR_DOUBLE;
import static com.google.firebase.remoteconfig.FirebaseRemoteConfig.DEFAULT_VALUE_FOR_LONG;
import static com.google.firebase.remoteconfig.FirebaseRemoteConfig.DEFAULT_VALUE_FOR_STRING;
import static com.google.firebase.remoteconfig.FirebaseRemoteConfig.LAST_FETCH_STATUS_THROTTLED;
import static com.google.firebase.remoteconfig.FirebaseRemoteConfig.toExperimentInfoMaps;
import static com.google.firebase.remoteconfig.internal.ConfigGetParameterHandler.FRC_BYTE_ARRAY_ENCODING;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.content.Context;
import android.content.res.Resources;
import com.google.android.gms.shadows.common.internal.ShadowPreconditions;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.TaskCompletionSource;
import com.google.android.gms.tasks.Tasks;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.abt.AbtException;
import com.google.firebase.abt.FirebaseABTesting;
import com.google.firebase.installations.FirebaseInstallationsApi;
import com.google.firebase.installations.InstallationTokenResult;
import com.google.firebase.remoteconfig.internal.ConfigCacheClient;
import com.google.firebase.remoteconfig.internal.ConfigContainer;
import com.google.firebase.remoteconfig.internal.ConfigFetchHandler;
import com.google.firebase.remoteconfig.internal.ConfigFetchHandler.FetchResponse;
import com.google.firebase.remoteconfig.internal.ConfigGetParameterHandler;
import com.google.firebase.remoteconfig.internal.ConfigMetadataClient;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;
import org.json.JSONArray;
import org.json.JSONException;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.skyscreamer.jsonassert.JSONAssert;

/**
 * Unit tests for the Firebase Remote Config API.
 *
 * @author Miraziz Yusupov
 */
@RunWith(RobolectricTestRunner.class)
@Config(
    manifest = Config.NONE,
    shadows = {ShadowPreconditions.class})
public final class FirebaseRemoteConfigTest {
  private static final String APP_ID = "1:14368190084:android:09cb977358c6f241";
  private static final String API_KEY = "AIzaSyabcdefghijklmnopqrstuvwxyz1234567";
  private static final String PROJECT_ID = "fake-frc-test-id";

  private static final String FIREPERF_NAMESPACE = "fireperf";

  private static final String STRING_KEY = "string_key";
  private static final String BOOLEAN_KEY = "boolean_key";
  private static final String BYTE_ARRAY_KEY = "byte_array_key";
  private static final String DOUBLE_KEY = "double_key";
  private static final String LONG_KEY = "long_key";

  private static final String ETAG = "ETag";

  private static final String INSTALLATION_ID = "'fL71_VyL3uo9jNMWu1L60S";
  private static final String INSTALLATION_TOKEN =
      "eyJhbGciOiJF.eyJmaWQiOiJmaXMt.AB2LPV8wRQIhAPs4NvEgA3uhubH";
  private static final InstallationTokenResult INSTALLATION_TOKEN_RESULT =
      InstallationTokenResult.builder()
          .setToken(INSTALLATION_TOKEN)
          .setTokenCreationTimestamp(1)
          .setTokenExpirationTimestamp(1)
          .build();

  // We use a HashMap so that Mocking is easier.
  private static final HashMap<String, Object> DEFAULTS_MAP = new HashMap<>();
  private static final HashMap<String, String> DEFAULTS_STRING_MAP = new HashMap<>();

  @Mock private ConfigCacheClient mockFetchedCache;
  @Mock private ConfigCacheClient mockActivatedCache;
  @Mock private ConfigCacheClient mockDefaultsCache;
  @Mock private ConfigFetchHandler mockFetchHandler;
  @Mock private ConfigGetParameterHandler mockGetHandler;
  @Mock private ConfigMetadataClient metadataClient;

  @Mock private ConfigCacheClient mockFireperfFetchedCache;
  @Mock private ConfigCacheClient mockFireperfActivatedCache;
  @Mock private ConfigCacheClient mockFireperfDefaultsCache;
  @Mock private ConfigFetchHandler mockFireperfFetchHandler;
  @Mock private ConfigGetParameterHandler mockFireperfGetHandler;

  @Mock private FirebaseRemoteConfigInfo mockFrcInfo;

  @Mock private FirebaseABTesting mockFirebaseAbt;
  @Mock private FirebaseInstallationsApi mockFirebaseInstallations;

  private FirebaseRemoteConfig frc;
  private FirebaseRemoteConfig fireperfFrc;
  private ConfigContainer firstFetchedContainer;
  private ConfigContainer secondFetchedContainer;

  private FetchResponse firstFetchedContainerResponse;

  @Before
  public void setUp() throws Exception {
    DEFAULTS_MAP.put("first_default_key", "first_default_value");
    DEFAULTS_MAP.put("second_default_key", "second_default_value");
    DEFAULTS_MAP.put("third_default_key", "third_default_value");
    DEFAULTS_MAP.put("byte_array_default_key", "fourth_default_value".getBytes());

    DEFAULTS_STRING_MAP.put("first_default_key", "first_default_value");
    DEFAULTS_STRING_MAP.put("second_default_key", "second_default_value");
    DEFAULTS_STRING_MAP.put("third_default_key", "third_default_value");
    DEFAULTS_STRING_MAP.put("byte_array_default_key", "fourth_default_value");

    MockitoAnnotations.initMocks(this);

    Executor directExecutor = MoreExecutors.directExecutor();
    Context context = RuntimeEnvironment.application;
    FirebaseApp firebaseApp = initializeFirebaseApp(context);

    // Catch all to avoid NPEs (the getters should never return null).
    when(mockFetchedCache.get()).thenReturn(Tasks.forResult(null));
    when(mockActivatedCache.get()).thenReturn(Tasks.forResult(null));
    when(mockFireperfFetchedCache.get()).thenReturn(Tasks.forResult(null));
    when(mockFireperfActivatedCache.get()).thenReturn(Tasks.forResult(null));

    frc =
        new FirebaseRemoteConfig(
            context,
            firebaseApp,
            mockFirebaseInstallations,
            mockFirebaseAbt,
            directExecutor,
            mockFetchedCache,
            mockActivatedCache,
            mockDefaultsCache,
            mockFetchHandler,
            mockGetHandler,
            metadataClient);

    // Set up an FRC instance for the Fireperf namespace that uses mocked clients.
    fireperfFrc =
        FirebaseApp.getInstance()
            .get(RemoteConfigComponent.class)
            .get(
                firebaseApp,
                FIREPERF_NAMESPACE,
                mockFirebaseInstallations,
                /*firebaseAbt=*/ null,
                directExecutor,
                mockFireperfFetchedCache,
                mockFireperfActivatedCache,
                mockFireperfDefaultsCache,
                mockFireperfFetchHandler,
                mockFireperfGetHandler,
                RemoteConfigComponent.getMetadataClient(context, APP_ID, FIREPERF_NAMESPACE));

    firstFetchedContainer =
        ConfigContainer.newBuilder()
            .replaceConfigsWith(ImmutableMap.of("long_param", "1L", "string_param", "string_value"))
            .withFetchTime(new Date(1000L))
            .build();

    secondFetchedContainer =
        ConfigContainer.newBuilder()
            .replaceConfigsWith(
                ImmutableMap.of("string_param", "string_value", "double_param", "0.1"))
            .withFetchTime(new Date(5000L))
            .build();

    firstFetchedContainerResponse =
        FetchResponse.forBackendUpdatesFetched(firstFetchedContainer, ETAG);
  }

  @Test
  public void ensureInitialized_notInitialized_isNotComplete() {
    loadCacheWithConfig(mockFetchedCache, /*container=*/ null);
    loadCacheWithConfig(mockDefaultsCache, /*container=*/ null);
    loadActivatedCacheWithIncompleteTask();
    loadInstanceIdAndToken();

    Task<FirebaseRemoteConfigInfo> initStatus = frc.ensureInitialized();

    assertWithMessage("FRC is initialized even though activated configs have not loaded!")
        .that(initStatus.isComplete())
        .isFalse();
  }

  @Test
  public void ensureInitialized_initialized_returnsCorrectFrcInfo() {
    loadCacheWithConfig(mockFetchedCache, /*container=*/ null);
    loadCacheWithConfig(mockDefaultsCache, /*container=*/ null);
    loadCacheWithConfig(mockActivatedCache, /*container=*/ null);
    loadInstanceIdAndToken();

    Task<FirebaseRemoteConfigInfo> initStatus = frc.ensureInitialized();

    assertWithMessage("FRC is not initialized even though everything is loaded!")
        .that(initStatus.isComplete())
        .isTrue();
  }

  @Test
  public void fetchAndActivate_hasNetworkError_taskReturnsException() {
    when(mockFetchHandler.fetch())
        .thenReturn(Tasks.forException(new IOException("Network call failed.")));

    Task<Boolean> task = frc.fetchAndActivate();

    assertThat(task.isComplete()).isTrue();
    assertWithMessage("Fetch succeeded even though there's a network error!")
        .that(task.getException())
        .isNotNull();
  }

  @Test
  public void fetchAndActivate_getFetchedFailed_returnsFalse() {
    loadFetchHandlerWithResponse();
    loadCacheWithIoException(mockFetchedCache);
    loadCacheWithConfig(mockActivatedCache, null);

    Task<Boolean> task = frc.fetchAndActivate();

    assertWithMessage("fetchAndActivate() succeeded with no fetched values!")
        .that(getTaskResult(task))
        .isFalse();

    verify(mockActivatedCache, never()).put(any());
    verify(mockFetchedCache, never()).clear();
  }

  @Test
  public void fetchAndActivate_noFetchedConfigs_returnsFalse() {
    loadFetchHandlerWithResponse();
    loadCacheWithConfig(mockFetchedCache, null);
    loadCacheWithConfig(mockActivatedCache, null);

    Task<Boolean> task = frc.fetchAndActivate();

    assertWithMessage("fetchAndActivate() succeeded with no fetched values!")
        .that(getTaskResult(task))
        .isFalse();

    verify(mockActivatedCache, never()).put(any());
    verify(mockFetchedCache, never()).clear();
  }

  @Test
  public void fetchAndActivate_staleFetchedConfigs_returnsFalse() {
    loadFetchHandlerWithResponse();
    loadCacheWithConfig(mockFetchedCache, firstFetchedContainer);
    loadCacheWithConfig(mockActivatedCache, firstFetchedContainer);

    Task<Boolean> task = frc.fetchAndActivate();

    assertWithMessage("fetchAndActivate() succeeded with stale values!")
        .that(getTaskResult(task))
        .isFalse();

    verify(mockActivatedCache, never()).put(any());
    verify(mockFetchedCache, never()).clear();
  }

  @Test
  public void fetchAndActivate_noActivatedConfigs_activatesAndClearsFetched() {
    loadFetchHandlerWithResponse();
    loadCacheWithConfig(mockFetchedCache, firstFetchedContainer);
    loadCacheWithConfig(mockActivatedCache, null);

    cachePutReturnsConfig(mockActivatedCache, firstFetchedContainer);

    Task<Boolean> task = frc.fetchAndActivate();

    assertWithMessage("fetchAndActivate() failed with no activated values!")
        .that(getTaskResult(task))
        .isTrue();

    verify(mockActivatedCache).put(firstFetchedContainer);
    verify(mockFetchedCache).clear();
  }

  @Test
  public void fetchAndActivate_getActivatedFailed_activatesAndClearsFetched() {
    loadFetchHandlerWithResponse();
    loadCacheWithConfig(mockFetchedCache, firstFetchedContainer);
    loadCacheWithIoException(mockActivatedCache);

    cachePutReturnsConfig(mockActivatedCache, firstFetchedContainer);

    Task<Boolean> task = frc.fetchAndActivate();

    assertWithMessage("fetchAndActivate() failed with no activated values!")
        .that(getTaskResult(task))
        .isTrue();

    verify(mockActivatedCache).put(firstFetchedContainer);
    verify(mockFetchedCache).clear();
  }

  @Test
  public void fetchAndActivate_freshFetchedConfigs_activatesAndClearsFetched() {
    loadFetchHandlerWithResponse();
    loadCacheWithConfig(mockFetchedCache, secondFetchedContainer);
    loadCacheWithConfig(mockActivatedCache, firstFetchedContainer);

    cachePutReturnsConfig(mockActivatedCache, secondFetchedContainer);

    Task<Boolean> task = frc.fetchAndActivate();

    assertWithMessage("fetchAndActivate() failed!").that(getTaskResult(task)).isTrue();

    verify(mockActivatedCache).put(secondFetchedContainer);
    verify(mockFetchedCache).clear();
  }

  @Test
  public void fetchAndActivate_fileWriteFails_doesNotClearFetchedAndReturnsFalse() {
    loadFetchHandlerWithResponse();
    loadCacheWithConfig(mockFetchedCache, secondFetchedContainer);
    loadCacheWithConfig(mockActivatedCache, firstFetchedContainer);

    when(mockActivatedCache.put(secondFetchedContainer))
        .thenReturn(Tasks.forException(new IOException("Should have handled disk error.")));

    Task<Boolean> task = frc.fetchAndActivate();

    assertWithMessage("fetchAndActivate() succeeded even though file write failed!")
        .that(getTaskResult(task))
        .isFalse();

    verify(mockActivatedCache).put(secondFetchedContainer);
    verify(mockFetchedCache, never()).clear();
  }

  @Test
  public void fetchAndActivate_hasNoAbtExperiments_sendsEmptyListToAbt() throws Exception {
    loadFetchHandlerWithResponse();
    ConfigContainer containerWithNoAbtExperiments =
        ConfigContainer.newBuilder().withFetchTime(new Date(1000L)).build();

    loadCacheWithConfig(mockFetchedCache, containerWithNoAbtExperiments);
    cachePutReturnsConfig(mockActivatedCache, containerWithNoAbtExperiments);

    Task<Boolean> task = frc.fetchAndActivate();

    assertWithMessage("fetchAndActivate() failed!").that(getTaskResult(task)).isTrue();

    verify(mockFirebaseAbt).replaceAllExperiments(ImmutableList.of());
  }

  @Test
  public void fetchAndActivate_callToAbtFails_activateStillSucceeds() throws Exception {
    loadFetchHandlerWithResponse();
    ConfigContainer containerWithAbtExperiments =
        ConfigContainer.newBuilder(firstFetchedContainer)
            .withAbtExperiments(generateAbtExperiments())
            .build();

    loadCacheWithConfig(mockFetchedCache, containerWithAbtExperiments);
    cachePutReturnsConfig(mockActivatedCache, containerWithAbtExperiments);

    doThrow(new AbtException("Abt failure!")).when(mockFirebaseAbt).replaceAllExperiments(any());

    Task<Boolean> task = frc.fetchAndActivate();

    assertWithMessage("fetchAndActivate() failed!").that(getTaskResult(task)).isTrue();
  }

  @Test
  public void fetchAndActivate_hasAbtExperiments_sendsExperimentsToAbt() throws Exception {
    loadFetchHandlerWithResponse();
    ConfigContainer containerWithAbtExperiments =
        ConfigContainer.newBuilder(firstFetchedContainer)
            .withAbtExperiments(generateAbtExperiments())
            .build();

    loadCacheWithConfig(mockFetchedCache, containerWithAbtExperiments);
    cachePutReturnsConfig(mockActivatedCache, containerWithAbtExperiments);

    Task<Boolean> task = frc.fetchAndActivate();

    assertWithMessage("fetchAndActivate() failed!").that(getTaskResult(task)).isTrue();

    List<Map<String, String>> expectedExperimentInfoMaps =
        toExperimentInfoMaps(containerWithAbtExperiments.getAbtExperiments());
    verify(mockFirebaseAbt).replaceAllExperiments(expectedExperimentInfoMaps);
  }

  @Test
  public void fetchAndActivate2p_hasNoAbtExperiments_doesNotCallAbt() throws Exception {
    load2pFetchHandlerWithResponse();
    ConfigContainer containerWithNoAbtExperiments =
        ConfigContainer.newBuilder().withFetchTime(new Date(1000L)).build();

    loadCacheWithConfig(mockFireperfFetchedCache, containerWithNoAbtExperiments);
    cachePutReturnsConfig(mockFireperfActivatedCache, containerWithNoAbtExperiments);

    Task<Boolean> task = fireperfFrc.fetchAndActivate();

    assertWithMessage("2p fetchAndActivate() failed!").that(getTaskResult(task)).isTrue();

    verify(mockFirebaseAbt, never()).replaceAllExperiments(any());
  }

  @Test
  public void fetchAndActivate2p_hasAbtExperiments_doesNotCallAbt() throws Exception {
    load2pFetchHandlerWithResponse();
    ConfigContainer containerWithAbtExperiments =
        ConfigContainer.newBuilder(firstFetchedContainer)
            .withAbtExperiments(generateAbtExperiments())
            .build();

    loadCacheWithConfig(mockFireperfFetchedCache, containerWithAbtExperiments);
    cachePutReturnsConfig(mockFireperfActivatedCache, containerWithAbtExperiments);

    Task<Boolean> task = fireperfFrc.fetchAndActivate();

    assertWithMessage("2p fetchAndActivate() failed!").that(getTaskResult(task)).isTrue();

    verify(mockFirebaseAbt, never()).replaceAllExperiments(any());
  }

  @SuppressWarnings("deprecation")
  @Test
  public void activateFetched_noFetchedConfigs_returnsFalse() {
    loadCacheWithConfig(mockFetchedCache, /*container=*/ null);
    loadCacheWithConfig(mockActivatedCache, /*container=*/ null);

    assertWithMessage("activateFetched() succeeded with no fetched values!")
        .that(frc.activateFetched())
        .isFalse();

    verify(mockActivatedCache, never()).put(any());
    verify(mockFetchedCache, never()).clear();
  }

  @SuppressWarnings("deprecation")
  @Test
  public void activateFetched_staleFetchedConfigs_returnsFalse() {
    loadCacheWithConfig(mockFetchedCache, firstFetchedContainer);
    loadCacheWithConfig(mockActivatedCache, firstFetchedContainer);

    assertWithMessage("activateFetched() succeeded with stale fetched values!")
        .that(frc.activateFetched())
        .isFalse();

    verify(mockActivatedCache, never()).put(any());
    verify(mockFetchedCache, never()).clear();
  }

  @SuppressWarnings("deprecation")
  @Test
  public void activateFetched_freshFetchedConfigs_activatesAndClearsFetched() {
    loadCacheWithConfig(mockFetchedCache, secondFetchedContainer);
    loadCacheWithConfig(mockActivatedCache, firstFetchedContainer);
    // When the fetched values are activated, they should be put into the activated cache.
    when(mockActivatedCache.putWithoutWaitingForDiskWrite(secondFetchedContainer))
        .thenReturn(Tasks.forResult(secondFetchedContainer));

    assertWithMessage("activateFetched() failed!").that(frc.activateFetched()).isTrue();

    verify(mockActivatedCache).putWithoutWaitingForDiskWrite(secondFetchedContainer);
    verify(mockFetchedCache).clear();
  }

  @SuppressWarnings("deprecation")
  @Test
  public void activateFetched_fileWriteFails_doesNotClearFetchedAndReturnsTrue() {
    loadCacheWithConfig(mockFetchedCache, secondFetchedContainer);
    loadCacheWithConfig(mockActivatedCache, firstFetchedContainer);
    when(mockActivatedCache.putWithoutWaitingForDiskWrite(secondFetchedContainer))
        .thenReturn(Tasks.forException(new IOException("Should have handled disk error.")));

    assertWithMessage("activateFetched() failed!").that(frc.activateFetched()).isTrue();

    verify(mockActivatedCache).putWithoutWaitingForDiskWrite(secondFetchedContainer);
    verify(mockFetchedCache, never()).clear();
  }

  @SuppressWarnings("deprecation")
  @Test
  public void activateFetched_hasNoAbtExperiments_sendsEmptyListToAbt() throws Exception {
    ConfigContainer containerWithNoAbtExperiments =
        ConfigContainer.newBuilder().withFetchTime(new Date(1000L)).build();
    loadCacheWithConfig(mockFetchedCache, containerWithNoAbtExperiments);

    // When the fetched values are activated, they should be put into the activated cache.
    when(mockActivatedCache.putWithoutWaitingForDiskWrite(containerWithNoAbtExperiments))
        .thenReturn(Tasks.forResult(containerWithNoAbtExperiments));

    assertWithMessage("activateFetched() failed!").that(frc.activateFetched()).isTrue();

    verify(mockFirebaseAbt).replaceAllExperiments(ImmutableList.of());
  }

  @SuppressWarnings("deprecation")
  @Test
  public void activateFetched_callToAbtFails_activateStillSucceeds() throws Exception {
    ConfigContainer containerWithAbtExperiments =
        ConfigContainer.newBuilder(firstFetchedContainer)
            .withAbtExperiments(generateAbtExperiments())
            .build();
    loadCacheWithConfig(mockFetchedCache, containerWithAbtExperiments);
    loadCacheWithConfig(mockActivatedCache, /*container=*/ null);

    // When the fetched values are activated, they should be put into the activated cache.
    when(mockActivatedCache.putWithoutWaitingForDiskWrite(containerWithAbtExperiments))
        .thenReturn(Tasks.forResult(containerWithAbtExperiments));

    doThrow(new AbtException("Abt failure!")).when(mockFirebaseAbt).replaceAllExperiments(any());

    assertWithMessage("activateFetched() failed!").that(frc.activateFetched()).isTrue();
  }

  @SuppressWarnings("deprecation")
  @Test
  public void activateFetched_hasAbtExperiments_sendsExperimentsToAbt() throws Exception {
    ConfigContainer containerWithAbtExperiments =
        ConfigContainer.newBuilder(firstFetchedContainer)
            .withAbtExperiments(generateAbtExperiments())
            .build();
    loadCacheWithConfig(mockFetchedCache, containerWithAbtExperiments);

    // When the fetched values are activated, they should be put into the activated cache.
    when(mockActivatedCache.putWithoutWaitingForDiskWrite(containerWithAbtExperiments))
        .thenReturn(Tasks.forResult(containerWithAbtExperiments));

    assertWithMessage("activateFetched() failed!").that(frc.activateFetched()).isTrue();

    List<Map<String, String>> expectedExperimentInfoMaps =
        toExperimentInfoMaps(containerWithAbtExperiments.getAbtExperiments());
    verify(mockFirebaseAbt).replaceAllExperiments(expectedExperimentInfoMaps);
  }

  @SuppressWarnings("deprecation")
  @Test
  public void activateFetched_fireperfNamespace_noFetchedConfigs_returnsFalse() {
    loadCacheWithConfig(mockFireperfFetchedCache, /*container=*/ null);
    loadCacheWithConfig(mockFireperfActivatedCache, /*container=*/ null);

    assertWithMessage("activateFetched(fireperf) succeeded with no fetched values!")
        .that(fireperfFrc.activateFetched())
        .isFalse();

    verify(mockFireperfActivatedCache, never()).put(any());
    verify(mockFireperfFetchedCache, never()).clear();
  }

  @SuppressWarnings("deprecation")
  @Test
  public void activateFetched_fireperfNamespace_freshFetchedConfigs_activatesAndClearsFetched() {
    loadCacheWithConfig(mockFireperfFetchedCache, secondFetchedContainer);
    loadCacheWithConfig(mockFireperfActivatedCache, firstFetchedContainer);
    // When the fetched values are activated, they should be put into the activated cache.
    when(mockFireperfActivatedCache.putWithoutWaitingForDiskWrite(secondFetchedContainer))
        .thenReturn(Tasks.forResult(secondFetchedContainer));

    assertWithMessage("activateFetched(fireperf) failed!")
        .that(fireperfFrc.activateFetched())
        .isTrue();

    verify(mockFireperfActivatedCache).putWithoutWaitingForDiskWrite(secondFetchedContainer);
    verify(mockFireperfFetchedCache).clear();
  }

  @SuppressWarnings("deprecation")
  @Test
  public void activateFetched2p_hasNoAbtExperiments_doesNotCallAbt() throws Exception {
    ConfigContainer containerWithNoAbtExperiments =
        ConfigContainer.newBuilder().withFetchTime(new Date(1000L)).build();
    loadCacheWithConfig(mockFireperfFetchedCache, containerWithNoAbtExperiments);

    // When the fetched values are activated, they should be put into the activated cache.
    when(mockFireperfActivatedCache.putWithoutWaitingForDiskWrite(containerWithNoAbtExperiments))
        .thenReturn(Tasks.forResult(containerWithNoAbtExperiments));

    assertWithMessage("activateFetched(fireperf) failed!")
        .that(fireperfFrc.activateFetched())
        .isTrue();

    verify(mockFirebaseAbt, never()).replaceAllExperiments(any());
  }

  @SuppressWarnings("deprecation")
  @Test
  public void activateFetched2p_hasAbtExperiments_doesNotCallAbt() throws Exception {
    ConfigContainer containerWithAbtExperiments =
        ConfigContainer.newBuilder(firstFetchedContainer)
            .withAbtExperiments(generateAbtExperiments())
            .build();
    loadCacheWithConfig(mockFireperfFetchedCache, containerWithAbtExperiments);

    // When the fetched values are activated, they should be put into the activated cache.
    when(mockFireperfActivatedCache.putWithoutWaitingForDiskWrite(containerWithAbtExperiments))
        .thenReturn(Tasks.forResult(containerWithAbtExperiments));

    assertWithMessage("activateFetched(fireperf) failed!")
        .that(fireperfFrc.activateFetched())
        .isTrue();

    verify(mockFirebaseAbt, never()).replaceAllExperiments(any());
  }

  @Test
  public void activate_getFetchedFailed_returnsFalse() {
    loadCacheWithIoException(mockFetchedCache);
    loadCacheWithConfig(mockActivatedCache, null);

    Task<Boolean> activateTask = frc.activate();

    assertWithMessage("activate() succeeded with no fetched values!")
        .that(activateTask.getResult())
        .isFalse();

    verify(mockActivatedCache, never()).put(any());
    verify(mockFetchedCache, never()).clear();
  }

  @Test
  public void activate_noFetchedConfigs_returnsFalse() {
    loadCacheWithConfig(mockFetchedCache, null);
    loadCacheWithConfig(mockActivatedCache, null);

    Task<Boolean> activateTask = frc.activate();

    assertWithMessage("activate() succeeded with no fetched values!")
        .that(activateTask.getResult())
        .isFalse();

    verify(mockActivatedCache, never()).put(any());
    verify(mockFetchedCache, never()).clear();
  }

  @Test
  public void activate_staleFetchedConfigs_returnsFalse() {
    loadCacheWithConfig(mockFetchedCache, firstFetchedContainer);
    loadCacheWithConfig(mockActivatedCache, firstFetchedContainer);

    Task<Boolean> activateTask = frc.activate();

    assertWithMessage("activate() succeeded with stale values!")
        .that(activateTask.getResult())
        .isFalse();

    verify(mockActivatedCache, never()).put(any());
    verify(mockFetchedCache, never()).clear();
  }

  @Test
  public void activate_noActivatedConfigs_activatesAndClearsFetched() {
    loadCacheWithConfig(mockFetchedCache, firstFetchedContainer);
    loadCacheWithConfig(mockActivatedCache, null);

    cachePutReturnsConfig(mockActivatedCache, firstFetchedContainer);

    Task<Boolean> activateTask = frc.activate();

    assertWithMessage("activate() failed with no activated values!")
        .that(activateTask.getResult())
        .isTrue();

    verify(mockActivatedCache).put(firstFetchedContainer);
    verify(mockFetchedCache).clear();
  }

  @Test
  public void activate_getActivatedFailed_activatesAndClearsFetched() {
    loadCacheWithConfig(mockFetchedCache, firstFetchedContainer);
    loadCacheWithIoException(mockActivatedCache);

    cachePutReturnsConfig(mockActivatedCache, firstFetchedContainer);

    Task<Boolean> activateTask = frc.activate();

    assertWithMessage("activate() failed with no activated values!")
        .that(activateTask.getResult())
        .isTrue();

    verify(mockActivatedCache).put(firstFetchedContainer);
    verify(mockFetchedCache).clear();
  }

  @Test
  public void activate_freshFetchedConfigs_activatesAndClearsFetched() {
    loadCacheWithConfig(mockFetchedCache, secondFetchedContainer);
    loadCacheWithConfig(mockActivatedCache, firstFetchedContainer);

    cachePutReturnsConfig(mockActivatedCache, secondFetchedContainer);

    Task<Boolean> activateTask = frc.activate();

    assertWithMessage("activate() failed!").that(activateTask.getResult()).isTrue();

    verify(mockActivatedCache).put(secondFetchedContainer);
    verify(mockFetchedCache).clear();
  }

  @Test
  public void activate_fileWriteFails_doesNotClearFetchedAndReturnsFalse() {
    loadCacheWithConfig(mockFetchedCache, secondFetchedContainer);
    loadCacheWithConfig(mockActivatedCache, firstFetchedContainer);

    when(mockActivatedCache.put(secondFetchedContainer))
        .thenReturn(Tasks.forException(new IOException("Should have handled disk error.")));

    Task<Boolean> activateTask = frc.activate();

    assertWithMessage("activate() succeeded even though file write failed!")
        .that(activateTask.getResult())
        .isFalse();

    verify(mockActivatedCache).put(secondFetchedContainer);
    verify(mockFetchedCache, never()).clear();
  }

  @Test
  public void activate_hasNoAbtExperiments_sendsEmptyListToAbt() throws Exception {
    ConfigContainer containerWithNoAbtExperiments =
        ConfigContainer.newBuilder().withFetchTime(new Date(1000L)).build();

    loadCacheWithConfig(mockFetchedCache, containerWithNoAbtExperiments);
    cachePutReturnsConfig(mockActivatedCache, containerWithNoAbtExperiments);

    Task<Boolean> activateTask = frc.activate();

    assertWithMessage("activate() failed!").that(activateTask.getResult()).isTrue();

    verify(mockFirebaseAbt).replaceAllExperiments(ImmutableList.of());
  }

  @Test
  public void activate_callToAbtFails_activateStillSucceeds() throws Exception {
    ConfigContainer containerWithAbtExperiments =
        ConfigContainer.newBuilder(firstFetchedContainer)
            .withAbtExperiments(generateAbtExperiments())
            .build();

    loadCacheWithConfig(mockFetchedCache, containerWithAbtExperiments);
    cachePutReturnsConfig(mockActivatedCache, containerWithAbtExperiments);

    doThrow(new AbtException("Abt failure!")).when(mockFirebaseAbt).replaceAllExperiments(any());

    Task<Boolean> activateTask = frc.activate();

    assertWithMessage("activate() failed!").that(activateTask.getResult()).isTrue();
  }

  @Test
  public void activate_hasAbtExperiments_sendsExperimentsToAbt() throws Exception {
    ConfigContainer containerWithAbtExperiments =
        ConfigContainer.newBuilder(firstFetchedContainer)
            .withAbtExperiments(generateAbtExperiments())
            .build();

    loadCacheWithConfig(mockFetchedCache, containerWithAbtExperiments);
    cachePutReturnsConfig(mockActivatedCache, containerWithAbtExperiments);

    Task<Boolean> activateTask = frc.activate();

    assertWithMessage("activate() failed!").that(activateTask.getResult()).isTrue();

    List<Map<String, String>> expectedExperimentInfoMaps =
        toExperimentInfoMaps(containerWithAbtExperiments.getAbtExperiments());
    verify(mockFirebaseAbt).replaceAllExperiments(expectedExperimentInfoMaps);
  }

  @Test
  public void activate2p_hasNoAbtExperiments_doesNotCallAbt() throws Exception {
    ConfigContainer containerWithNoAbtExperiments =
        ConfigContainer.newBuilder().withFetchTime(new Date(1000L)).build();

    loadCacheWithConfig(mockFireperfFetchedCache, containerWithNoAbtExperiments);
    cachePutReturnsConfig(mockFireperfActivatedCache, containerWithNoAbtExperiments);

    Task<Boolean> activateTask = fireperfFrc.activate();

    assertWithMessage("Fireperf activate() failed!").that(activateTask.getResult()).isTrue();

    verify(mockFirebaseAbt, never()).replaceAllExperiments(any());
  }

  @Test
  public void activate2p_hasAbtExperiments_doesNotCallAbt() throws Exception {
    ConfigContainer containerWithAbtExperiments =
        ConfigContainer.newBuilder(firstFetchedContainer)
            .withAbtExperiments(generateAbtExperiments())
            .build();

    loadCacheWithConfig(mockFireperfFetchedCache, containerWithAbtExperiments);
    cachePutReturnsConfig(mockFireperfActivatedCache, containerWithAbtExperiments);

    Task<Boolean> activateTask = fireperfFrc.activate();

    assertWithMessage("Fireperf activate() failed!").that(activateTask.getResult()).isTrue();

    verify(mockFirebaseAbt, never()).replaceAllExperiments(any());
  }

  @Test
  public void fetch_hasNoErrors_taskReturnsSuccess() {
    when(mockFetchHandler.fetch()).thenReturn(Tasks.forResult(firstFetchedContainerResponse));

    Task<Void> fetchTask = frc.fetch();

    assertWithMessage("Fetch failed!").that(fetchTask.isSuccessful()).isTrue();
  }

  @Test
  public void fetch_hasNetworkError_taskReturnsException() {
    when(mockFetchHandler.fetch())
        .thenReturn(
            Tasks.forException(new FirebaseRemoteConfigClientException("Network call failed.")));

    Task<Void> fetchTask = frc.fetch();

    assertWithMessage("Fetch succeeded even though there's a network error!")
        .that(fetchTask.isSuccessful())
        .isFalse();
  }

  @Test
  public void fetchWithInterval_hasNoErrors_taskReturnsSuccess() {
    long minimumFetchIntervalInSeconds = 600L;
    when(mockFetchHandler.fetch(minimumFetchIntervalInSeconds))
        .thenReturn(Tasks.forResult(firstFetchedContainerResponse));

    Task<Void> fetchTask = frc.fetch(minimumFetchIntervalInSeconds);

    assertWithMessage("Fetch failed!").that(fetchTask.isSuccessful()).isTrue();
  }

  @Test
  public void fetchWithInterval_hasNetworkError_taskReturnsException() {
    long minimumFetchIntervalInSeconds = 600L;
    when(mockFetchHandler.fetch(minimumFetchIntervalInSeconds))
        .thenReturn(
            Tasks.forException(new FirebaseRemoteConfigClientException("Network call failed.")));

    Task<Void> fetchTask = frc.fetch(minimumFetchIntervalInSeconds);

    assertWithMessage("Fetch succeeded even though there's a network error!")
        .that(fetchTask.isSuccessful())
        .isFalse();
  }

  @Test
  public void getKeysByPrefix_noKeysWithPrefix_returnsEmptySet() {
    when(mockGetHandler.getKeysByPrefix("pre")).thenReturn(ImmutableSet.of());

    assertThat(frc.getKeysByPrefix("pre")).isEmpty();
  }

  @Test
  public void getKeysByPrefix_hasKeysWithPrefix_returnsKeysWithPrefix() {
    Set<String> keysWithPrefix = ImmutableSet.of("pre11", "pre12");
    when(mockGetHandler.getKeysByPrefix("pre")).thenReturn(keysWithPrefix);

    assertThat(frc.getKeysByPrefix("pre")).containsExactlyElementsIn(keysWithPrefix);
  }

  @Test
  public void getString_keyDoesNotExist_returnsDefaultValue() {
    when(mockGetHandler.getString(STRING_KEY)).thenReturn(DEFAULT_VALUE_FOR_STRING);

    assertThat(frc.getString(STRING_KEY)).isEqualTo(DEFAULT_VALUE_FOR_STRING);
  }

  @Test
  public void getString_keyExists_returnsRemoteValue() {
    String remoteValue = "remote value";
    when(mockGetHandler.getString(STRING_KEY)).thenReturn(remoteValue);

    assertThat(frc.getString(STRING_KEY)).isEqualTo(remoteValue);
  }

  @SuppressWarnings("deprecation")
  @Test
  public void getString_fireperfNamespace_keyDoesNotExist_returnsDefaultValue() {
    when(mockFireperfGetHandler.getString(STRING_KEY)).thenReturn(DEFAULT_VALUE_FOR_STRING);

    assertThat(fireperfFrc.getString(STRING_KEY)).isEqualTo(DEFAULT_VALUE_FOR_STRING);
  }

  @SuppressWarnings("deprecation")
  @Test
  public void getString_fireperfNamespace_keyExists_returnsRemoteValue() {
    String remoteValue = "remote value";
    when(mockFireperfGetHandler.getString(STRING_KEY)).thenReturn(remoteValue);

    assertThat(fireperfFrc.getString(STRING_KEY)).isEqualTo(remoteValue);
  }

  @Test
  public void getBoolean_keyDoesNotExist_returnsDefaultValue() {
    when(mockGetHandler.getBoolean(BOOLEAN_KEY)).thenReturn(DEFAULT_VALUE_FOR_BOOLEAN);

    assertThat(frc.getBoolean(BOOLEAN_KEY)).isEqualTo(DEFAULT_VALUE_FOR_BOOLEAN);
  }

  @Test
  public void getBoolean_keyExists_returnsRemoteValue() {
    when(mockGetHandler.getBoolean(BOOLEAN_KEY)).thenReturn(true);

    assertThat(frc.getBoolean(BOOLEAN_KEY)).isTrue();
  }

  @SuppressWarnings("deprecation")
  @Test
  public void getBoolean_fireperfNamespace_keyDoesNotExist_returnsDefaultValue() {
    when(mockFireperfGetHandler.getBoolean(BOOLEAN_KEY)).thenReturn(DEFAULT_VALUE_FOR_BOOLEAN);

    assertThat(fireperfFrc.getBoolean(BOOLEAN_KEY)).isEqualTo(DEFAULT_VALUE_FOR_BOOLEAN);
  }

  @SuppressWarnings("deprecation")
  @Test
  public void getBoolean_fireperfNamespace_keyExists_returnsRemoteValue() {
    when(mockFireperfGetHandler.getBoolean(BOOLEAN_KEY)).thenReturn(true);

    assertThat(fireperfFrc.getBoolean(BOOLEAN_KEY)).isTrue();
  }

  @SuppressWarnings("deprecation")
  @Test
  public void getByteArray_keyDoesNotExist_returnsDefaultValue() {
    when(mockGetHandler.getByteArray(BYTE_ARRAY_KEY)).thenReturn(DEFAULT_VALUE_FOR_BYTE_ARRAY);

    assertThat(frc.getByteArray(BYTE_ARRAY_KEY)).isEqualTo(DEFAULT_VALUE_FOR_BYTE_ARRAY);
  }

  @SuppressWarnings("deprecation")
  @Test
  public void getByteArray_keyExists_returnsRemoteValue() {
    byte[] remoteValue = "remote value".getBytes(FRC_BYTE_ARRAY_ENCODING);
    when(mockGetHandler.getByteArray(BYTE_ARRAY_KEY)).thenReturn(remoteValue);

    assertThat(frc.getByteArray(BYTE_ARRAY_KEY)).isEqualTo(remoteValue);
  }

  @SuppressWarnings("deprecation")
  @Test
  public void getByteArray_fireperfNamespace_keyDoesNotExist_returnsDefaultValue() {
    when(mockFireperfGetHandler.getByteArray(BYTE_ARRAY_KEY))
        .thenReturn(DEFAULT_VALUE_FOR_BYTE_ARRAY);

    assertThat(fireperfFrc.getByteArray(BYTE_ARRAY_KEY)).isEqualTo(DEFAULT_VALUE_FOR_BYTE_ARRAY);
  }

  @SuppressWarnings("deprecation")
  @Test
  public void getByteArray_fireperfNamespace_keyExists_returnsRemoteValue() {
    byte[] remoteValue = "remote value".getBytes(FRC_BYTE_ARRAY_ENCODING);
    when(mockFireperfGetHandler.getByteArray(BYTE_ARRAY_KEY)).thenReturn(remoteValue);

    assertThat(fireperfFrc.getByteArray(BYTE_ARRAY_KEY)).isEqualTo(remoteValue);
  }

  @Test
  public void getDouble_keyDoesNotExist_returnsDefaultValue() {
    when(mockGetHandler.getDouble(DOUBLE_KEY)).thenReturn(DEFAULT_VALUE_FOR_DOUBLE);

    assertThat(frc.getDouble(DOUBLE_KEY)).isEqualTo(DEFAULT_VALUE_FOR_DOUBLE);
  }

  @Test
  public void getDouble_keyExists_returnsRemoteValue() {
    double remoteValue = 555.5;
    when(mockGetHandler.getDouble(DOUBLE_KEY)).thenReturn(remoteValue);

    assertThat(frc.getDouble(DOUBLE_KEY)).isEqualTo(remoteValue);
  }

  @SuppressWarnings("deprecation")
  @Test
  public void getDouble_fireperfNamespace_keyDoesNotExist_returnsDefaultValue() {
    when(mockFireperfGetHandler.getDouble(DOUBLE_KEY)).thenReturn(DEFAULT_VALUE_FOR_DOUBLE);

    assertThat(fireperfFrc.getDouble(DOUBLE_KEY)).isEqualTo(DEFAULT_VALUE_FOR_DOUBLE);
  }

  @SuppressWarnings("deprecation")
  @Test
  public void getDouble_fireperfNamespace_keyExists_returnsRemoteValue() {
    double remoteValue = 555.5;
    when(mockFireperfGetHandler.getDouble(DOUBLE_KEY)).thenReturn(remoteValue);

    assertThat(fireperfFrc.getDouble(DOUBLE_KEY)).isEqualTo(remoteValue);
  }

  @Test
  public void getLong_keyDoesNotExist_returnsDefaultValue() {
    when(mockGetHandler.getLong(LONG_KEY)).thenReturn(DEFAULT_VALUE_FOR_LONG);

    assertThat(frc.getLong(LONG_KEY)).isEqualTo(DEFAULT_VALUE_FOR_LONG);
  }

  @Test
  public void getLong_keyExists_returnsRemoteValue() {
    long remoteValue = 555L;
    when(mockGetHandler.getLong(LONG_KEY)).thenReturn(remoteValue);

    assertThat(frc.getLong(LONG_KEY)).isEqualTo(remoteValue);
  }

  @SuppressWarnings("deprecation")
  @Test
  public void getLong_fireperfNamespace_keyDoesNotExist_returnsDefaultValue() {
    when(mockFireperfGetHandler.getLong(LONG_KEY)).thenReturn(DEFAULT_VALUE_FOR_LONG);

    assertThat(fireperfFrc.getLong(LONG_KEY)).isEqualTo(DEFAULT_VALUE_FOR_LONG);
  }

  @SuppressWarnings("deprecation")
  @Test
  public void getLong_fireperfNamespace_keyExists_returnsRemoteValue() {
    long remoteValue = 555L;
    when(mockFireperfGetHandler.getLong(LONG_KEY)).thenReturn(remoteValue);

    assertThat(fireperfFrc.getLong(LONG_KEY)).isEqualTo(remoteValue);
  }

  @Test
  public void getInfo_returnsInfo() {
    when(metadataClient.getInfo()).thenReturn(mockFrcInfo);

    long fetchTimeInMillis = 100L;
    int lastFetchStatus = LAST_FETCH_STATUS_THROTTLED;
    long fetchTimeoutInSeconds = 10L;
    long minimumFetchIntervalInSeconds = 100L;
    when(mockFrcInfo.getFetchTimeMillis()).thenReturn(fetchTimeInMillis);
    when(mockFrcInfo.getLastFetchStatus()).thenReturn(lastFetchStatus);
    when(mockFrcInfo.getConfigSettings())
        .thenReturn(
            new FirebaseRemoteConfigSettings.Builder()
                .setDeveloperModeEnabled(true)
                .setFetchTimeoutInSeconds(fetchTimeoutInSeconds)
                .setMinimumFetchIntervalInSeconds(minimumFetchIntervalInSeconds)
                .build());

    FirebaseRemoteConfigInfo info = frc.getInfo();

    assertThat(info.getFetchTimeMillis()).isEqualTo(fetchTimeInMillis);
    assertThat(info.getLastFetchStatus()).isEqualTo(lastFetchStatus);
    assertThat(info.getConfigSettings().isDeveloperModeEnabled()).isEqualTo(true);
    assertThat(info.getConfigSettings().getFetchTimeoutInSeconds())
        .isEqualTo(fetchTimeoutInSeconds);
    assertThat(info.getConfigSettings().getMinimumFetchIntervalInSeconds())
        .isEqualTo(minimumFetchIntervalInSeconds);
  }

  @Test
  public void setDefaults_withMap_setsDefaults() throws Exception {
    frc.setDefaults(ImmutableMap.copyOf(DEFAULTS_MAP));

    ConfigContainer defaultsContainer = newDefaultsContainer(DEFAULTS_STRING_MAP);
    ArgumentCaptor<ConfigContainer> captor = ArgumentCaptor.forClass(ConfigContainer.class);

    verify(mockDefaultsCache).putWithoutWaitingForDiskWrite(captor.capture());
    JSONAssert.assertEquals(defaultsContainer.toString(), captor.getValue().toString(), false);
  }

  @Test
  public void setDefaultsAsync_withMap_setsDefaults() throws Exception {
    ConfigContainer defaultsContainer = newDefaultsContainer(DEFAULTS_STRING_MAP);
    ArgumentCaptor<ConfigContainer> captor = ArgumentCaptor.forClass(ConfigContainer.class);
    cachePutReturnsConfig(mockDefaultsCache, defaultsContainer);

    boolean isComplete = frc.setDefaultsAsync(ImmutableMap.copyOf(DEFAULTS_MAP)).isComplete();

    assertThat(isComplete).isTrue();
    // Assert defaults were set correctly.
    verify(mockDefaultsCache).put(captor.capture());

    JSONAssert.assertEquals(defaultsContainer.toString(), captor.getValue().toString(), false);
  }

  @Test
  public void clear_hasSettings_clearsEverything() {
    frc.reset();

    verify(mockActivatedCache).clear();
    verify(mockFetchedCache).clear();
    verify(mockDefaultsCache).clear();
    verify(metadataClient).clear();
  }

  @Test
  public void setConfigSettings_updatesMetadata() {
    long fetchTimeout = 13L;
    long minimumFetchInterval = 666L;
    FirebaseRemoteConfigSettings frcSettings =
        new FirebaseRemoteConfigSettings.Builder()
            .setDeveloperModeEnabled(true)
            .setFetchTimeoutInSeconds(fetchTimeout)
            .setMinimumFetchIntervalInSeconds(minimumFetchInterval)
            .build();

    frc.setConfigSettings(frcSettings);

    verify(metadataClient).setConfigSettingsWithoutWaitingOnDiskWrite(frcSettings);
  }

  @Test
  public void setConfigSettingsAsync_updatesMetadata() {
    long fetchTimeout = 13L;
    long minimumFetchInterval = 666L;
    FirebaseRemoteConfigSettings frcSettings =
        new FirebaseRemoteConfigSettings.Builder()
            .setDeveloperModeEnabled(true)
            .setFetchTimeoutInSeconds(fetchTimeout)
            .setMinimumFetchIntervalInSeconds(minimumFetchInterval)
            .build();

    Task<Void> setterTask = frc.setConfigSettingsAsync(frcSettings);

    assertThat(setterTask.isSuccessful()).isTrue();
    verify(metadataClient).setConfigSettings(frcSettings);
  }

  private static void loadCacheWithConfig(
      ConfigCacheClient cacheClient, ConfigContainer container) {
    when(cacheClient.getBlocking()).thenReturn(container);
    when(cacheClient.get()).thenReturn(Tasks.forResult(container));
  }

  private static void loadCacheWithIoException(ConfigCacheClient cacheClient) {
    when(cacheClient.getBlocking()).thenReturn(null);
    when(cacheClient.get())
        .thenReturn(Tasks.forException(new IOException("Should have handled disk error.")));
  }

  private void loadActivatedCacheWithIncompleteTask() {
    TaskCompletionSource<ConfigContainer> taskSource = new TaskCompletionSource<>();
    when(mockActivatedCache.get()).thenReturn(taskSource.getTask());
  }

  private static void cachePutReturnsConfig(
      ConfigCacheClient cacheClient, ConfigContainer container) {
    when(cacheClient.put(container)).thenReturn(Tasks.forResult(container));
  }

  private void loadFetchHandlerWithResponse() {
    when(mockFetchHandler.fetch()).thenReturn(Tasks.forResult(firstFetchedContainerResponse));
  }

  private void load2pFetchHandlerWithResponse() {
    when(mockFireperfFetchHandler.fetch())
        .thenReturn(Tasks.forResult(firstFetchedContainerResponse));
  }

  private void loadInstanceIdAndToken() {
    when(mockFirebaseInstallations.getId()).thenReturn(Tasks.forResult(INSTALLATION_ID));
    when(mockFirebaseInstallations.getToken(false))
        .thenReturn(Tasks.forResult(INSTALLATION_TOKEN_RESULT));
  }

  private static int getResourceId(String xmlResourceName) {
    Resources r = RuntimeEnvironment.application.getResources();
    return r.getIdentifier(xmlResourceName, "xml", RuntimeEnvironment.application.getPackageName());
  }

  private static ConfigContainer newDefaultsContainer(Map<String, String> configsMap)
      throws Exception {
    return ConfigContainer.newBuilder()
        .replaceConfigsWith(configsMap)
        .withFetchTime(new Date(0L))
        .build();
  }

  private <T> T getTaskResult(Task<T> task) {
    assertThat(task.isComplete()).isTrue();
    assertThat(task.getResult()).isNotNull();
    return task.getResult();
  }

  private static JSONArray generateAbtExperiments() throws JSONException {
    JSONArray experiments = new JSONArray();
    for (int experimentNum = 1; experimentNum <= 5; experimentNum++) {
      experiments.put(createAbtExperiment("exp" + experimentNum));
    }
    return experiments;
  }

  private static FirebaseApp initializeFirebaseApp(Context context) {
    FirebaseApp.clearInstancesForTest();

    return FirebaseApp.initializeApp(
        context,
        new FirebaseOptions.Builder()
            .setApiKey(API_KEY)
            .setApplicationId(APP_ID)
            .setProjectId(PROJECT_ID)
            .build());
  }
}