/*
 * Copyright 2022 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.android.libraries.mobiledatadownload.internal;

import static com.google.android.libraries.mobiledatadownload.internal.MddTestUtil.writeSharedFiles;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

import android.accounts.Account;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import androidx.test.core.app.ApplicationProvider;
import com.google.mobiledatadownload.internal.MetadataProto.DataFile;
import com.google.mobiledatadownload.internal.MetadataProto.DataFileGroupBookkeeping;
import com.google.mobiledatadownload.internal.MetadataProto.DataFileGroupInternal;
import com.google.mobiledatadownload.internal.MetadataProto.DataFileGroupInternal.AllowedReaders;
import com.google.mobiledatadownload.internal.MetadataProto.DownloadConditions;
import com.google.mobiledatadownload.internal.MetadataProto.DownloadConditions.ActivatingCondition;
import com.google.mobiledatadownload.internal.MetadataProto.DownloadConditions.DeviceNetworkPolicy;
import com.google.mobiledatadownload.internal.MetadataProto.DownloadConditions.DeviceStoragePolicy;
import com.google.mobiledatadownload.internal.MetadataProto.ExtraHttpHeader;
import com.google.mobiledatadownload.internal.MetadataProto.FileStatus;
import com.google.mobiledatadownload.internal.MetadataProto.GroupKey;
import com.google.mobiledatadownload.internal.MetadataProto.NewFileKey;
import com.google.mobiledatadownload.internal.MetadataProto.SharedFile;
import com.google.android.libraries.mobiledatadownload.AccountSource;
import com.google.android.libraries.mobiledatadownload.AggregateException;
import com.google.android.libraries.mobiledatadownload.DownloadException;
import com.google.android.libraries.mobiledatadownload.DownloadException.DownloadResultCode;
import com.google.android.libraries.mobiledatadownload.FileSource;
import com.google.android.libraries.mobiledatadownload.SilentFeedback;
import com.google.android.libraries.mobiledatadownload.account.AccountUtil;
import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage;
import com.google.android.libraries.mobiledatadownload.file.backends.AndroidFileBackend;
import com.google.android.libraries.mobiledatadownload.file.common.LimitExceededException;
import com.google.android.libraries.mobiledatadownload.file.spi.Backend;
import com.google.android.libraries.mobiledatadownload.internal.FileGroupManager.GroupDownloadStatus;
import com.google.android.libraries.mobiledatadownload.internal.collect.GroupKeyAndGroup;
import com.google.android.libraries.mobiledatadownload.internal.downloader.DownloaderCallbackImpl;
import com.google.android.libraries.mobiledatadownload.internal.downloader.MddFileDownloader;
import com.google.android.libraries.mobiledatadownload.internal.experimentation.DownloadStageManager;
import com.google.android.libraries.mobiledatadownload.internal.experimentation.NoOpDownloadStageManager;
import com.google.android.libraries.mobiledatadownload.internal.logging.DownloadStateLogger;
import com.google.android.libraries.mobiledatadownload.internal.logging.EventLogger;
import com.google.android.libraries.mobiledatadownload.internal.logging.testing.FakeEventLogger;
import com.google.android.libraries.mobiledatadownload.internal.util.DirectoryUtil;
import com.google.android.libraries.mobiledatadownload.internal.util.FileGroupUtil;
import com.google.android.libraries.mobiledatadownload.monitor.DownloadProgressMonitor;
import com.google.android.libraries.mobiledatadownload.testing.FakeTimeSource;
import com.google.android.libraries.mobiledatadownload.testing.TestFlags;
import com.google.common.base.Optional;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.labs.concurrent.LabsFutures;
import com.google.common.truth.Correspondence;
import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.mobiledatadownload.LogEnumsProto.MddClientEvent;
import com.google.mobiledatadownload.LogEnumsProto.MddDownloadResult;
import com.google.mobiledatadownload.LogProto.DataDownloadFileGroupStats;
import com.google.protobuf.Any;
import com.google.protobuf.ByteString;
import com.google.protobuf.ExtensionRegistryLite;
import com.google.protobuf.StringValue;
import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.mockito.stubbing.Answer;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.Shadows;
import org.robolectric.util.ReflectionHelpers;

@RunWith(RobolectricTestRunner.class)
public class FileGroupManagerTest {

  private static final long CURRENT_TIMESTAMP = 1000;

  private static final int TRAFFIC_TAG = 1000;

  private static final Executor SEQUENTIAL_CONTROL_EXECUTOR =
      Executors.newSingleThreadScheduledExecutor();

  private static final String TEST_GROUP = "test-group";
  private static final String TEST_GROUP_2 = "test-group-2";
  private static final String TEST_GROUP_3 = "test-group-3";
  private static final String TEST_GROUP_4 = "test-group-4";
  private static final long FILE_GROUP_EXPIRATION_DATE_SECS = 10;
  private static final String HOST_APP_LOG_SOURCE = "HOST_APP_LOG_SOURCE";
  private static final String HOST_APP_PRIMES_LOG_SOURCE = "HOST_APP_PRIMES_LOG_SOURCE";

  private static final Correspondence<GroupKey, String> GROUP_KEY_TO_VARIANT =
      Correspondence.transforming(GroupKey::getVariantId, "using variant");
  private static final Correspondence<GroupKeyAndGroup, String> KEY_GROUP_PAIR_TO_VARIANT =
      Correspondence.transforming(
          keyGroupPair -> {
            assertThat(keyGroupPair.groupKey().getVariantId())
                .isEqualTo(keyGroupPair.dataFileGroup().getVariantId());
            return keyGroupPair.dataFileGroup().getVariantId();
          },
          "using variant from group key and file group");

  private static GroupKey testKey;
  private static GroupKey testKey2;
  private static GroupKey testKey3;
  private static GroupKey testKey4;

  private Context context;
  private FileGroupManager fileGroupManager;
  private FileGroupsMetadata fileGroupsMetadata;
  private SharedFileManager sharedFileManager;
  private SharedFilesMetadata sharedFilesMetadata;
  private FakeTimeSource testClock;
  private SynchronousFileStorage fileStorage;
  public File publicDirectory;
  private final TestFlags flags = new TestFlags();

  @Rule(order = 2)
  public TemporaryFolder folder = new TemporaryFolder();

  @Rule(order = 3)
  public final MockitoRule mocks = MockitoJUnit.rule();

  @Mock EventLogger mockLogger;
  @Mock SilentFeedback mockSilentFeedback;
  @Mock MddFileDownloader mockDownloader;
  @Mock SharedFileManager mockSharedFileManager;
  @Mock FileGroupsMetadata mockFileGroupsMetadata;
  @Mock DownloadProgressMonitor mockDownloadMonitor;
  @Mock AccountSource mockAccountSource;
  @Mock Backend mockBackend;
  @Mock Closeable closeable;

  @Captor ArgumentCaptor<FileSource> fileSourceCaptor;
  @Captor ArgumentCaptor<GroupKey> groupKeyCaptor;
  @Captor ArgumentCaptor<List<GroupKey>> groupKeysCaptor;

  private DownloadStageManager downloadStageManager;

  @Before
  public void setUp() throws Exception {
    context = ApplicationProvider.getApplicationContext();

    when(mockBackend.name()).thenReturn("blobstore");
    fileStorage =
        new SynchronousFileStorage(
            Arrays.asList(AndroidFileBackend.builder(context).build(), mockBackend));

    testClock = new FakeTimeSource().set(CURRENT_TIMESTAMP);

    testKey =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP)
            .setOwnerPackage(context.getPackageName())
            .build();
    testKey2 =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP_2)
            .setOwnerPackage(context.getPackageName())
            .build();
    testKey3 =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP_3)
            .setOwnerPackage(context.getPackageName())
            .build();
    testKey4 =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP_4)
            .setOwnerPackage(context.getPackageName())
            .build();

    fileGroupsMetadata =
        new SharedPreferencesFileGroupsMetadata(
            context,
            testClock,
            mockSilentFeedback,
            Optional.absent(),
            MoreExecutors.directExecutor());
    sharedFilesMetadata =
        new SharedPreferencesSharedFilesMetadata(
            context, mockSilentFeedback, Optional.absent(), flags);
    sharedFileManager =
        new SharedFileManager(
            context,
            mockSilentFeedback,
            sharedFilesMetadata,
            fileStorage,
            mockDownloader,
            Optional.absent(),
            Optional.of(mockDownloadMonitor),
            mockLogger,
            flags,
            fileGroupsMetadata,
            Optional.absent(),
            MoreExecutors.directExecutor());

    downloadStageManager = new NoOpDownloadStageManager();

    fileGroupManager =
        new FileGroupManager(
            context,
            mockLogger,
            mockSilentFeedback,
            fileGroupsMetadata,
            sharedFileManager,
            testClock,
            Optional.of(mockAccountSource),
            SEQUENTIAL_CONTROL_EXECUTOR,
            Optional.absent(),
            fileStorage,
            downloadStageManager,
            flags);
    // TODO(b/117571083): Replace with fileStorage API.
    File downloadDirectory =
        new File(context.getFilesDir(), DirectoryUtil.MDD_STORAGE_MODULE + "/" + "shared");
    publicDirectory = new File(downloadDirectory, DirectoryUtil.MDD_STORAGE_ALL_GOOGLE_APPS);
    publicDirectory.mkdirs();

    // file sharing is available for SDK R+
    ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.R);
  }

  private void assertLoggedNewConfigs(
      FakeEventLogger fakeEventLogger,
      DataDownloadFileGroupStats fileGroupStats,
      Void newConfigReceivedInfo) {
    ArrayListMultimap<DataDownloadFileGroupStats, Void> loggedConfigs =
        fakeEventLogger.getLoggedNewConfigReceived();
    assertThat(loggedConfigs).hasSize(1);
    assertThat(loggedConfigs.get(fileGroupStats)).containsExactly(newConfigReceivedInfo);
  }

  @Test
  public void testAddGroupForDownload() throws Exception {
    FakeEventLogger fakeEventLogger = new FakeEventLogger();

    resetFileGroupManager(fakeEventLogger, fileGroupsMetadata, sharedFileManager);

    DataFileGroupInternal dataFileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    NewFileKey[] groupKeys = MddTestUtil.createFileKeysForDataFileGroupInternal(dataFileGroup);

    assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup).get()).isTrue();
    verifyAddGroupForDownloadWritesMetadata(testKey, dataFileGroup, CURRENT_TIMESTAMP);

    // Check that downloaded file groups doesn't contain this file group.
    assertThat(readDownloadedFileGroup(testKey)).isNull();

    assertThat(sharedFileManager.getSharedFile(groupKeys[0]).get()).isNotNull();
    assertThat(sharedFileManager.getSharedFile(groupKeys[1]).get()).isNotNull();

    assertLoggedNewConfigs(
        fakeEventLogger, createFileGroupDetails(dataFileGroup).clearFileCount().build(), null);
  }

  @Test
  public void testAddGroupForDownload_correctlyPopulatesBuildIdAndVariantId() throws Exception {
    FakeEventLogger fakeEventLogger = new FakeEventLogger();
    resetFileGroupManager(fakeEventLogger, fileGroupsMetadata, sharedFileManager);

    DataFileGroupInternal dataFileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2).toBuilder()
            .setBuildId(10)
            .setVariantId("testVariant")
            .build();
    NewFileKey[] groupKeys = MddTestUtil.createFileKeysForDataFileGroupInternal(dataFileGroup);

    assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup).get()).isTrue();
    verifyAddGroupForDownloadWritesMetadata(testKey, dataFileGroup, CURRENT_TIMESTAMP);

    // Check that downloaded file groups doesn't contain this file group.
    assertThat(readDownloadedFileGroup(testKey)).isNull();

    assertThat(sharedFileManager.getSharedFile(groupKeys[0]).get()).isNotNull();
    assertThat(sharedFileManager.getSharedFile(groupKeys[1]).get()).isNotNull();

    assertLoggedNewConfigs(
        fakeEventLogger, createFileGroupDetails(dataFileGroup).clearFileCount().build(), null);
  }

  @Test
  public void testAddGroupForDownload_groupUpdated() throws Exception {
    FakeEventLogger fakeEventLogger = new FakeEventLogger();
    resetFileGroupManager(fakeEventLogger, fileGroupsMetadata, sharedFileManager);

    DataFileGroupInternal dataFileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup).get()).isTrue();
    verifyAddGroupForDownloadWritesMetadata(testKey, dataFileGroup, CURRENT_TIMESTAMP);

    assertLoggedNewConfigs(
        fakeEventLogger, createFileGroupDetails(dataFileGroup).clearFileCount().build(), null);
    fakeEventLogger.reset();

    // Update the file id and see that the group gets updated in the pending groups list.
    dataFileGroup =
        dataFileGroup.toBuilder()
            .setFile(0, dataFileGroup.getFile(0).toBuilder().setFileId("file2"))
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .build();
    assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup).get()).isTrue();
    verifyAddGroupForDownloadWritesMetadata(testKey, dataFileGroup, CURRENT_TIMESTAMP);

    assertLoggedNewConfigs(
        fakeEventLogger, createFileGroupDetails(dataFileGroup).clearFileCount().build(), null);
    fakeEventLogger.reset();

    // Update other parameters and check that we successfully add the group.
    dataFileGroup = dataFileGroup.toBuilder().setFileGroupVersionNumber(2).build();
    assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup).get()).isTrue();
    verifyAddGroupForDownloadWritesMetadata(testKey, dataFileGroup, CURRENT_TIMESTAMP);

    assertLoggedNewConfigs(
        fakeEventLogger, createFileGroupDetails(dataFileGroup).clearFileCount().build(), null);
    fakeEventLogger.reset();

    dataFileGroup = dataFileGroup.toBuilder().setStaleLifetimeSecs(50).build();
    assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup).get()).isTrue();
    verifyAddGroupForDownloadWritesMetadata(testKey, dataFileGroup, CURRENT_TIMESTAMP);

    assertLoggedNewConfigs(
        fakeEventLogger, createFileGroupDetails(dataFileGroup).clearFileCount().build(), null);
    fakeEventLogger.reset();

    dataFileGroup =
        dataFileGroup.toBuilder()
            .setDownloadConditions(
                DownloadConditions.newBuilder()
                    .setDeviceNetworkPolicy(
                        DeviceNetworkPolicy.DOWNLOAD_FIRST_ON_WIFI_THEN_ON_ANY_NETWORK))
            .build();
    assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup).get()).isTrue();
    verifyAddGroupForDownloadWritesMetadata(testKey, dataFileGroup, CURRENT_TIMESTAMP);

    assertLoggedNewConfigs(
        fakeEventLogger, createFileGroupDetails(dataFileGroup).clearFileCount().build(), null);
    fakeEventLogger.reset();

    DownloadConditions downloadConditions =
        DownloadConditions.newBuilder()
            .setDeviceStoragePolicy(DeviceStoragePolicy.BLOCK_DOWNLOAD_LOWER_THRESHOLD)
            .build();
    dataFileGroup = dataFileGroup.toBuilder().setDownloadConditions(downloadConditions).build();
    assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup).get()).isTrue();
    verifyAddGroupForDownloadWritesMetadata(testKey, dataFileGroup, CURRENT_TIMESTAMP);

    assertLoggedNewConfigs(
        fakeEventLogger, createFileGroupDetails(dataFileGroup).clearFileCount().build(), null);
    fakeEventLogger.reset();

    dataFileGroup =
        dataFileGroup.toBuilder()
            .setAllowedReadersEnum(AllowedReaders.ONLY_GOOGLE_PLAY_SERVICES)
            .build();
    assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup).get()).isTrue();
    verifyAddGroupForDownloadWritesMetadata(testKey, dataFileGroup, CURRENT_TIMESTAMP);

    assertLoggedNewConfigs(
        fakeEventLogger, createFileGroupDetails(dataFileGroup).clearFileCount().build(), null);
  }

  @Test
  public void testAddGroupForDownload_groupUpdated_whenBuildChanges() throws Exception {
    FakeEventLogger fakeEventLogger = new FakeEventLogger();
    resetFileGroupManager(fakeEventLogger, fileGroupsMetadata, sharedFileManager);

    DataFileGroupInternal dataFileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup).get()).isTrue();
    verifyAddGroupForDownloadWritesMetadata(testKey, dataFileGroup, CURRENT_TIMESTAMP);

    // Reset to clear events before next add group call
    fakeEventLogger.reset();

    // Update the file id and see that the group gets updated in the pending groups list.
    dataFileGroup = dataFileGroup.toBuilder().setBuildId(123456789L).build();
    assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup).get()).isTrue();
    verifyAddGroupForDownloadWritesMetadata(testKey, dataFileGroup, CURRENT_TIMESTAMP);

    assertLoggedNewConfigs(
        fakeEventLogger, createFileGroupDetails(dataFileGroup).clearFileCount().build(), null);
  }

  @Test
  public void testAddGroupForDownload_groupUpdated_whenVariantChanges() throws Exception {
    FakeEventLogger fakeEventLogger = new FakeEventLogger();
    resetFileGroupManager(fakeEventLogger, fileGroupsMetadata, sharedFileManager);

    DataFileGroupInternal dataFileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup).get()).isTrue();
    verifyAddGroupForDownloadWritesMetadata(testKey, dataFileGroup, CURRENT_TIMESTAMP);

    // Reset to clear events before next add group call
    fakeEventLogger.reset();

    // Update the file id and see that the group gets updated in the pending groups list.
    dataFileGroup = dataFileGroup.toBuilder().setVariantId("some-different-variant").build();
    assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup).get()).isTrue();
    verifyAddGroupForDownloadWritesMetadata(testKey, dataFileGroup, CURRENT_TIMESTAMP);

    assertLoggedNewConfigs(
        fakeEventLogger, createFileGroupDetails(dataFileGroup).clearFileCount().build(), null);
  }

  @Test
  public void testAddGroupForDownloadWithSyncId_failedToUpdateMetadataNoScheduleViaSpe()
      throws Exception {
    // Mock FileGroupsMetadata and SharedFileManager to test failure scenario.
    resetFileGroupManager(mockFileGroupsMetadata, mockSharedFileManager);
    DataFileGroupInternal dataFileGroup =
        MddTestUtil.createDataFileGroupInternalWithDownloadId(TEST_GROUP, 2);
    NewFileKey[] groupKeys = MddTestUtil.createFileKeysForDataFileGroupInternal(dataFileGroup);

    when(mockSharedFileManager.reserveFileEntry(any(NewFileKey.class)))
        .thenReturn(Futures.immediateFuture(true));

    // Failed to write to Metadata, no task will be scheduled via SPE.
    when(mockFileGroupsMetadata.write(any(GroupKey.class), any(DataFileGroupInternal.class)))
        .thenReturn(Futures.immediateFuture(false));
    when(mockFileGroupsMetadata.read(any(GroupKey.class)))
        .thenReturn(Futures.immediateFuture(null));

    ListenableFuture<Boolean> addGroupFuture =
        fileGroupManager.addGroupForDownload(testKey, dataFileGroup);
    assertThrows(ExecutionException.class, addGroupFuture::get);
    IOException e = LabsFutures.getFailureCauseAs(addGroupFuture, IOException.class);
    assertThat(e).hasMessageThat().contains("Failed to commit new group metadata to disk.");

    // Check that downloaded file groups doesn't contain this file group.
    GroupKey downloadedkey = testKey.toBuilder().setDownloaded(true).build();
    assertWithMessage(String.format("Expected that key %s should not exist.", downloadedkey))
        .that(mockFileGroupsMetadata.read(downloadedkey).get())
        .isNull();
    // Check that the get method doesn't return this file group.
    assertThat(fileGroupManager.getFileGroup(testKey, true).get()).isNull();

    verify(mockSharedFileManager).reserveFileEntry(groupKeys[0]);
    verify(mockSharedFileManager).reserveFileEntry(groupKeys[1]);
  }

  @Test
  public void testAddGroupForDownload_duplicatePendingGroup() throws Exception {
    DataFileGroupInternal dataFileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);

    assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup).get()).isTrue();
    verifyAddGroupForDownloadWritesMetadata(testKey, dataFileGroup, CURRENT_TIMESTAMP);

    // Send the exact same group again, and check that it is considered duplicate.
    assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup).get()).isFalse();
  }

  @Test
  public void testAddGroupForDownload_duplicateDownloadedGroup() throws Exception {
    DataFileGroupInternal dataFileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    writeDownloadedFileGroup(testKey, dataFileGroup);

    // Send the exact same group as the downloaded group, and check that it is considered duplicate.
    assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup).get()).isFalse();

    verifyNoInteractions(mockLogger);
  }

  @Test
  public void testAddGroupForDownload_filePropertiesUpdated() throws Exception {
    DataFileGroupInternal dataFileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    writePendingFileGroup(testKey, dataFileGroup);

    dataFileGroup =
        dataFileGroup.toBuilder()
            .setFile(0, dataFileGroup.getFile(0).toBuilder().setUrlToDownload("https://file2"))
            .build();
    // Send the same group with different property, and check that it is NOT duplicate.
    assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup).get()).isTrue();

    dataFileGroup =
        dataFileGroup.toBuilder()
            .setFile(0, dataFileGroup.getFile(0).toBuilder().setUrlToDownload("https://file3"))
            .build();
    assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup).get()).isTrue();
  }

  @Test
  public void testAddGroupForDownload_differentPendingGroup_duplicateDownloadedGroup()
      throws Exception {

    DataFileGroupInternal firstGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    assertThat(fileGroupManager.addGroupForDownload(testKey, firstGroup).get()).isTrue();
    verifyAddGroupForDownloadWritesMetadata(testKey, firstGroup, CURRENT_TIMESTAMP);

    verify(mockLogger)
        .logNewConfigReceived(createFileGroupDetails(firstGroup).clearFileCount().build(), null);
    reset(mockLogger);

    // Create a second group that is identical except for one different file id.
    DataFileGroupInternal.Builder secondGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2).toBuilder();
    secondGroup.setFile(0, secondGroup.getFile(0).toBuilder().setFileId("file2"));
    writeDownloadedFileGroup(testKey, secondGroup.build());

    // Send the updated group, and check that it is not considered duplicate.
    assertThat(fileGroupManager.addGroupForDownload(testKey, secondGroup.build()).get()).isTrue();
    verifyAddGroupForDownloadWritesMetadata(testKey, secondGroup.build(), CURRENT_TIMESTAMP);
    verify(mockLogger)
        .logNewConfigReceived(createFileGroupDetails(firstGroup).clearFileCount().build(), null);
  }

  @Test
  public void testAddGroupForDownload_subscribeFailed() throws Exception {
    // Mock SharedFileManager to test failure scenario.
    resetFileGroupManager(fileGroupsMetadata, mockSharedFileManager);
    DataFileGroupInternal dataFileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 3);
    NewFileKey[] groupKeys = MddTestUtil.createFileKeysForDataFileGroupInternal(dataFileGroup);

    ArgumentCaptor<NewFileKey> fileCaptor = ArgumentCaptor.forClass(NewFileKey.class);
    when(mockSharedFileManager.reserveFileEntry(fileCaptor.capture()))
        .thenReturn(
            Futures.immediateFuture(true),
            Futures.immediateFuture(false),
            Futures.immediateFuture(true));
    ExecutionException ex =
        assertThrows(
            ExecutionException.class,
            fileGroupManager.addGroupForDownload(testKey, dataFileGroup)::get);
    assertThat(ex).hasCauseThat().isInstanceOf(IOException.class);

    // Verify that we tried to subscribe to only the first 2 files.
    assertThat(fileCaptor.getAllValues()).containsExactly(groupKeys[0], groupKeys[1]);

    verify(mockLogger)
        .logNewConfigReceived(createFileGroupDetails(dataFileGroup).clearFileCount().build(), null);
  }

  @Test
  public void testAddGroupForDownload_subscribeFailed_firstFile() throws Exception {
    // Mock SharedFileManager to test failure scenario.
    resetFileGroupManager(fileGroupsMetadata, mockSharedFileManager);
    DataFileGroupInternal dataFileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 3);
    NewFileKey[] groupKeys = MddTestUtil.createFileKeysForDataFileGroupInternal(dataFileGroup);

    ArgumentCaptor<NewFileKey> fileCaptor = ArgumentCaptor.forClass(NewFileKey.class);
    when(mockSharedFileManager.reserveFileEntry(fileCaptor.capture()))
        .thenReturn(
            Futures.immediateFuture(false),
            Futures.immediateFuture(true),
            Futures.immediateFuture(true));
    ExecutionException ex =
        assertThrows(
            ExecutionException.class,
            fileGroupManager.addGroupForDownload(testKey, dataFileGroup)::get);
    assertThat(ex).hasCauseThat().isInstanceOf(IOException.class);

    // Verify that we tried to subscribe to only the first file.
    assertThat(fileCaptor.getAllValues()).containsExactly(groupKeys[0]);

    verify(mockLogger)
        .logNewConfigReceived(createFileGroupDetails(dataFileGroup).clearFileCount().build(), null);
  }

  @Test
  public void testAddGroupForDownload_alreadyDownloadedGroup() throws Exception {
    // Write a group to the pending shared prefs.
    DataFileGroupInternal pendingGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    writePendingFileGroup(testKey, pendingGroup);

    DataFileGroupInternal oldDownloadedGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1);
    writeDownloadedFileGroup(testKey, oldDownloadedGroup);

    // Add a newer version of that group
    DataFileGroupInternal receivedGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 3);

    assertThat(fileGroupManager.addGroupForDownload(testKey, receivedGroup).get()).isTrue();

    // The new added group should be the pending group.
    verifyAddGroupForDownloadWritesMetadata(testKey, receivedGroup, CURRENT_TIMESTAMP);
    assertThat(oldDownloadedGroup).isEqualTo(readDownloadedFileGroup(testKey));
  }

  @Test
  public void testAddGroupForDownload_addEmptyGroup() throws Exception {
    // Write a group to the pending shared prefs.
    DataFileGroupInternal pendingGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    writePendingFileGroup(testKey, pendingGroup);

    DataFileGroupInternal emptyGroup =
        DataFileGroupInternal.newBuilder().setGroupName(TEST_GROUP).build();
    assertThat(fileGroupManager.addGroupForDownload(testKey, emptyGroup).get()).isTrue();
    verifyAddGroupForDownloadWritesMetadata(testKey, emptyGroup, CURRENT_TIMESTAMP);
  }

  @Test
  public void testAddGroupForDownload_addGroupForUninstalledApp() throws Exception {
    DataFileGroupInternal dataFileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    GroupKey uninstalledAppKey =
        GroupKey.newBuilder().setGroupName(TEST_GROUP).setOwnerPackage("not.installed.app").build();

    // Send a group with an owner package that is not installed. Ensure that this group is rejected.
    assertThrows(
        UninstalledAppException.class,
        () -> fileGroupManager.addGroupForDownload(uninstalledAppKey, dataFileGroup));
    verify(mockLogger)
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            TEST_GROUP,
            /* fileGroupVersionNumber= */ 0,
            /* buildId= */ 0,
            /* variantId= */ "");
    verifyNoMoreInteractions(mockLogger);
  }

  @Test
  public void testAddGroupForDownload_expiredGroup() throws Exception {
    Calendar date = new Calendar.Builder().setDate(1970, Calendar.JANUARY, 2).build();
    DataFileGroupInternal dataFileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2).toBuilder()
            .setExpirationDateSecs(date.getTimeInMillis() / 1000)
            .build();

    testClock.set(System.currentTimeMillis());

    // Send a group with an expiration date that has already passed.
    assertThrows(
        ExpiredFileGroupException.class,
        () -> fileGroupManager.addGroupForDownload(testKey, dataFileGroup));
    verify(mockLogger)
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            TEST_GROUP,
            /* fileGroupVersionNumber= */ 0,
            /* buildId= */ 0,
            /* variantId= */ "");
    verifyNoMoreInteractions(mockLogger);
  }

  @Test
  public void testAddGroupForDownload_justExpiredGroup() throws Exception {
    long oneHourAgo = (System.currentTimeMillis() - TimeUnit.HOURS.toMillis(1)) / 1000;
    DataFileGroupInternal dataFileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2).toBuilder()
            .setExpirationDateSecs(oneHourAgo)
            .build();

    testClock.set(System.currentTimeMillis());

    // Send a group with an expiration date that has already passed.
    assertThrows(
        ExpiredFileGroupException.class,
        () -> fileGroupManager.addGroupForDownload(testKey, dataFileGroup));
    verify(mockLogger)
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            TEST_GROUP,
            /* fileGroupVersionNumber= */ 0,
            /* buildId= */ 0,
            /* variantId= */ "");
    verifyNoMoreInteractions(mockLogger);
  }

  @Test
  public void testAddGroupForDownload_nonexpiredGroup() throws Exception {
    DataFileGroupInternal dataFileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    NewFileKey[] groupKeys = MddTestUtil.createFileKeysForDataFileGroupInternal(dataFileGroup);

    long tenDaysFromNow = (System.currentTimeMillis() + TimeUnit.DAYS.toMillis(10)) / 1000;
    dataFileGroup = dataFileGroup.toBuilder().setExpirationDateSecs(tenDaysFromNow).build();

    testClock.set(System.currentTimeMillis());

    assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup).get()).isTrue();
    verifyAddGroupForDownloadWritesMetadata(testKey, dataFileGroup, testClock.currentTimeMillis());

    // Check that downloaded file groups doesn't contain this file group.
    assertThat(readDownloadedFileGroup(testKey)).isNull();
    // Check that the get method doesn't return this file group.
    assertThat(fileGroupManager.getFileGroup(testKey, true).get()).isNull();

    assertThat(sharedFileManager.getSharedFile(groupKeys[0]).get()).isNotNull();
    assertThat(sharedFileManager.getSharedFile(groupKeys[1]).get()).isNotNull();
    verify(mockLogger)
        .logNewConfigReceived(createFileGroupDetails(dataFileGroup).clearFileCount().build(), null);
  }

  @Test
  public void testAddGroupForDownload_nonexpiredGroupNoExpiration() throws Exception {
    DataFileGroupInternal.Builder dataFileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2).toBuilder();
    NewFileKey[] groupKeys =
        MddTestUtil.createFileKeysForDataFileGroupInternal(dataFileGroup.build());

    dataFileGroup.setExpirationDateSecs(0); // 0 means don't expire

    testClock.set(System.currentTimeMillis());

    assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup.build()).get()).isTrue();
    verifyAddGroupForDownloadWritesMetadata(
        testKey, dataFileGroup.build(), testClock.currentTimeMillis());

    // Check that downloaded file groups doesn't contain this file group.
    assertThat(readDownloadedFileGroup(testKey)).isNull();
    // Check that the get method doesn't return this file group.
    assertThat(fileGroupManager.getFileGroup(testKey, true).get()).isNull();

    assertThat(sharedFileManager.getSharedFile(groupKeys[0]).get()).isNotNull();
    assertThat(sharedFileManager.getSharedFile(groupKeys[1]).get()).isNotNull();
    verify(mockLogger)
        .logNewConfigReceived(
            createFileGroupDetails(dataFileGroup.build()).clearFileCount().build(), null);
  }

  @Test
  public void testAddGroupForDownload_extendExpiration() throws Exception {
    DataFileGroupInternal.Builder dataFileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2).toBuilder();
    long tenDaysFromNow = (System.currentTimeMillis() + TimeUnit.DAYS.toMillis(10)) / 1000;
    dataFileGroup.setExpirationDateSecs(tenDaysFromNow);

    assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup.build()).get()).isTrue();
    verifyAddGroupForDownloadWritesMetadata(testKey, dataFileGroup.build(), CURRENT_TIMESTAMP);

    // Now send the group again with a longer expiration.
    long twentyDaysFromNow = tenDaysFromNow + TimeUnit.DAYS.toSeconds(10);
    dataFileGroup = dataFileGroup.setExpirationDateSecs(twentyDaysFromNow);

    assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup.build()).get()).isTrue();
    verifyAddGroupForDownloadWritesMetadata(testKey, dataFileGroup.build(), CURRENT_TIMESTAMP);
  }

  @Test
  public void testAddGroupForDownload_reduceExpiration() throws Exception {
    long tenDaysFromNow = (System.currentTimeMillis() + TimeUnit.DAYS.toMillis(10)) / 1000;
    DataFileGroupInternal dataFileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2).toBuilder()
            .setExpirationDateSecs(tenDaysFromNow)
            .build();

    assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup).get()).isTrue();
    verifyAddGroupForDownloadWritesMetadata(testKey, dataFileGroup, CURRENT_TIMESTAMP);

    // Now send the group again with a longer expiration.
    long fiveDaysFromNow = tenDaysFromNow - TimeUnit.DAYS.toSeconds(5);
    dataFileGroup = dataFileGroup.toBuilder().setExpirationDateSecs(fiveDaysFromNow).build();

    assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup).get()).isTrue();
    verifyAddGroupForDownloadWritesMetadata(testKey, dataFileGroup, CURRENT_TIMESTAMP);
  }

  @Test
  public void testAddGroupForDownload_delayedDownload() throws Exception {
    flags.enableDelayedDownload = Optional.of(true);

    // Create 2 groups, one of which requires device side activation.
    DataFileGroupInternal fileGroup1 =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2).toBuilder()
            .setDownloadConditions(
                DownloadConditions.newBuilder()
                    .setActivatingCondition(ActivatingCondition.DEVICE_ACTIVATED))
            .build();

    DataFileGroupInternal fileGroup2 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP_2, 3);

    // Assert that adding the first group throws an exception.
    ExecutionException ex =
        assertThrows(
            ExecutionException.class,
            fileGroupManager.addGroupForDownload(testKey, fileGroup1)::get);
    assertThat(ex).hasCauseThat().isInstanceOf(ActivationRequiredForGroupException.class);
    assertThat(fileGroupManager.addGroupForDownload(testKey2, fileGroup2).get()).isTrue();

    // Now activate the group and verify that we are able to add the first group.
    assertThat(fileGroupManager.setGroupActivation(testKey, true).get()).isTrue();
    assertThat(fileGroupManager.addGroupForDownload(testKey, fileGroup1).get()).isTrue();

    // Deactivate the group again and verify that we should no longer be able to add it.
    assertThat(fileGroupManager.setGroupActivation(testKey, false).get()).isTrue();
    ex =
        assertThrows(
            ExecutionException.class,
            fileGroupManager.addGroupForDownload(testKey, fileGroup1)::get);
    assertThat(ex).hasCauseThat().isInstanceOf(ActivationRequiredForGroupException.class);
  }

  @Test
  public void testAddGroupForDownload_onWifiFirst() throws Exception {
    int elapsedTime = 1000;
    DataFileGroupInternal.Builder dataFileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2).toBuilder();

    {
      testClock.set(elapsedTime);
      assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup.build()).get())
          .isTrue();
      // The wifi only download timestamp is set correctly.
      verifyAddGroupForDownloadWritesMetadata(testKey, dataFileGroup.build(), elapsedTime);
    }

    {
      // Update metadata does not change the wifi only download timestamp.
      long tenDaysFromNow = (System.currentTimeMillis() + TimeUnit.DAYS.toMillis(10)) / 1000;
      dataFileGroup.setExpirationDateSecs(tenDaysFromNow);
      assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup.build()).get())
          .isTrue();
      verifyAddGroupForDownloadWritesMetadata(testKey, dataFileGroup.build(), elapsedTime);
    }

    {
      // Change another metadata field does not change the wifi only download timestamp.
      dataFileGroup.setFileGroupVersionNumber(2);
      assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup.build()).get())
          .isTrue();
      // The wifi only download timestamp does not change.
      verifyAddGroupForDownloadWritesMetadata(testKey, dataFileGroup.build(), elapsedTime);
    }

    {
      // Update the file's urlToDownload will reset the wifi only download timestamp.
      elapsedTime = 2000;
      testClock.set(elapsedTime);
      dataFileGroup.setFile(
          0, dataFileGroup.getFile(0).toBuilder().setUrlToDownload("https://new_url"));
      assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup.build()).get())
          .isTrue();
      // The wifi only download timestamp change since we change the urlToDownload
      verifyAddGroupForDownloadWritesMetadata(testKey, dataFileGroup.build(), elapsedTime);
    }

    {
      // Update the file's byteSize will reset the wifi only download timestamp.
      elapsedTime = 3000;
      testClock.set(elapsedTime);
      dataFileGroup.setFile(1, dataFileGroup.getFile(1).toBuilder().setByteSize(5001));
      assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup.build()).get())
          .isTrue();
      // The wifi only download timestamp change since we change the urlToDownload
      verifyAddGroupForDownloadWritesMetadata(testKey, dataFileGroup.build(), elapsedTime);
    }

    {
      // Update the file's checksum will reset the wifi only download timestamp.
      elapsedTime = 4000;
      testClock.set(elapsedTime);
      dataFileGroup.setFile(1, dataFileGroup.getFile(1).toBuilder().setChecksum("new check sum"));
      assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup.build()).get())
          .isTrue();
      // The wifi only download timestamp change since we change the urlToDownload
      verifyAddGroupForDownloadWritesMetadata(testKey, dataFileGroup.build(), elapsedTime);
    }
  }

  @Test
  public void testAddGroupForDownload_addsSideloadedGroup() throws Exception {
    // Create sideloaded group
    DataFileGroupInternal sideloadedGroup =
        DataFileGroupInternal.newBuilder()
            .setGroupName(TEST_GROUP)
            .addFile(
                DataFile.newBuilder()
                    .setFileId("sideloaded_file")
                    .setUrlToDownload("file:/test")
                    .setChecksumType(DataFile.ChecksumType.NONE)
                    .build())
            .build();

    assertThat(fileGroupManager.addGroupForDownload(testKey, sideloadedGroup).get()).isTrue();

    verifyAddGroupForDownloadWritesMetadata(testKey, sideloadedGroup, 1000L);
  }

  @Test
  public void testAddGroupForDownload_multipleVariants() throws Exception {
    // Create 3 group keys of the same group, but with different variants
    GroupKey defaultGroupKey =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP)
            .setOwnerPackage(context.getPackageName())
            .build();
    GroupKey enGroupKey = defaultGroupKey.toBuilder().setVariantId("en").build();
    GroupKey frGroupKey = defaultGroupKey.toBuilder().setVariantId("fr").build();

    DataFileGroupInternal defaultFileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1);
    DataFileGroupInternal enFileGroup = defaultFileGroup.toBuilder().setVariantId("en").build();
    DataFileGroupInternal frFileGroup = defaultFileGroup.toBuilder().setVariantId("fr").build();

    assertThat(fileGroupManager.addGroupForDownload(defaultGroupKey, defaultFileGroup).get())
        .isTrue();
    assertThat(fileGroupManager.addGroupForDownload(enGroupKey, enFileGroup).get()).isTrue();
    assertThat(fileGroupManager.addGroupForDownload(frGroupKey, frFileGroup).get()).isTrue();

    assertThat(fileGroupsMetadata.getAllGroupKeys().get())
        .comparingElementsUsing(GROUP_KEY_TO_VARIANT)
        .containsExactly("", "en", "fr");
  }

  @Test
  public void removeFileGroup_noVersionExists() throws Exception {
    // No record for both pending key and downloaded key.
    GroupKey groupKey =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP)
            .setOwnerPackage(context.getPackageName())
            .build();

    fileGroupManager.removeFileGroup(groupKey, /* pendingOnly= */ false).get();

    assertThat(fileGroupsMetadata.getAllStaleGroups().get()).isEmpty();
    assertThat(fileGroupsMetadata.getAllGroupKeys().get()).isEmpty();

    // There is no pending file group, so no call to clearSyncReasons.
    verifyNoInteractions(mockLogger);
  }

  @Test
  public void removeFileGroup_pendingVersionExists() throws Exception {
    DataFile dataFile1 = DataFile.newBuilder().setFileId("file1").setChecksum("checksum1").build();
    DataFile dataFile2 = DataFile.newBuilder().setFileId("file2").setChecksum("checksum2").build();

    NewFileKey newFileKey1 =
        SharedFilesMetadata.createKeyFromDataFile(dataFile1, AllowedReaders.ALL_GOOGLE_APPS);
    NewFileKey newFileKey2 =
        SharedFilesMetadata.createKeyFromDataFile(dataFile2, AllowedReaders.ALL_GOOGLE_APPS);

    DataFileGroupInternal pendingFileGroup =
        DataFileGroupInternal.newBuilder()
            .setGroupName(TEST_GROUP)
            .setFileGroupVersionNumber(1)
            .setAllowedReadersEnum(AllowedReaders.ALL_GOOGLE_APPS)
            .addAllFile(Lists.newArrayList(dataFile1, dataFile2))
            .build();

    GroupKey groupKey =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP)
            .setOwnerPackage(context.getPackageName())
            .build();
    GroupKey pendingGroupKey = groupKey.toBuilder().setDownloaded(false).build();
    GroupKey downloadedGroupKey = groupKey.toBuilder().setDownloaded(true).build();

    writePendingFileGroup(pendingGroupKey, pendingFileGroup);
    writeSharedFiles(
        sharedFilesMetadata,
        pendingFileGroup,
        ImmutableList.of(FileStatus.DOWNLOAD_IN_PROGRESS, FileStatus.DOWNLOAD_IN_PROGRESS));

    fileGroupManager.removeFileGroup(groupKey, /* pendingOnly= */ false).get();

    assertThat(readPendingFileGroup(pendingGroupKey)).isNull();
    assertThat(readPendingFileGroup(downloadedGroupKey)).isNull();
    assertThat(fileGroupsMetadata.getAllGroupKeys().get()).isEmpty();

    Uri pendingFileUri1 =
        DirectoryUtil.getOnDeviceUri(
            context,
            newFileKey1.getAllowedReaders(),
            dataFile1.getFileId(),
            newFileKey1.getChecksum(),
            mockSilentFeedback,
            /* instanceId= */ Optional.absent(),
            /* androidShared= */ false);
    Uri pendingFileUri2 =
        DirectoryUtil.getOnDeviceUri(
            context,
            newFileKey2.getAllowedReaders(),
            dataFile2.getFileId(),
            newFileKey2.getChecksum(),
            mockSilentFeedback,
            /* instanceId= */ Optional.absent(),
            /* androidShared= */ false);

    verify(mockDownloader).stopDownloading(newFileKey1.getChecksum(), pendingFileUri1);
    verify(mockDownloader).stopDownloading(newFileKey2.getChecksum(), pendingFileUri2);

    verifyNoInteractions(mockLogger);
  }

  @Test
  public void removeFileGroup_downloadedVersionExists() throws Exception {
    DataFile dataFile1 = DataFile.newBuilder().setFileId("file1").setChecksum("checksum1").build();
    DataFile dataFile2 = DataFile.newBuilder().setFileId("file2").setChecksum("checksum2").build();

    DataFileGroupInternal downloadedFileGroup =
        DataFileGroupInternal.newBuilder()
            .setGroupName(TEST_GROUP)
            .setFileGroupVersionNumber(0)
            .setBuildId(0)
            .setVariantId("")
            .setAllowedReadersEnum(AllowedReaders.ALL_GOOGLE_APPS)
            .addAllFile(Lists.newArrayList(dataFile1, dataFile2))
            .build();

    GroupKey groupKey =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP)
            .setOwnerPackage(context.getPackageName())
            .build();
    GroupKey pendingGroupKey = groupKey.toBuilder().setDownloaded(false).build();
    GroupKey downloadedGroupKey = groupKey.toBuilder().setDownloaded(true).build();

    writeDownloadedFileGroup(downloadedGroupKey, downloadedFileGroup);
    writeSharedFiles(
        sharedFilesMetadata,
        downloadedFileGroup,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE, FileStatus.DOWNLOAD_COMPLETE));

    fileGroupManager.removeFileGroup(groupKey, /* pendingOnly= */ false).get();

    assertThat(readPendingFileGroup(pendingGroupKey)).isNull();
    assertThat(readPendingFileGroup(downloadedGroupKey)).isNull();
    assertThat(fileGroupsMetadata.getAllStaleGroups().get())
        .containsExactly(
            downloadedFileGroup.toBuilder()
                .setBookkeeping(
                    downloadedFileGroup.getBookkeeping().toBuilder()
                        .setStaleExpirationDate(1)
                        .build())
                .build());

    verify(mockDownloader, never()).stopDownloading(any(String.class), any(Uri.class));

    verifyNoInteractions(mockLogger);
  }

  @Test
  public void removeFileGroup_bothVersionsExist() throws Exception {
    DataFile registeredFile =
        DataFile.newBuilder().setFileId("file").setChecksum("registered").build();
    DataFile downloadedFile =
        DataFile.newBuilder().setFileId("file").setChecksum("downloaded").build();

    NewFileKey registeredFileKey =
        SharedFilesMetadata.createKeyFromDataFile(registeredFile, AllowedReaders.ALL_GOOGLE_APPS);

    DataFileGroupInternal pendingFileGroup =
        DataFileGroupInternal.newBuilder()
            .setGroupName(TEST_GROUP)
            .setFileGroupVersionNumber(1)
            .setAllowedReadersEnum(AllowedReaders.ALL_GOOGLE_APPS)
            .addFile(registeredFile)
            .build();
    DataFileGroupInternal downloadedFileGroup =
        DataFileGroupInternal.newBuilder()
            .setGroupName(TEST_GROUP)
            .setFileGroupVersionNumber(0)
            .setBuildId(0)
            .setVariantId("")
            .setAllowedReadersEnum(AllowedReaders.ALL_GOOGLE_APPS)
            .addFile(downloadedFile)
            .build();

    GroupKey groupKey =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP)
            .setOwnerPackage(context.getPackageName())
            .build();
    GroupKey pendingGroupKey = groupKey.toBuilder().setDownloaded(false).build();
    GroupKey downloadedGroupKey = groupKey.toBuilder().setDownloaded(true).build();

    writePendingFileGroup(pendingGroupKey, pendingFileGroup);
    writeDownloadedFileGroup(downloadedGroupKey, downloadedFileGroup);
    writeSharedFiles(
        sharedFilesMetadata, pendingFileGroup, ImmutableList.of(FileStatus.DOWNLOAD_IN_PROGRESS));
    writeSharedFiles(
        sharedFilesMetadata, downloadedFileGroup, ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE));

    fileGroupManager.removeFileGroup(groupKey, /* pendingOnly= */ false).get();

    assertThat(readPendingFileGroup(pendingGroupKey)).isNull();
    assertThat(readPendingFileGroup(downloadedGroupKey)).isNull();
    assertThat(fileGroupsMetadata.getAllStaleGroups().get())
        .containsExactly(
            downloadedFileGroup.toBuilder()
                .setBookkeeping(
                    downloadedFileGroup.getBookkeeping().toBuilder()
                        .setStaleExpirationDate(1)
                        .build())
                .build());

    Uri pendingFileUri =
        DirectoryUtil.getOnDeviceUri(
            context,
            registeredFileKey.getAllowedReaders(),
            registeredFile.getFileId(),
            registeredFileKey.getChecksum(),
            mockSilentFeedback,
            /* instanceId= */ Optional.absent(),
            /* androidShared= */ false);

    // Only called once to stop download of pending file.
    verify(mockDownloader).stopDownloading(registeredFileKey.getChecksum(), pendingFileUri);

    verifyNoInteractions(mockLogger);
  }

  @Test
  public void removeFileGroup_bothVersionsExist_onlyRemovePending() throws Exception {
    DataFile registeredFile =
        DataFile.newBuilder().setFileId("file").setChecksum("registered").build();
    DataFile downloadedFile =
        DataFile.newBuilder().setFileId("file").setChecksum("downloaded").build();

    NewFileKey registeredFileKey =
        SharedFilesMetadata.createKeyFromDataFile(registeredFile, AllowedReaders.ALL_GOOGLE_APPS);

    DataFileGroupInternal pendingFileGroup =
        DataFileGroupInternal.newBuilder()
            .setGroupName(TEST_GROUP)
            .setFileGroupVersionNumber(1)
            .setAllowedReadersEnum(AllowedReaders.ALL_GOOGLE_APPS)
            .addFile(registeredFile)
            .build();
    DataFileGroupInternal downloadedFileGroup =
        DataFileGroupInternal.newBuilder()
            .setGroupName(TEST_GROUP)
            .setFileGroupVersionNumber(0)
            .setBuildId(0)
            .setVariantId("")
            .setAllowedReadersEnum(AllowedReaders.ALL_GOOGLE_APPS)
            .addFile(downloadedFile)
            .build();

    GroupKey groupKey =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP)
            .setOwnerPackage(context.getPackageName())
            .build();
    GroupKey pendingGroupKey = groupKey.toBuilder().setDownloaded(false).build();
    GroupKey downloadedGroupKey = groupKey.toBuilder().setDownloaded(true).build();

    writePendingFileGroup(pendingGroupKey, pendingFileGroup);
    writeDownloadedFileGroup(downloadedGroupKey, downloadedFileGroup);
    writeSharedFiles(
        sharedFilesMetadata, pendingFileGroup, ImmutableList.of(FileStatus.DOWNLOAD_IN_PROGRESS));
    writeSharedFiles(
        sharedFilesMetadata, downloadedFileGroup, ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE));

    fileGroupManager.removeFileGroup(groupKey, /* pendingOnly= */ true).get();

    assertThat(readPendingFileGroup(pendingGroupKey)).isNull();
    assertThat(readPendingFileGroup(downloadedGroupKey)).isNull();

    // Pending group was just removed, and downloaded was not added to stale groups.
    assertThat(fileGroupsMetadata.getAllStaleGroups().get()).isEmpty();
    // Downloaded group is still available.
    assertThat(fileGroupsMetadata.getAllFreshGroups().get())
        .containsExactly(GroupKeyAndGroup.create(downloadedGroupKey, downloadedFileGroup));

    Uri pendingFileUri =
        DirectoryUtil.getOnDeviceUri(
            context,
            registeredFileKey.getAllowedReaders(),
            registeredFile.getFileId(),
            registeredFileKey.getChecksum(),
            mockSilentFeedback,
            /* instanceId= */ Optional.absent(),
            /* androidShared= */ false);

    // Only called once to stop download of pending file.
    verify(mockDownloader).stopDownloading(registeredFileKey.getChecksum(), pendingFileUri);

    verifyNoInteractions(mockLogger);
  }

  @Test
  public void removeFileGroup_fileReferencedByOtherFileGroup_willNotCancelDownload()
      throws Exception {
    DataFile dataFile1 = DataFile.newBuilder().setFileId("file1").setChecksum("checksum1").build();
    DataFile dataFile2 = DataFile.newBuilder().setFileId("file2").setChecksum("checksum2").build();

    DataFileGroupInternal pendingFileGroup =
        DataFileGroupInternal.newBuilder()
            .setGroupName(TEST_GROUP)
            .setFileGroupVersionNumber(1)
            .setAllowedReadersEnum(AllowedReaders.ALL_GOOGLE_APPS)
            .addAllFile(Lists.newArrayList(dataFile1, dataFile2))
            .build();

    DataFileGroupInternal pendingFileGroup2 =
        DataFileGroupInternal.newBuilder()
            .setGroupName(TEST_GROUP_2)
            .setFileGroupVersionNumber(1)
            .setAllowedReadersEnum(AllowedReaders.ALL_GOOGLE_APPS)
            .addAllFile(Lists.newArrayList(dataFile1, dataFile2))
            .build();

    GroupKey groupKey =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP)
            .setOwnerPackage(context.getPackageName())
            .build();
    GroupKey pendingGroupKey = groupKey.toBuilder().setDownloaded(false).build();
    GroupKey downloadedGroupKey = groupKey.toBuilder().setDownloaded(true).build();

    GroupKey groupKey2 =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP_2)
            .setOwnerPackage(context.getPackageName())
            .build();
    GroupKey pendingGroupKey2 = groupKey2.toBuilder().setDownloaded(false).build();

    writePendingFileGroup(pendingGroupKey, pendingFileGroup);
    writePendingFileGroup(pendingGroupKey2, pendingFileGroup2);
    writeSharedFiles(
        sharedFilesMetadata,
        pendingFileGroup,
        ImmutableList.of(FileStatus.DOWNLOAD_IN_PROGRESS, FileStatus.DOWNLOAD_IN_PROGRESS));
    writeSharedFiles(
        sharedFilesMetadata,
        pendingFileGroup2,
        ImmutableList.of(FileStatus.DOWNLOAD_IN_PROGRESS, FileStatus.DOWNLOAD_IN_PROGRESS));
    assertThat(fileGroupsMetadata.getAllFreshGroups().get())
        .containsExactly(
            GroupKeyAndGroup.create(pendingGroupKey, pendingFileGroup),
            GroupKeyAndGroup.create(pendingGroupKey2, pendingFileGroup2));

    fileGroupManager.removeFileGroup(groupKey, /* pendingOnly= */ false).get();

    assertThat(readPendingFileGroup(pendingGroupKey)).isNull();
    assertThat(readPendingFileGroup(pendingGroupKey2)).isNotNull();
    assertThat(readPendingFileGroup(downloadedGroupKey)).isNull();
    assertThat(fileGroupsMetadata.getAllStaleGroups().get()).isEmpty();
    assertThat(fileGroupsMetadata.getAllFreshGroups().get())
        .containsExactly(GroupKeyAndGroup.create(pendingGroupKey2, pendingFileGroup2));

    verify(mockDownloader, never()).stopDownloading(any(String.class), any(Uri.class));

    verifyNoInteractions(mockLogger);
  }

  @Test
  public void removeFileGroup_onFailure() throws Exception {
    // Mock FileGroupsMetadata to test failure scenario.
    resetFileGroupManager(mockFileGroupsMetadata, sharedFileManager);
    DataFileGroupInternal pendingFileGroup =
        DataFileGroupInternal.newBuilder()
            .setGroupName(TEST_GROUP)
            .setFileGroupVersionNumber(1)
            .build();
    DataFileGroupInternal downloadedFileGroup =
        DataFileGroupInternal.newBuilder()
            .setGroupName(TEST_GROUP)
            .setFileGroupVersionNumber(0)
            .setBuildId(0)
            .setVariantId("")
            .build();

    GroupKey groupKey =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP)
            .setOwnerPackage(context.getPackageName())
            .build();
    GroupKey pendingGroupKey = groupKey.toBuilder().setDownloaded(false).build();
    GroupKey downloadedGroupKey = groupKey.toBuilder().setDownloaded(true).build();

    when(mockFileGroupsMetadata.read(pendingGroupKey))
        .thenReturn(Futures.immediateFuture(pendingFileGroup));
    when(mockFileGroupsMetadata.read(downloadedGroupKey))
        .thenReturn(Futures.immediateFuture(downloadedFileGroup));
    when(mockFileGroupsMetadata.remove(pendingGroupKey)).thenReturn(Futures.immediateFuture(true));
    when(mockFileGroupsMetadata.remove(downloadedGroupKey))
        .thenReturn(Futures.immediateFuture(false));

    // Exception should be thrown when fileGroupManager attempts to remove downloadedGroupKey.
    ExecutionException expected =
        assertThrows(
            ExecutionException.class,
            () -> fileGroupManager.removeFileGroup(groupKey, /* pendingOnly= */ false).get());
    assertThat(expected).hasCauseThat().isInstanceOf(IOException.class);

    verify(mockFileGroupsMetadata).remove(pendingGroupKey);
    verify(mockFileGroupsMetadata).remove(downloadedGroupKey);
    verify(mockFileGroupsMetadata, never()).addStaleGroup(any(DataFileGroupInternal.class));

    verify(mockLogger).logEventSampled(MddClientEvent.Code.EVENT_CODE_UNSPECIFIED);
  }

  @Test
  public void removeFileGroup_removesSideloadedGroup() throws Exception {
    // Create sideloaded group
    DataFileGroupInternal sideloadedGroup =
        DataFileGroupInternal.newBuilder()
            .setGroupName(TEST_GROUP)
            .addFile(
                DataFile.newBuilder()
                    .setFileId("sideloaded_file")
                    .setUrlToDownload("file:/test")
                    .setChecksumType(DataFile.ChecksumType.NONE)
                    .build())
            .build();

    writePendingFileGroup(testKey, sideloadedGroup);
    writeDownloadedFileGroup(testKey, sideloadedGroup);

    fileGroupManager.removeFileGroup(testKey, /* pendingOnly= */ false).get();

    assertThat(readPendingFileGroup(testKey)).isNull();
    assertThat(readDownloadedFileGroup(testKey)).isNull();
  }

  @Test
  public void
      removeFileGroup_whenMultipleVariantsExist_whenNoVariantSpecified_removesEmptyVariantGroup()
          throws Exception {
    // Create 3 variants of a group (default (no variant), en, fr) and have them all added. When
    // removeFileGroups is called and the group key given does not include a variant, ensure that
    // the default group is removed.

    GroupKey defaultGroupKey =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP)
            .setOwnerPackage(context.getPackageName())
            .build();
    GroupKey enGroupKey = defaultGroupKey.toBuilder().setVariantId("en").build();
    GroupKey frGroupKey = defaultGroupKey.toBuilder().setVariantId("fr").build();

    DataFileGroupInternal defaultFileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1);
    DataFileGroupInternal enFileGroup = defaultFileGroup.toBuilder().setVariantId("en").build();
    DataFileGroupInternal frFileGroup = defaultFileGroup.toBuilder().setVariantId("fr").build();

    writePendingFileGroup(getPendingKey(defaultGroupKey), defaultFileGroup);
    writePendingFileGroup(getPendingKey(enGroupKey), enFileGroup);
    writePendingFileGroup(getPendingKey(frGroupKey), frFileGroup);

    writeSharedFiles(
        sharedFilesMetadata, defaultFileGroup, ImmutableList.of(FileStatus.DOWNLOAD_IN_PROGRESS));
    writeSharedFiles(
        sharedFilesMetadata, enFileGroup, ImmutableList.of(FileStatus.DOWNLOAD_IN_PROGRESS));
    writeSharedFiles(
        sharedFilesMetadata, frFileGroup, ImmutableList.of(FileStatus.DOWNLOAD_IN_PROGRESS));

    // Assert that all file groups share the same file even through the variants are different
    assertThat(sharedFilesMetadata.getAllFileKeys().get()).hasSize(1);

    {
      // Perfrom removal once and check that the default group gets removed
      fileGroupManager.removeFileGroup(defaultGroupKey, /* pendingOnly= */ false).get();

      assertThat(fileGroupsMetadata.getAllGroupKeys().get())
          .comparingElementsUsing(GROUP_KEY_TO_VARIANT)
          .containsExactly("en", "fr");
      assertThat(fileGroupsMetadata.getAllFreshGroups().get())
          .comparingElementsUsing(KEY_GROUP_PAIR_TO_VARIANT)
          .containsExactly("en", "fr");

      assertThat(sharedFilesMetadata.getAllFileKeys().get()).hasSize(1);
    }

    {
      // Perform remove again and verify that there is no change in state
      fileGroupManager.removeFileGroup(defaultGroupKey, /* pendingOnly= */ false).get();

      assertThat(fileGroupsMetadata.getAllGroupKeys().get())
          .comparingElementsUsing(GROUP_KEY_TO_VARIANT)
          .containsExactly("en", "fr");
      assertThat(fileGroupsMetadata.getAllFreshGroups().get())
          .comparingElementsUsing(KEY_GROUP_PAIR_TO_VARIANT)
          .containsExactly("en", "fr");

      assertThat(sharedFilesMetadata.getAllFileKeys().get()).hasSize(1);
    }
  }

  @Test
  public void removeFileGroup_whenMultipleVariantsExist_whenVariantSpecified_removesVariantGroup()
      throws Exception {
    // Create 3 variants of a group (default (no variant), en, fr) and have them all added. When
    // removeFileGroups is called and the group key given includes a variant, ensure that only
    // the group with that variant is removed.

    GroupKey defaultGroupKey =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP)
            .setOwnerPackage(context.getPackageName())
            .build();
    GroupKey enGroupKey = defaultGroupKey.toBuilder().setVariantId("en").build();
    GroupKey frGroupKey = defaultGroupKey.toBuilder().setVariantId("fr").build();

    DataFileGroupInternal defaultFileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1);
    DataFileGroupInternal enFileGroup = defaultFileGroup.toBuilder().setVariantId("en").build();
    DataFileGroupInternal frFileGroup = defaultFileGroup.toBuilder().setVariantId("fr").build();

    writePendingFileGroup(getPendingKey(defaultGroupKey), defaultFileGroup);
    writePendingFileGroup(getPendingKey(enGroupKey), enFileGroup);
    writePendingFileGroup(getPendingKey(frGroupKey), frFileGroup);

    writeSharedFiles(
        sharedFilesMetadata, defaultFileGroup, ImmutableList.of(FileStatus.DOWNLOAD_IN_PROGRESS));
    writeSharedFiles(
        sharedFilesMetadata, enFileGroup, ImmutableList.of(FileStatus.DOWNLOAD_IN_PROGRESS));
    writeSharedFiles(
        sharedFilesMetadata, frFileGroup, ImmutableList.of(FileStatus.DOWNLOAD_IN_PROGRESS));

    // Assert that all file groups share the same file even through the variants are different
    assertThat(sharedFilesMetadata.getAllFileKeys().get()).hasSize(1);

    {
      // Perfrom removal once and check that the en group gets removed
      fileGroupManager.removeFileGroup(enGroupKey, /* pendingOnly= */ false).get();

      assertThat(fileGroupsMetadata.getAllGroupKeys().get())
          .comparingElementsUsing(GROUP_KEY_TO_VARIANT)
          .containsExactly("", "fr");
      assertThat(fileGroupsMetadata.getAllFreshGroups().get())
          .comparingElementsUsing(KEY_GROUP_PAIR_TO_VARIANT)
          .containsExactly("", "fr");

      assertThat(sharedFilesMetadata.getAllFileKeys().get()).hasSize(1);
    }

    {
      // Perform remove again and verify that there is no change in state
      fileGroupManager.removeFileGroup(enGroupKey, /* pendingOnly= */ false).get();

      assertThat(fileGroupsMetadata.getAllGroupKeys().get())
          .comparingElementsUsing(GROUP_KEY_TO_VARIANT)
          .containsExactly("", "fr");
      assertThat(fileGroupsMetadata.getAllFreshGroups().get())
          .comparingElementsUsing(KEY_GROUP_PAIR_TO_VARIANT)
          .containsExactly("", "fr");

      assertThat(sharedFilesMetadata.getAllFileKeys().get()).hasSize(1);
    }
  }

  @Test
  public void testRemoveFileGroups_whenNoGroupsExist_performsNoRemovals() throws Exception {
    GroupKey groupKey1 =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP)
            .setOwnerPackage(context.getPackageName())
            .build();
    GroupKey groupKey2 =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP_2)
            .setOwnerPackage(context.getPackageName())
            .build();

    fileGroupManager.removeFileGroups(ImmutableList.of(groupKey1, groupKey2)).get();

    assertThat(fileGroupsMetadata.getAllStaleGroups().get()).isEmpty();
    assertThat(fileGroupsMetadata.getAllGroupKeys().get()).isEmpty();
  }

  @Test
  public void testRemoveFileGroups_whenNoMatchingKeysExist_performsNoRemovals() throws Exception {
    // Create a pending and downloaded version of a file group
    // Pending group includes 2 files: 1 that is shared with downloaded group and one that will be
    // marked pending
    DataFileGroupInternal pendingFileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    DataFileGroupInternal downloadedFileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1);

    GroupKey.Builder groupKeyBuilder =
        GroupKey.newBuilder().setGroupName(TEST_GROUP).setOwnerPackage(context.getPackageName());
    GroupKey pendingGroupKey = groupKeyBuilder.setDownloaded(false).build();
    GroupKey downloadedGroupKey = groupKeyBuilder.setDownloaded(true).build();
    GroupKey nonMatchingGroupKey1 = groupKeyBuilder.setGroupName(TEST_GROUP_2).build();
    GroupKey nonMatchingGroupKey2 = groupKeyBuilder.setGroupName(TEST_GROUP_3).build();

    // Write file group and shared file metadata
    // NOTE: pending group contains all files in downloaded group, so we only need to write shared
    // file state once.
    writePendingFileGroup(pendingGroupKey, pendingFileGroup);
    writeDownloadedFileGroup(downloadedGroupKey, downloadedFileGroup);
    writeSharedFiles(
        sharedFilesMetadata,
        pendingFileGroup,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE, FileStatus.DOWNLOAD_IN_PROGRESS));

    fileGroupManager
        .removeFileGroups(ImmutableList.of(nonMatchingGroupKey1, nonMatchingGroupKey2))
        .get();

    assertThat(readPendingFileGroup(pendingGroupKey)).isNotNull();
    assertThat(readDownloadedFileGroup(downloadedGroupKey)).isNotNull();
    assertThat(fileGroupsMetadata.getAllFreshGroups().get()).hasSize(2);
    assertThat(fileGroupsMetadata.getAllStaleGroups().get()).isEmpty();

    verify(mockDownloader, times(0)).stopDownloading(any(), any());
  }

  @Test
  public void testRemoveFileGroups_whenMatchingPendingGroups_performsRemove() throws Exception {
    // Create 2 pending groups that will be removed, 1 pending group that shouldn't be removed, and
    // 1 downloaded group that shouldn't be removed
    DataFileGroupInternal pendingGroupToRemove1 =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1).toBuilder().build();
    DataFileGroupInternal pendingGroupToRemove2 =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP_2, 1);
    DataFileGroupInternal pendingGroupToKeep =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP_3, 1).toBuilder().build();
    DataFileGroupInternal downloadedGroupToKeep =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP_4, 1);

    GroupKey.Builder groupKeyBuilder =
        GroupKey.newBuilder().setOwnerPackage(context.getPackageName()).setDownloaded(false);
    GroupKey pendingGroupKeyToRemove1 = groupKeyBuilder.setGroupName(TEST_GROUP).build();
    GroupKey pendingGroupKeyToRemove2 = groupKeyBuilder.setGroupName(TEST_GROUP_2).build();
    GroupKey pendingGroupKeyToKeep = groupKeyBuilder.setGroupName(TEST_GROUP_3).build();
    GroupKey downloadedGroupKeyToKeep =
        groupKeyBuilder.setGroupName(TEST_GROUP_4).setDownloaded(true).build();

    writePendingFileGroup(pendingGroupKeyToRemove1, pendingGroupToRemove1);
    writePendingFileGroup(pendingGroupKeyToRemove2, pendingGroupToRemove2);
    writePendingFileGroup(pendingGroupKeyToKeep, pendingGroupToKeep);
    writeDownloadedFileGroup(downloadedGroupKeyToKeep, downloadedGroupToKeep);

    writeSharedFiles(
        sharedFilesMetadata,
        pendingGroupToRemove1,
        ImmutableList.of(FileStatus.DOWNLOAD_IN_PROGRESS));
    writeSharedFiles(
        sharedFilesMetadata,
        pendingGroupToRemove2,
        ImmutableList.of(FileStatus.DOWNLOAD_IN_PROGRESS));
    writeSharedFiles(
        sharedFilesMetadata, pendingGroupToKeep, ImmutableList.of(FileStatus.DOWNLOAD_IN_PROGRESS));
    writeSharedFiles(
        sharedFilesMetadata, downloadedGroupToKeep, ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE));

    fileGroupManager
        .removeFileGroups(ImmutableList.of(pendingGroupKeyToRemove1, pendingGroupKeyToRemove2))
        .get();

    // Construct Pending File Uris to check which downloads were stopped
    NewFileKey pendingFileKey1 =
        MddTestUtil.createFileKeysForDataFileGroupInternal(pendingGroupToRemove1)[0];
    NewFileKey pendingFileKey2 =
        MddTestUtil.createFileKeysForDataFileGroupInternal(pendingGroupToRemove2)[0];
    NewFileKey pendingFileKey3 =
        MddTestUtil.createFileKeysForDataFileGroupInternal(pendingGroupToKeep)[0];
    Uri pendingFileUri1 =
        DirectoryUtil.getOnDeviceUri(
            context,
            pendingFileKey1.getAllowedReaders(),
            pendingGroupToRemove1.getFile(0).getFileId(),
            pendingFileKey1.getChecksum(),
            mockSilentFeedback,
            /* instanceId= */ Optional.absent(),
            /* androidShared= */ false);
    Uri pendingFileUri2 =
        DirectoryUtil.getOnDeviceUri(
            context,
            pendingFileKey2.getAllowedReaders(),
            pendingGroupToRemove2.getFile(0).getFileId(),
            pendingFileKey2.getChecksum(),
            mockSilentFeedback,
            /* instanceId= */ Optional.absent(),
            /* androidShared= */ false);
    Uri pendingFileUri3 =
        DirectoryUtil.getOnDeviceUri(
            context,
            pendingFileKey3.getAllowedReaders(),
            pendingGroupToKeep.getFile(0).getFileId(),
            pendingFileKey3.getChecksum(),
            mockSilentFeedback,
            /* instanceId= */ Optional.absent(),
            /* androidShared= */ false);

    // Assert that matching pending groups are removed
    assertThat(readPendingFileGroup(pendingGroupKeyToRemove1)).isNull();
    assertThat(readPendingFileGroup(pendingGroupKeyToRemove2)).isNull();
    assertThat(readPendingFileGroup(pendingGroupKeyToKeep)).isNotNull();
    assertThat(readDownloadedFileGroup(downloadedGroupKeyToKeep)).isNotNull();
    assertThat(fileGroupsMetadata.getAllFreshGroups().get()).hasSize(2);
    assertThat(fileGroupsMetadata.getAllStaleGroups().get()).isEmpty();

    verify(mockDownloader).stopDownloading(pendingFileKey1.getChecksum(), pendingFileUri1);
    verify(mockDownloader).stopDownloading(pendingFileKey2.getChecksum(), pendingFileUri2);
    verify(mockDownloader, times(0))
        .stopDownloading(pendingFileKey3.getChecksum(), pendingFileUri3);
  }

  @Test
  public void testRemoveFileGroups_whenMatchingDownloadedGroups_performsRemove() throws Exception {
    // Create 2 downloaded groups that will be removed, 1 downloaded group that shouldn't be
    // removed, and 1 pending group that shouldn't be removed
    DataFileGroupInternal downloadedGroupToRemove1 =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1);
    DataFileGroupInternal downloadedGroupToRemove2 =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP_2, 1);
    DataFileGroupInternal downloadedGroupToKeep =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP_3, 1);
    DataFileGroupInternal pendingGroupToKeep =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP_4, 1).toBuilder().build();

    GroupKey.Builder groupKeyBuilder =
        GroupKey.newBuilder().setOwnerPackage(context.getPackageName()).setDownloaded(true);
    GroupKey downloadedGroupKeyToRemove1 = groupKeyBuilder.setGroupName(TEST_GROUP).build();
    GroupKey downloadedGroupKeyToRemove2 = groupKeyBuilder.setGroupName(TEST_GROUP_2).build();
    GroupKey downloadedGroupKeyToKeep = groupKeyBuilder.setGroupName(TEST_GROUP_3).build();
    GroupKey pendingGroupKeyToKeep =
        groupKeyBuilder.setGroupName(TEST_GROUP_4).setDownloaded(false).build();

    writeDownloadedFileGroup(downloadedGroupKeyToRemove1, downloadedGroupToRemove1);
    writeDownloadedFileGroup(downloadedGroupKeyToRemove2, downloadedGroupToRemove2);
    writeDownloadedFileGroup(downloadedGroupKeyToKeep, downloadedGroupToKeep);
    writePendingFileGroup(pendingGroupKeyToKeep, pendingGroupToKeep);

    writeSharedFiles(
        sharedFilesMetadata,
        downloadedGroupToRemove1,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE));
    writeSharedFiles(
        sharedFilesMetadata,
        downloadedGroupToRemove2,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE));
    writeSharedFiles(
        sharedFilesMetadata, downloadedGroupToKeep, ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE));
    writeSharedFiles(
        sharedFilesMetadata, pendingGroupToKeep, ImmutableList.of(FileStatus.DOWNLOAD_IN_PROGRESS));

    fileGroupManager
        .removeFileGroups(
            ImmutableList.of(downloadedGroupKeyToRemove1, downloadedGroupKeyToRemove2))
        .get();

    // Construct Pending File Uri to check that it isn't cancelled
    NewFileKey pendingFileKey1 =
        MddTestUtil.createFileKeysForDataFileGroupInternal(pendingGroupToKeep)[0];
    Uri pendingFileUri1 =
        DirectoryUtil.getOnDeviceUri(
            context,
            pendingFileKey1.getAllowedReaders(),
            pendingGroupToKeep.getFile(0).getFileId(),
            pendingFileKey1.getChecksum(),
            mockSilentFeedback,
            /* instanceId= */ Optional.absent(),
            /* androidShared= */ false);

    // Assert that matching pending groups are removed
    assertThat(readDownloadedFileGroup(downloadedGroupKeyToRemove1)).isNull();
    assertThat(readDownloadedFileGroup(downloadedGroupKeyToRemove2)).isNull();
    assertThat(readDownloadedFileGroup(downloadedGroupKeyToKeep)).isNotNull();
    assertThat(readPendingFileGroup(pendingGroupKeyToKeep)).isNotNull();

    assertThat(fileGroupsMetadata.getAllFreshGroups().get())
        .containsExactly(
            GroupKeyAndGroup.create(downloadedGroupKeyToKeep, downloadedGroupToKeep),
            GroupKeyAndGroup.create(pendingGroupKeyToKeep, pendingGroupToKeep));
    assertThat(fileGroupsMetadata.getAllStaleGroups().get())
        .containsExactly(
            downloadedGroupToRemove1.toBuilder()
                .setBookkeeping(
                    downloadedGroupToRemove1.getBookkeeping().toBuilder()
                        .setStaleExpirationDate(1)
                        .build())
                .build(),
            downloadedGroupToRemove2.toBuilder()
                .setBookkeeping(
                    downloadedGroupToRemove2.getBookkeeping().toBuilder()
                        .setStaleExpirationDate(1)
                        .build())
                .build());

    verify(mockDownloader, times(0))
        .stopDownloading(pendingFileKey1.getChecksum(), pendingFileUri1);
  }

  @Test
  public void testRemoveFileGroups_whenMatchingBothVersions_performsRemove() throws Exception {
    // Create 2 file groups, each with 2 versions (downloaded and pending)
    DataFileGroupInternal downloadedGroupToRemove1 =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1);
    DataFileGroupInternal downloadedGroupToRemove2 =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP_2, 1);
    DataFileGroupInternal pendingGroupToRemove1 =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1).toBuilder().build();
    DataFileGroupInternal pendingGroupToRemove2 =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP_2, 1);

    GroupKey.Builder groupKeyBuilder =
        GroupKey.newBuilder().setOwnerPackage(context.getPackageName()).setDownloaded(true);
    GroupKey downloadedGroupKeyToRemove1 = groupKeyBuilder.setGroupName(TEST_GROUP).build();
    GroupKey downloadedGroupKeyToRemove2 = groupKeyBuilder.setGroupName(TEST_GROUP_2).build();
    GroupKey pendingGroupKeyToRemove1 =
        groupKeyBuilder.setGroupName(TEST_GROUP).setDownloaded(false).build();
    GroupKey pendingGroupKeyToRemove2 =
        groupKeyBuilder.setGroupName(TEST_GROUP_2).setDownloaded(false).build();

    writeDownloadedFileGroup(downloadedGroupKeyToRemove1, downloadedGroupToRemove1);
    writeDownloadedFileGroup(downloadedGroupKeyToRemove2, downloadedGroupToRemove2);
    writePendingFileGroup(pendingGroupKeyToRemove1, pendingGroupToRemove1);
    writePendingFileGroup(pendingGroupKeyToRemove2, pendingGroupToRemove2);

    writeSharedFiles(
        sharedFilesMetadata,
        downloadedGroupToRemove1,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE));
    writeSharedFiles(
        sharedFilesMetadata,
        downloadedGroupToRemove2,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE));
    writeSharedFiles(
        sharedFilesMetadata,
        pendingGroupToRemove1,
        ImmutableList.of(FileStatus.DOWNLOAD_IN_PROGRESS));
    writeSharedFiles(
        sharedFilesMetadata,
        pendingGroupToRemove2,
        ImmutableList.of(FileStatus.DOWNLOAD_IN_PROGRESS));

    // NOTE: the downloaded version of keys are used in this call, but this shouldn't be relevant;
    // both downloaded and pending versions of group keys should be checked for removal.
    fileGroupManager
        .removeFileGroups(
            ImmutableList.of(downloadedGroupKeyToRemove1, downloadedGroupKeyToRemove2))
        .get();

    // Construct Pending File Uri to check that its download was cancelled
    NewFileKey pendingFileKey1 =
        MddTestUtil.createFileKeysForDataFileGroupInternal(pendingGroupToRemove1)[0];
    NewFileKey pendingFileKey2 =
        MddTestUtil.createFileKeysForDataFileGroupInternal(pendingGroupToRemove2)[0];
    Uri pendingFileUri1 =
        DirectoryUtil.getOnDeviceUri(
            context,
            pendingFileKey1.getAllowedReaders(),
            pendingGroupToRemove1.getFile(0).getFileId(),
            pendingFileKey1.getChecksum(),
            mockSilentFeedback,
            /* instanceId= */ Optional.absent(),
            /* androidShared= */ false);
    Uri pendingFileUri2 =
        DirectoryUtil.getOnDeviceUri(
            context,
            pendingFileKey2.getAllowedReaders(),
            pendingGroupToRemove2.getFile(0).getFileId(),
            pendingFileKey2.getChecksum(),
            mockSilentFeedback,
            /* instanceId= */ Optional.absent(),
            /* androidShared= */ false);

    // Assert that matching pending groups are removed
    assertThat(readDownloadedFileGroup(downloadedGroupKeyToRemove1)).isNull();
    assertThat(readDownloadedFileGroup(downloadedGroupKeyToRemove2)).isNull();
    assertThat(readPendingFileGroup(pendingGroupKeyToRemove1)).isNull();
    assertThat(readPendingFileGroup(pendingGroupKeyToRemove2)).isNull();

    assertThat(fileGroupsMetadata.getAllFreshGroups().get()).isEmpty();
    assertThat(fileGroupsMetadata.getAllStaleGroups().get())
        .containsExactly(
            downloadedGroupToRemove1.toBuilder()
                .setBookkeeping(
                    downloadedGroupToRemove1.getBookkeeping().toBuilder()
                        .setStaleExpirationDate(1)
                        .build())
                .build(),
            downloadedGroupToRemove2.toBuilder()
                .setBookkeeping(
                    downloadedGroupToRemove2.getBookkeeping().toBuilder()
                        .setStaleExpirationDate(1)
                        .build())
                .build());

    verify(mockDownloader, times(1))
        .stopDownloading(pendingFileKey1.getChecksum(), pendingFileUri1);
    verify(mockDownloader, times(1))
        .stopDownloading(pendingFileKey2.getChecksum(), pendingFileUri2);
  }

  @Test
  public void testRemoveFileGroups_whenFilesAreReferencedByOtherGroups_doesNotCancelDownloads()
      throws Exception {
    // Setup 2 pending groups to remove that each contain a file referenced by a 3rd pending group
    // that doesn't get removed. The pending file downloads referenced by the 3rd group should not
    // be cancelled.
    DataFileGroupInternal pendingGroupToKeep =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    DataFileGroupInternal pendingGroupToRemove1 =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP_2, 1).toBuilder()
            .addFile(pendingGroupToKeep.getFile(0))
            .build();
    DataFileGroupInternal pendingGroupToRemove2 =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP_3, 1).toBuilder()
            .addFile(pendingGroupToKeep.getFile(1))
            .build();

    GroupKey.Builder groupKeyBuilder =
        GroupKey.newBuilder().setOwnerPackage(context.getPackageName()).setDownloaded(false);
    GroupKey pendingGroupKeyToKeep = groupKeyBuilder.setGroupName(TEST_GROUP).build();
    GroupKey pendingGroupKeyToRemove1 = groupKeyBuilder.setGroupName(TEST_GROUP_2).build();
    GroupKey pendingGroupKeyToRemove2 = groupKeyBuilder.setGroupName(TEST_GROUP_3).build();

    writePendingFileGroup(pendingGroupKeyToKeep, pendingGroupToKeep);
    writePendingFileGroup(pendingGroupKeyToRemove1, pendingGroupToRemove1);
    writePendingFileGroup(pendingGroupKeyToRemove2, pendingGroupToRemove2);

    writeSharedFiles(
        sharedFilesMetadata,
        pendingGroupToKeep,
        ImmutableList.of(FileStatus.DOWNLOAD_IN_PROGRESS, FileStatus.DOWNLOAD_IN_PROGRESS));
    writeSharedFiles(
        sharedFilesMetadata,
        pendingGroupToRemove1,
        ImmutableList.of(FileStatus.DOWNLOAD_IN_PROGRESS, FileStatus.DOWNLOAD_IN_PROGRESS));
    writeSharedFiles(
        sharedFilesMetadata,
        pendingGroupToRemove2,
        ImmutableList.of(FileStatus.DOWNLOAD_IN_PROGRESS, FileStatus.DOWNLOAD_IN_PROGRESS));

    fileGroupManager
        .removeFileGroups(ImmutableList.of(pendingGroupKeyToRemove1, pendingGroupKeyToRemove2))
        .get();

    assertThat(readPendingFileGroup(pendingGroupKeyToRemove1)).isNull();
    assertThat(readPendingFileGroup(pendingGroupKeyToRemove2)).isNull();
    assertThat(readPendingFileGroup(pendingGroupKeyToKeep)).isNotNull();
    assertThat(fileGroupsMetadata.getAllFreshGroups().get())
        .containsExactly(GroupKeyAndGroup.create(pendingGroupKeyToKeep, pendingGroupToKeep));

    // Get On Device Uris to check if file downloads were cancelled
    List<Uri> uncancelledFileUris = getOnDeviceUrisForFileGroup(pendingGroupToKeep);
    verify(mockDownloader, times(0)).stopDownloading(any(), eq(uncancelledFileUris.get(0)));
    verify(mockDownloader, times(0)).stopDownloading(any(), eq(uncancelledFileUris.get(1)));

    verify(mockDownloader, times(1))
        .stopDownloading(
            pendingGroupToRemove1.getFile(0).getChecksum(),
            getOnDeviceUrisForFileGroup(pendingGroupToRemove1).get(0));
    verify(mockDownloader, times(1))
        .stopDownloading(
            pendingGroupToRemove2.getFile(0).getChecksum(),
            getOnDeviceUrisForFileGroup(pendingGroupToRemove2).get(0));
  }

  @Test
  public void testRemoveFileGroups_whenRemovePendingGroupFails_doesNotContinue() throws Exception {
    // Use Mocks to simulate failure scenario
    resetFileGroupManager(mockFileGroupsMetadata, mockSharedFileManager);

    DataFileGroupInternal pendingGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1);

    GroupKey.Builder groupKeyBuilder =
        GroupKey.newBuilder().setOwnerPackage(context.getPackageName());
    GroupKey pendingGroupKey =
        groupKeyBuilder.setGroupName(TEST_GROUP).setDownloaded(false).build();
    GroupKey downloadedGroupKey =
        groupKeyBuilder.setGroupName(TEST_GROUP_2).setDownloaded(true).build();

    when(mockFileGroupsMetadata.read(pendingGroupKey))
        .thenReturn(Futures.immediateFuture(pendingGroup));
    when(mockFileGroupsMetadata.read(getPendingKey(downloadedGroupKey)))
        .thenReturn(Futures.immediateFuture(null));
    when(mockFileGroupsMetadata.removeAllGroupsWithKeys(groupKeysCaptor.capture()))
        .thenReturn(Futures.immediateFuture(false));

    ExecutionException ex =
        assertThrows(
            ExecutionException.class,
            () ->
                fileGroupManager
                    .removeFileGroups(ImmutableList.of(pendingGroupKey, downloadedGroupKey))
                    .get());
    assertThat(ex).hasCauseThat().isInstanceOf(IOException.class);

    verify(mockFileGroupsMetadata, times(0)).addStaleGroup(any());
    verify(mockSharedFileManager, times(0)).cancelDownload(any());
    verify(mockLogger, times(1)).logEventSampled(MddClientEvent.Code.EVENT_CODE_UNSPECIFIED);
    verify(mockFileGroupsMetadata, times(1)).removeAllGroupsWithKeys(any());
    List<GroupKey> attemptedRemoveKeys = groupKeysCaptor.getValue();
    assertThat(attemptedRemoveKeys).containsExactly(pendingGroupKey);
  }

  @Test
  public void testRemoveFileGroups_whenRemoveDownloadedGroupFails_doesNotContinue()
      throws Exception {
    // Use Mock FileGroupsMetadata to simulate failure scenario
    resetFileGroupManager(mockFileGroupsMetadata, mockSharedFileManager);

    DataFileGroupInternal pendingGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1);
    DataFileGroupInternal downloadedGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP_2, 1);

    GroupKey.Builder groupKeyBuilder =
        GroupKey.newBuilder().setOwnerPackage(context.getPackageName());
    GroupKey pendingGroupKey =
        groupKeyBuilder.setGroupName(TEST_GROUP).setDownloaded(false).build();
    GroupKey downloadedGroupKey =
        groupKeyBuilder.setGroupName(TEST_GROUP_2).setDownloaded(true).build();

    // Mock variations of group key reads
    when(mockFileGroupsMetadata.read(pendingGroupKey))
        .thenReturn(Futures.immediateFuture(pendingGroup));
    when(mockFileGroupsMetadata.read(getPendingKey(downloadedGroupKey)))
        .thenReturn(Futures.immediateFuture(null));
    when(mockFileGroupsMetadata.read(downloadedGroupKey))
        .thenReturn(Futures.immediateFuture(downloadedGroup));
    when(mockFileGroupsMetadata.read(getDownloadedKey(pendingGroupKey)))
        .thenReturn(Futures.immediateFuture(null));

    // Return true for pending groups removed, but false for downloaded groups
    when(mockFileGroupsMetadata.removeAllGroupsWithKeys(groupKeysCaptor.capture()))
        .thenReturn(Futures.immediateFuture(true))
        .thenReturn(Futures.immediateFuture(false));

    ExecutionException ex =
        assertThrows(
            ExecutionException.class,
            () ->
                fileGroupManager
                    .removeFileGroups(ImmutableList.of(pendingGroupKey, downloadedGroupKey))
                    .get());
    assertThat(ex).hasCauseThat().isInstanceOf(IOException.class);

    verify(mockFileGroupsMetadata, times(0)).addStaleGroup(any());
    verify(mockSharedFileManager, times(0)).cancelDownload(any());
    verify(mockLogger, times(1)).logEventSampled(MddClientEvent.Code.EVENT_CODE_UNSPECIFIED);
    verify(mockFileGroupsMetadata, times(2)).removeAllGroupsWithKeys(any());
    List<List<GroupKey>> removeCallInvocations = groupKeysCaptor.getAllValues();
    assertThat(removeCallInvocations.get(0)).containsExactly(pendingGroupKey);
    assertThat(removeCallInvocations.get(1)).containsExactly(downloadedGroupKey);
  }

  @Test
  public void testRemoveFileGroups_whenAddingStaleGroupFails_doesNotContinue() throws Exception {
    // Use Mock FileGroupsMetadata to simulate failure scenario
    resetFileGroupManager(mockFileGroupsMetadata, mockSharedFileManager);

    DataFileGroupInternal pendingGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1);
    DataFileGroupInternal downloadedGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP_2, 1);

    GroupKey.Builder groupKeyBuilder =
        GroupKey.newBuilder().setOwnerPackage(context.getPackageName());
    GroupKey pendingGroupKey =
        groupKeyBuilder.setGroupName(TEST_GROUP).setDownloaded(false).build();
    GroupKey downloadedGroupKey =
        groupKeyBuilder.setGroupName(TEST_GROUP_2).setDownloaded(true).build();

    // Mock read group key variations
    when(mockFileGroupsMetadata.read(pendingGroupKey))
        .thenReturn(Futures.immediateFuture(pendingGroup));
    when(mockFileGroupsMetadata.read(getPendingKey(downloadedGroupKey)))
        .thenReturn(Futures.immediateFuture(null));
    when(mockFileGroupsMetadata.read(downloadedGroupKey))
        .thenReturn(Futures.immediateFuture(downloadedGroup));
    when(mockFileGroupsMetadata.read(getDownloadedKey(pendingGroupKey)))
        .thenReturn(Futures.immediateFuture(null));

    // Always return true for remove calls
    when(mockFileGroupsMetadata.removeAllGroupsWithKeys(groupKeysCaptor.capture()))
        .thenReturn(Futures.immediateFuture(true));

    // Fail when attempting to add a stale group
    when(mockFileGroupsMetadata.addStaleGroup(downloadedGroup))
        .thenReturn(Futures.immediateFuture(false));

    ExecutionException ex =
        assertThrows(
            ExecutionException.class,
            () ->
                fileGroupManager
                    .removeFileGroups(ImmutableList.of(pendingGroupKey, downloadedGroupKey))
                    .get());
    assertThat(ex).hasCauseThat().isInstanceOf(AggregateException.class);

    verify(mockFileGroupsMetadata, times(1)).addStaleGroup(downloadedGroup);
    verify(mockSharedFileManager, times(0)).cancelDownload(any());
    verify(mockLogger, times(1)).logEventSampled(MddClientEvent.Code.EVENT_CODE_UNSPECIFIED);
    verify(mockFileGroupsMetadata, times(2)).removeAllGroupsWithKeys(any());
    List<List<GroupKey>> removeCallInvocations = groupKeysCaptor.getAllValues();
    assertThat(removeCallInvocations.get(0)).containsExactly(pendingGroupKey);
    assertThat(removeCallInvocations.get(1)).containsExactly(downloadedGroupKey);
  }

  @Test
  public void testRemoveFileGroups_whenCancellingPendingDownloadFails_doesNotContinue()
      throws Exception {
    // Use Mock FileGroupsMetadata to simulate failure scenario
    resetFileGroupManager(fileGroupsMetadata, mockSharedFileManager);

    DataFileGroupInternal pendingGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1);
    DataFileGroupInternal downloadedGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP_2, 1);

    GroupKey.Builder groupKeyBuilder =
        GroupKey.newBuilder().setOwnerPackage(context.getPackageName());
    GroupKey pendingGroupKey =
        groupKeyBuilder.setGroupName(TEST_GROUP).setDownloaded(false).build();
    GroupKey downloadedGroupKey =
        groupKeyBuilder.setGroupName(TEST_GROUP_2).setDownloaded(true).build();

    writePendingFileGroup(pendingGroupKey, pendingGroup);
    writeDownloadedFileGroup(downloadedGroupKey, downloadedGroup);
    writeSharedFiles(
        sharedFilesMetadata, pendingGroup, ImmutableList.of(FileStatus.DOWNLOAD_IN_PROGRESS));
    writeSharedFiles(
        sharedFilesMetadata, downloadedGroup, ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE));

    // Fail when cancelling download
    NewFileKey[] pendingFileKeys = MddTestUtil.createFileKeysForDataFileGroupInternal(pendingGroup);
    when(mockSharedFileManager.cancelDownload(pendingFileKeys[0]))
        .thenReturn(Futures.immediateFailedFuture(new Exception("Test failure")));

    ExecutionException ex =
        assertThrows(
            ExecutionException.class,
            () ->
                fileGroupManager
                    .removeFileGroups(ImmutableList.of(pendingGroupKey, downloadedGroupKey))
                    .get());
    assertThat(ex).hasCauseThat().isInstanceOf(AggregateException.class);

    assertThat(readPendingFileGroup(pendingGroupKey)).isNull();
    assertThat(readDownloadedFileGroup(downloadedGroupKey)).isNull();
    assertThat(fileGroupsMetadata.getAllFreshGroups().get()).isEmpty();
    assertThat(fileGroupsMetadata.getAllStaleGroups().get())
        .containsExactly(
            downloadedGroup.toBuilder()
                .setBookkeeping(
                    downloadedGroup.getBookkeeping().toBuilder().setStaleExpirationDate(1).build())
                .build());
  }

  @Test
  public void testRemoveFileGroups_whenMultipleVariantsExists_removesVariantsSpecified()
      throws Exception {
    // Create multiple variants of a group (default (empty), en, fr) and remove the default (empty)
    // variant and en keys. Ensure that only the fr group remains.
    GroupKey defaultGroupKey =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP)
            .setOwnerPackage(context.getPackageName())
            .build();
    GroupKey enGroupKey = defaultGroupKey.toBuilder().setVariantId("en").build();
    GroupKey frGroupKey = defaultGroupKey.toBuilder().setVariantId("fr").build();

    DataFileGroupInternal defaultFileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1);
    DataFileGroupInternal enFileGroup = defaultFileGroup.toBuilder().setVariantId("en").build();
    DataFileGroupInternal frFileGroup = defaultFileGroup.toBuilder().setVariantId("fr").build();

    writePendingFileGroup(getPendingKey(defaultGroupKey), defaultFileGroup);
    writePendingFileGroup(getPendingKey(enGroupKey), enFileGroup);
    writePendingFileGroup(getPendingKey(frGroupKey), frFileGroup);

    writeSharedFiles(
        sharedFilesMetadata, defaultFileGroup, ImmutableList.of(FileStatus.DOWNLOAD_IN_PROGRESS));
    writeSharedFiles(
        sharedFilesMetadata, enFileGroup, ImmutableList.of(FileStatus.DOWNLOAD_IN_PROGRESS));
    writeSharedFiles(
        sharedFilesMetadata, frFileGroup, ImmutableList.of(FileStatus.DOWNLOAD_IN_PROGRESS));

    // Assert that all file groups share the same file even through the variants are different
    assertThat(sharedFilesMetadata.getAllFileKeys().get()).hasSize(1);

    {
      // Perfrom removal once and check that the correct groups get removed
      fileGroupManager.removeFileGroups(ImmutableList.of(defaultGroupKey, enGroupKey)).get();

      assertThat(fileGroupsMetadata.getAllGroupKeys().get())
          .comparingElementsUsing(GROUP_KEY_TO_VARIANT)
          .containsExactly("fr");
      assertThat(fileGroupsMetadata.getAllFreshGroups().get())
          .comparingElementsUsing(KEY_GROUP_PAIR_TO_VARIANT)
          .containsExactly("fr");

      assertThat(sharedFilesMetadata.getAllFileKeys().get()).hasSize(1);
    }

    {
      // Perform remove again and verify that there is no change in state
      fileGroupManager.removeFileGroups(ImmutableList.of(defaultGroupKey, enGroupKey)).get();

      assertThat(fileGroupsMetadata.getAllGroupKeys().get())
          .comparingElementsUsing(GROUP_KEY_TO_VARIANT)
          .containsExactly("fr");
      assertThat(fileGroupsMetadata.getAllFreshGroups().get())
          .comparingElementsUsing(KEY_GROUP_PAIR_TO_VARIANT)
          .containsExactly("fr");

      assertThat(sharedFilesMetadata.getAllFileKeys().get()).hasSize(1);
    }
  }

  @Test
  public void testGetDownloadedGroup() throws Exception {
    assertThat(fileGroupManager.getFileGroup(testKey, true).get()).isNull();

    DataFileGroupInternal dataFileGroup =
        MddTestUtil.createDownloadedDataFileGroupInternal(TEST_GROUP, 2);
    writeDownloadedFileGroup(testKey, dataFileGroup);

    DataFileGroupInternal downloadedGroup = fileGroupManager.getFileGroup(testKey, true).get();
    MddTestUtil.assertMessageEquals(dataFileGroup, downloadedGroup);
  }

  @Test
  public void testGetDownloadedGroup_whenMultipleVariantsExists_getsCorrectGroup()
      throws Exception {
    GroupKey defaultGroupKey =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP)
            .setOwnerPackage(context.getPackageName())
            .build();
    GroupKey enGroupKey = defaultGroupKey.toBuilder().setVariantId("en").build();

    // Initially, assert that groups don't exist
    assertThat(fileGroupManager.getFileGroup(defaultGroupKey, true).get()).isNull();
    assertThat(fileGroupManager.getFileGroup(enGroupKey, true).get()).isNull();

    // Create groups and write them
    DataFileGroupInternal defaultFileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1);
    DataFileGroupInternal enFileGroup = defaultFileGroup.toBuilder().setVariantId("en").build();

    writeDownloadedFileGroup(getDownloadedKey(defaultGroupKey), defaultFileGroup);
    writeDownloadedFileGroup(getDownloadedKey(enGroupKey), enFileGroup);

    // Assert the correct group is returned for each key
    DataFileGroupInternal groupForDefaultKey =
        fileGroupManager.getFileGroup(defaultGroupKey, true).get();
    MddTestUtil.assertMessageEquals(defaultFileGroup, groupForDefaultKey);

    DataFileGroupInternal groupForEnKey = fileGroupManager.getFileGroup(enGroupKey, true).get();
    MddTestUtil.assertMessageEquals(enFileGroup, groupForEnKey);
  }

  @Test
  public void testGetPendingGroup() throws Exception {
    assertThat(fileGroupManager.getFileGroup(testKey, false).get()).isNull();

    DataFileGroupInternal dataFileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    writePendingFileGroup(testKey, dataFileGroup);

    DataFileGroupInternal pendingGroup = fileGroupManager.getFileGroup(testKey, false).get();
    MddTestUtil.assertMessageEquals(dataFileGroup, pendingGroup);
  }

  @Test
  public void testGetPendingGroup_whenMultipleVariantsExists_getsCorrectGroup() throws Exception {
    GroupKey defaultGroupKey =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP)
            .setOwnerPackage(context.getPackageName())
            .build();
    GroupKey enGroupKey = defaultGroupKey.toBuilder().setVariantId("en").build();

    // Initially, assert that groups don't exist
    assertThat(fileGroupManager.getFileGroup(defaultGroupKey, false).get()).isNull();
    assertThat(fileGroupManager.getFileGroup(enGroupKey, false).get()).isNull();

    // Create groups and write them
    DataFileGroupInternal defaultFileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1);
    DataFileGroupInternal enFileGroup = defaultFileGroup.toBuilder().setVariantId("en").build();

    writePendingFileGroup(getPendingKey(defaultGroupKey), defaultFileGroup);
    writePendingFileGroup(getPendingKey(enGroupKey), enFileGroup);

    // Assert the correct group is returned for each key
    DataFileGroupInternal groupForDefaultKey =
        fileGroupManager.getFileGroup(defaultGroupKey, false).get();
    MddTestUtil.assertMessageEquals(defaultFileGroup, groupForDefaultKey);

    DataFileGroupInternal groupForEnKey = fileGroupManager.getFileGroup(enGroupKey, false).get();
    MddTestUtil.assertMessageEquals(enFileGroup, groupForEnKey);
  }

  @Test
  public void testSetGroupActivation_deactivationRemovesGroupsRequiringActivation()
      throws Exception {
    flags.enableDelayedDownload = Optional.of(true);

    // Create 2 groups, one of which requires device side activation.
    DataFileGroupInternal.Builder fileGroup1 =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2).toBuilder();
    DownloadConditions.Builder downloadConditions =
        DownloadConditions.newBuilder()
            .setActivatingCondition(ActivatingCondition.DEVICE_ACTIVATED);
    fileGroup1.setDownloadConditions(downloadConditions);

    DataFileGroupInternal fileGroup2 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP_2, 3);

    // Activate both group keys and add groups to FileGroupManager.
    assertThat(fileGroupManager.setGroupActivation(testKey, true).get()).isTrue();

    assertThat(fileGroupManager.addGroupForDownload(testKey, fileGroup1.build()).get()).isTrue();

    assertThat(fileGroupManager.setGroupActivation(testKey2, true).get()).isTrue();

    assertThat(fileGroupManager.addGroupForDownload(testKey2, fileGroup2).get()).isTrue();

    // Add a downloaded version of the second group, that requires device side activation.
    DataFileGroupInternal downloadedfileGroup2 =
        MddTestUtil.createDownloadedDataFileGroupInternal(TEST_GROUP_2, 1);
    downloadConditions = DownloadConditions.newBuilder();
    downloadedfileGroup2 =
        downloadedfileGroup2.toBuilder()
            .setDownloadConditions(
                downloadConditions.setActivatingCondition(ActivatingCondition.DEVICE_ACTIVATED))
            .build();
    writeDownloadedFileGroup(testKey2, downloadedfileGroup2);

    // Deactivate both group keys, and check that the groups that required activation are deleted.
    assertThat(fileGroupManager.setGroupActivation(testKey, false).get()).isTrue();
    // Setting group activation to false will only remove groups that have
    // ActivatingCondition.DEVICE_ACTIVATED. So the pending version will remain, while the
    // downloaded one is removed.
    assertThat(fileGroupManager.setGroupActivation(testKey2, false).get()).isTrue();

    assertThat(readPendingFileGroup(testKey)).isNull();
    assertThat(readPendingFileGroup(testKey2)).isNotNull();
    assertThat(readDownloadedFileGroup(testKey2)).isNull();
  }

  @Test
  public void testImportFilesIntoFileGroup_whenExistingGroupDoesNotExist_fails() throws Exception {
    DataFile inlineFile =
        DataFile.newBuilder()
            .setFileId("inline-file")
            .setChecksum("abc")
            .setUrlToDownload("inlinefile:sha1:abc")
            .build();
    ImmutableList<DataFile> updatedDataFileList = ImmutableList.of(inlineFile);

    GroupKey groupKey = GroupKey.newBuilder().setGroupName("non-existing-group").build();

    ExecutionException ex =
        assertThrows(
            ExecutionException.class,
            () ->
                fileGroupManager
                    .importFilesIntoFileGroup(
                        groupKey,
                        /* buildId= */ 0,
                        /* variantId= */ "",
                        updatedDataFileList,
                        /* inlineFileMap= */ ImmutableMap.of(),
                        /* customPropertyOptional= */ Optional.absent(),
                        noCustomValidation())
                    .get());

    assertThat(ex).hasCauseThat().isInstanceOf(DownloadException.class);
    DownloadException dex = (DownloadException) ex.getCause();
    assertThat(dex.getDownloadResultCode()).isEqualTo(DownloadResultCode.GROUP_NOT_FOUND_ERROR);
  }

  @Test
  public void testImportFilesIntoFileGroup_whenExistingPendingGroupDoesNotMatchIdentifiers_fails()
      throws Exception {
    // Set up existing pending file group
    DataFileGroupInternal existingFileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 0).toBuilder()
            .addFile(
                DataFile.newBuilder()
                    .setFileId("inline-file")
                    .setChecksum("abc")
                    .setUrlToDownload("inlinefile:sha1:abc")
                    .build())
            .build();
    GroupKey groupKey = GroupKey.newBuilder().setGroupName(TEST_GROUP).build();

    writePendingFileGroup(getPendingKey(groupKey), existingFileGroup);
    writeSharedFiles(
        sharedFilesMetadata, existingFileGroup, ImmutableList.of(FileStatus.DOWNLOAD_IN_PROGRESS));

    ExecutionException ex =
        assertThrows(
            ExecutionException.class,
            () ->
                fileGroupManager
                    .importFilesIntoFileGroup(
                        groupKey,
                        /* buildId= */ 1,
                        /* variantId= */ "",
                        /* updatedDataFileList= */ ImmutableList.of(),
                        /* inlineFileMap= */ ImmutableMap.of(),
                        /* customPropertyOptional= */ Optional.absent(),
                        noCustomValidation())
                    .get());

    assertThat(ex).hasCauseThat().isInstanceOf(DownloadException.class);
    DownloadException dex = (DownloadException) ex.getCause();
    assertThat(dex.getDownloadResultCode()).isEqualTo(DownloadResultCode.GROUP_NOT_FOUND_ERROR);
  }

  @Test
  public void
      testImportFilesIntoFileGroup_whenExistingDownloadedGroupDoesNotMatchIdentifiers_fails()
          throws Exception {
    // Set up existing downloaded file group
    DataFileGroupInternal existingFileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 0).toBuilder()
            .addFile(
                DataFile.newBuilder()
                    .setFileId("inline-file")
                    .setChecksum("abc")
                    .setUrlToDownload("inlinefile:sha1:abc")
                    .build())
            .build();
    GroupKey groupKey = GroupKey.newBuilder().setGroupName(TEST_GROUP).build();

    writeDownloadedFileGroup(getDownloadedKey(groupKey), existingFileGroup);
    writeSharedFiles(
        sharedFilesMetadata, existingFileGroup, ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE));

    ExecutionException ex =
        assertThrows(
            ExecutionException.class,
            () ->
                fileGroupManager
                    .importFilesIntoFileGroup(
                        groupKey,
                        /* buildId= */ 0,
                        /* variantId= */ "testvariant",
                        /* updatedDataFileList= */ ImmutableList.of(),
                        /* inlineFileMap= */ ImmutableMap.of(),
                        /* customPropertyOptional= */ Optional.absent(),
                        noCustomValidation())
                    .get());

    assertThat(ex).hasCauseThat().isInstanceOf(DownloadException.class);
    DownloadException dex = (DownloadException) ex.getCause();
    assertThat(dex.getDownloadResultCode()).isEqualTo(DownloadResultCode.GROUP_NOT_FOUND_ERROR);
  }

  @Test
  public void testImportFilesIntoFileGroup_whenBuildIdDoesNotMatch_fails() throws Exception {
    // Set up existing pending/downloaded groups and check that they do not match due to build ID
    // differences.

    // Any can pack proto messages only, so use StringValue.
    Any customProperty =
        Any.parseFrom(
            StringValue.of("testCustomProperty").toByteString(),
            ExtensionRegistryLite.getEmptyRegistry());
    DataFileGroupInternal existingDownloadedFileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 0).toBuilder()
            .setBuildId(1)
            .setVariantId("testvariant")
            .setCustomProperty(customProperty)
            .build();
    DataFileGroupInternal existingPendingFileGroup =
        existingDownloadedFileGroup.toBuilder().setBuildId(2).build();

    GroupKey groupKey = GroupKey.newBuilder().setGroupName(TEST_GROUP).build();

    writeDownloadedFileGroup(getDownloadedKey(groupKey), existingDownloadedFileGroup);
    writePendingFileGroup(getPendingKey(groupKey), existingPendingFileGroup);

    ExecutionException ex =
        assertThrows(
            ExecutionException.class,
            () ->
                fileGroupManager
                    .importFilesIntoFileGroup(
                        groupKey,
                        /* buildId= */ 3,
                        /* variantId= */ "testvariant",
                        /* updatedDataFileList= */ ImmutableList.of(),
                        /* inlineFileMap= */ ImmutableMap.of(),
                        /* customPropertyOptional= */ Optional.of(customProperty),
                        noCustomValidation())
                    .get());

    assertThat(ex).hasCauseThat().isInstanceOf(DownloadException.class);
    DownloadException dex = (DownloadException) ex.getCause();
    assertThat(dex.getDownloadResultCode()).isEqualTo(DownloadResultCode.GROUP_NOT_FOUND_ERROR);
  }

  @Test
  public void testImportFilesIntoFileGroup_whenVariantIdDoesNotMatch_fails() throws Exception {
    // Set up existing pending/downloaded groups and check that they do not match due to variant ID
    // differences.

    // Any can pack proto messages only, so use StringValue.
    Any customProperty =
        Any.parseFrom(
            StringValue.of("testCustomProperty").toByteString(),
            ExtensionRegistryLite.getEmptyRegistry());
    DataFileGroupInternal existingDownloadedFileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 0).toBuilder()
            .setBuildId(1)
            .setVariantId("testvariant")
            .setCustomProperty(customProperty)
            .build();
    DataFileGroupInternal existingPendingFileGroup =
        existingDownloadedFileGroup.toBuilder().setVariantId("testvariant2").build();

    GroupKey groupKey = GroupKey.newBuilder().setGroupName(TEST_GROUP).build();

    writeDownloadedFileGroup(getDownloadedKey(groupKey), existingDownloadedFileGroup);
    writePendingFileGroup(getPendingKey(groupKey), existingPendingFileGroup);

    ExecutionException ex =
        assertThrows(
            ExecutionException.class,
            () ->
                fileGroupManager
                    .importFilesIntoFileGroup(
                        groupKey,
                        /* buildId= */ 1,
                        /* variantId= */ "testvariant3",
                        /* updatedDataFileList= */ ImmutableList.of(),
                        /* inlineFileMap= */ ImmutableMap.of(),
                        /* customPropertyOptional= */ Optional.of(customProperty),
                        noCustomValidation())
                    .get());

    assertThat(ex).hasCauseThat().isInstanceOf(DownloadException.class);
    DownloadException dex = (DownloadException) ex.getCause();
    assertThat(dex.getDownloadResultCode()).isEqualTo(DownloadResultCode.GROUP_NOT_FOUND_ERROR);
  }

  @Test
  public void testImportFilesIntoFileGroup_whenCustomPropertyDoesNotMatch_whenDueToMismatch_fails()
      throws Exception {
    // Set up existing pending/downloaded groups and check that they do not match due to custom
    // property differences.

    // Any can pack proto messages only, so use StringValue.
    Any downloadedCustomProperty =
        Any.parseFrom(
            StringValue.of("testDownloadedCustomProperty").toByteString(),
            ExtensionRegistryLite.getEmptyRegistry());
    Any pendingCustomProperty =
        Any.parseFrom(
            StringValue.of("testPendingCustomProperty").toByteString(),
            ExtensionRegistryLite.getEmptyRegistry());
    Any mismatchedCustomProperty =
        Any.parseFrom(
            StringValue.of("testMismatcheCustomProperty").toByteString(),
            ExtensionRegistryLite.getEmptyRegistry());

    DataFileGroupInternal existingDownloadedFileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 0).toBuilder()
            .setBuildId(1)
            .setVariantId("testvariant")
            .setCustomProperty(downloadedCustomProperty)
            .build();
    DataFileGroupInternal existingPendingFileGroup =
        existingDownloadedFileGroup.toBuilder().setCustomProperty(pendingCustomProperty).build();

    GroupKey groupKey = GroupKey.newBuilder().setGroupName(TEST_GROUP).build();

    writeDownloadedFileGroup(getDownloadedKey(groupKey), existingDownloadedFileGroup);
    writePendingFileGroup(getPendingKey(groupKey), existingPendingFileGroup);

    ExecutionException ex =
        assertThrows(
            ExecutionException.class,
            () ->
                fileGroupManager
                    .importFilesIntoFileGroup(
                        groupKey,
                        /* buildId= */ 1,
                        /* variantId= */ "testvariant",
                        /* updatedDataFileList= */ ImmutableList.of(),
                        /* inlineFileMap= */ ImmutableMap.of(),
                        /* customPropertyOptional= */ Optional.of(mismatchedCustomProperty),
                        noCustomValidation())
                    .get());

    assertThat(ex).hasCauseThat().isInstanceOf(DownloadException.class);
    DownloadException dex = (DownloadException) ex.getCause();
    assertThat(dex.getDownloadResultCode()).isEqualTo(DownloadResultCode.GROUP_NOT_FOUND_ERROR);
  }

  @Test
  public void
      testImportFilesIntoFileGroup_whenCustomPropertyDoesNotMatch_whenDueToBeingAbsent_fails()
          throws Exception {
    // Set up existing pending/downloaded groups and check that they do not match due to custom
    // property differences.

    // Any can pack proto messages only, so use StringValue.
    Any downloadedCustomProperty =
        Any.parseFrom(
            StringValue.of("testDownloadedCustomProperty").toByteString(),
            ExtensionRegistryLite.getEmptyRegistry());
    Any pendingCustomProperty =
        Any.parseFrom(
            StringValue.of("testPendingCustomProperty").toByteString(),
            ExtensionRegistryLite.getEmptyRegistry());

    DataFileGroupInternal existingDownloadedFileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 0).toBuilder()
            .setBuildId(1)
            .setVariantId("testvariant")
            .setCustomProperty(downloadedCustomProperty)
            .build();
    DataFileGroupInternal existingPendingFileGroup =
        existingDownloadedFileGroup.toBuilder().setCustomProperty(pendingCustomProperty).build();

    GroupKey groupKey = GroupKey.newBuilder().setGroupName(TEST_GROUP).build();

    writeDownloadedFileGroup(getDownloadedKey(groupKey), existingDownloadedFileGroup);
    writePendingFileGroup(getPendingKey(groupKey), existingPendingFileGroup);

    ExecutionException ex =
        assertThrows(
            ExecutionException.class,
            () ->
                fileGroupManager
                    .importFilesIntoFileGroup(
                        groupKey,
                        /* buildId= */ 1,
                        /* variantId= */ "testvariant",
                        /* updatedDataFileList= */ ImmutableList.of(),
                        /* inlineFileMap= */ ImmutableMap.of(),
                        /* customPropertyOptional= */ Optional.absent(),
                        noCustomValidation())
                    .get());

    assertThat(ex).hasCauseThat().isInstanceOf(DownloadException.class);
    DownloadException dex = (DownloadException) ex.getCause();
    assertThat(dex.getDownloadResultCode()).isEqualTo(DownloadResultCode.GROUP_NOT_FOUND_ERROR);
  }

  @Test
  public void testImportFilesIntoFileGroup_whenUnableToReserveNewFiles_fails() throws Exception {
    // Reset with mock SharedFileManager to force failures
    resetFileGroupManager(fileGroupsMetadata, mockSharedFileManager);

    // Create existing file group and a new file group that will add an inline file (which needs to
    // be reserved in SharedFileManager).
    DataFileGroupInternal existingFileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1);
    ImmutableList<DataFile> updatedDataFileList =
        ImmutableList.of(
            DataFile.newBuilder()
                .setFileId("inline-file")
                .setChecksum("abc")
                .setUrlToDownload("inlinefile:sha1:abc")
                .build());

    GroupKey groupKey = GroupKey.newBuilder().setGroupName(TEST_GROUP).build();

    writeDownloadedFileGroup(getDownloadedKey(groupKey), existingFileGroup);
    writeSharedFiles(
        sharedFilesMetadata, existingFileGroup, ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE));

    NewFileKey newInlineFileKey =
        SharedFilesMetadata.createKeyFromDataFile(
            updatedDataFileList.get(0), existingFileGroup.getAllowedReadersEnum());
    NewFileKey existingFileKey =
        SharedFilesMetadata.createKeyFromDataFile(
            existingFileGroup.getFile(0), existingFileGroup.getAllowedReadersEnum());

    when(mockSharedFileManager.reserveFileEntry(newInlineFileKey))
        .thenReturn(Futures.immediateFuture(false));
    when(mockSharedFileManager.reserveFileEntry(existingFileKey))
        .thenReturn(Futures.immediateFuture(true));

    ExecutionException ex =
        assertThrows(
            ExecutionException.class,
            () ->
                fileGroupManager
                    .importFilesIntoFileGroup(
                        groupKey,
                        /* buildId= */ 0,
                        /* variantId= */ "",
                        /* updatedDataFileList= */ updatedDataFileList,
                        /* inlineFileMap= */ ImmutableMap.of(),
                        /* customPropertyOptional= */ Optional.absent(),
                        noCustomValidation())
                    .get());
    assertThat(ex).hasCauseThat().isInstanceOf(DownloadException.class);
    DownloadException dex = (DownloadException) ex.getCause();
    assertThat(dex.getDownloadResultCode())
        .isEqualTo(DownloadResultCode.UNABLE_TO_RESERVE_FILE_ENTRY);
  }

  @Test
  public void
      testImportFilesIntoFileGroup_whenNoNewInlineFilesSpecifiedAndFilesDownloaded_completes()
          throws Exception {
    // Create a group that has 1 standard file and 1 inline file, both downloaded
    DataFileGroupInternal dataFileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1).toBuilder()
            .addFile(
                DataFile.newBuilder()
                    .setFileId("inline-file")
                    .setChecksum("abc")
                    .setUrlToDownload("inlinefile:sha1:abc")
                    .build())
            .build();
    ImmutableMap<String, FileSource> inlineFileMap =
        ImmutableMap.of(
            "inline-file", FileSource.ofByteString(ByteString.copyFromUtf8("TEST_CONTENT")));

    GroupKey groupKey = GroupKey.newBuilder().setGroupName(TEST_GROUP).build();

    writeDownloadedFileGroup(getDownloadedKey(groupKey), dataFileGroup);
    writeSharedFiles(
        sharedFilesMetadata,
        dataFileGroup,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE, FileStatus.DOWNLOAD_COMPLETE));

    fileGroupManager
        .importFilesIntoFileGroup(
            groupKey,
            /* buildId= */ 0,
            /* variantId= */ "",
            /* updatedDataFileList= */ ImmutableList.of(),
            inlineFileMap,
            /* customPropertyOptional= */ Optional.absent(),
            noCustomValidation())
        .get();

    // Since no new files were specified, the group should remain the same (downloaded).
    DataFileGroupInternal downloadedFileGroup =
        fileGroupsMetadata.read(getDownloadedKey(groupKey)).get();
    assertThat(downloadedFileGroup.getGroupName()).isEqualTo(dataFileGroup.getGroupName());
    assertThat(downloadedFileGroup.getBuildId()).isEqualTo(dataFileGroup.getBuildId());
    assertThat(downloadedFileGroup.getVariantId()).isEqualTo(dataFileGroup.getVariantId());
    assertThat(downloadedFileGroup.getFileList())
        .containsExactlyElementsIn(dataFileGroup.getFileList());
  }

  @Test
  public void testImportFilesIntoFileGroup_whenNoNewFilesSpecifiedAndFilesPending_completes()
      throws Exception {
    DataFileGroupInternal dataFileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1);

    GroupKey groupKey = GroupKey.newBuilder().setGroupName(TEST_GROUP).build();

    writePendingFileGroup(getPendingKey(groupKey), dataFileGroup);
    writeSharedFiles(
        sharedFilesMetadata, dataFileGroup, ImmutableList.of(FileStatus.DOWNLOAD_IN_PROGRESS));

    fileGroupManager
        .importFilesIntoFileGroup(
            groupKey,
            /* buildId= */ 0,
            /* variantId= */ "",
            /* updatedDataFileList= */ ImmutableList.of(),
            /* inlineFileMap= */ ImmutableMap.of(),
            /* customPropertyOptional= */ Optional.absent(),
            noCustomValidation())
        .get();

    // Since no new files were specified, the group should remain the same (pending).
    DataFileGroupInternal pendingFileGroup = fileGroupsMetadata.read(getPendingKey(groupKey)).get();
    assertThat(pendingFileGroup.getGroupName()).isEqualTo(dataFileGroup.getGroupName());
    assertThat(pendingFileGroup.getBuildId()).isEqualTo(dataFileGroup.getBuildId());
    assertThat(pendingFileGroup.getVariantId()).isEqualTo(dataFileGroup.getVariantId());
    assertThat(pendingFileGroup.getFileList()).isEqualTo(dataFileGroup.getFileList());
  }

  @Test
  public void testImportFilesIntoFileGroup_whenImportingInlineFileAndPending_mergesGroup()
      throws Exception {
    // Set up an existing pending file group and a new inline file to merge
    DataFileGroupInternal existingPendingFileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1);
    ImmutableList<DataFile> updatedDataFileList =
        ImmutableList.of(
            DataFile.newBuilder()
                .setFileId("inline-file")
                .setChecksum("abc")
                .setUrlToDownload("inlinefile:sha1:abc")
                .build());
    ImmutableMap<String, FileSource> inlineFileMap =
        ImmutableMap.of(
            "inline-file", FileSource.ofByteString(ByteString.copyFromUtf8("TEST_CONTENT")));

    GroupKey groupKey = GroupKey.newBuilder().setGroupName(TEST_GROUP).build();

    writePendingFileGroup(getPendingKey(groupKey), existingPendingFileGroup);
    writeSharedFiles(
        sharedFilesMetadata,
        existingPendingFileGroup,
        ImmutableList.of(FileStatus.DOWNLOAD_IN_PROGRESS));

    // TODO: remove once SFM can perform import
    // write inline file as downloaded so FGM can find it
    NewFileKey newInlineFileKey =
        SharedFilesMetadata.createKeyFromDataFile(
            updatedDataFileList.get(0), existingPendingFileGroup.getAllowedReadersEnum());
    SharedFile newInlineSharedFile =
        SharedFile.newBuilder()
            .setFileName(updatedDataFileList.get(0).getFileId())
            .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
            .build();
    sharedFilesMetadata.write(newInlineFileKey, newInlineSharedFile).get();

    fileGroupManager
        .importFilesIntoFileGroup(
            groupKey,
            /* buildId= */ 0,
            /* variantId= */ "",
            updatedDataFileList,
            inlineFileMap,
            /* customPropertyOptional= */ Optional.absent(),
            noCustomValidation())
        .get();

    // Check that resulting file group remains pending, but should have both files merged together
    DataFileGroupInternal pendingFileGroupAfterImport =
        fileGroupsMetadata.read(getPendingKey(groupKey)).get();
    assertThat(pendingFileGroupAfterImport.getFileCount()).isEqualTo(2);
    assertThat(pendingFileGroupAfterImport.getFileList())
        .containsExactly(existingPendingFileGroup.getFile(0), updatedDataFileList.get(0));
  }

  @Test
  public void testImportFilesIntoFileGroup_whenImportingInlineFileAndDownloaded_mergesGroup()
      throws Exception {
    // Set up an existing downloaded file group and a new file group with an inline file to merge
    DataFileGroupInternal existingDownloadedFileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1);
    ImmutableList<DataFile> updatedDataFileList =
        ImmutableList.of(
            DataFile.newBuilder()
                .setFileId("inline-file")
                .setChecksum("abc")
                .setUrlToDownload("inlinefile:sha1:abc")
                .build());
    ImmutableMap<String, FileSource> inlineFileMap =
        ImmutableMap.of(
            "inline-file", FileSource.ofByteString(ByteString.copyFromUtf8("TEST_CONTENT")));

    GroupKey groupKey = GroupKey.newBuilder().setGroupName(TEST_GROUP).build();

    writeDownloadedFileGroup(getDownloadedKey(groupKey), existingDownloadedFileGroup);
    writeSharedFiles(
        sharedFilesMetadata,
        existingDownloadedFileGroup,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE));

    // TODO: remove once SFM can perform import
    // write inline file as downloaded so FGM can find it
    NewFileKey newInlineFileKey =
        SharedFilesMetadata.createKeyFromDataFile(
            updatedDataFileList.get(0), existingDownloadedFileGroup.getAllowedReadersEnum());
    SharedFile newInlineSharedFile =
        SharedFile.newBuilder()
            .setFileName(updatedDataFileList.get(0).getFileId())
            .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
            .build();
    sharedFilesMetadata.write(newInlineFileKey, newInlineSharedFile).get();

    fileGroupManager
        .importFilesIntoFileGroup(
            groupKey,
            /* buildId= */ 0,
            /* variantId= */ "",
            updatedDataFileList,
            inlineFileMap,
            /* customPropertyOptional= */ Optional.absent(),
            noCustomValidation())
        .get();

    // Check that resulting file group is downloaded, but should have both files merged together
    DataFileGroupInternal downloadedFileGroupAfterImport =
        fileGroupsMetadata.read(getDownloadedKey(groupKey)).get();
    assertThat(downloadedFileGroupAfterImport.getFileCount()).isEqualTo(2);
    assertThat(downloadedFileGroupAfterImport.getFileList())
        .containsExactly(existingDownloadedFileGroup.getFile(0), updatedDataFileList.get(0));
  }

  @Test
  public void testImportFilesIntoFileGroup_whenMatchesDownloadedButNotPending_importsToDownloaded()
      throws Exception {
    // Set up an existing pending file group, an existing downloaded file group and a new file
    // group that matches the downloaded file group
    DataFileGroupInternal existingPendingFileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1);
    DataFileGroupInternal existingDownloadedFileGroup =
        existingPendingFileGroup.toBuilder()
            .clearFile()
            .addFile(MddTestUtil.createDataFile("downloaded-file", 0))
            .setBuildId(10)
            .build();
    ImmutableList<DataFile> updatedDataFileList =
        ImmutableList.of(
            DataFile.newBuilder()
                .setFileId("inline-file")
                .setChecksum("abc")
                .setUrlToDownload("inlinefile:abc")
                .build());
    ImmutableMap<String, FileSource> inlineFileMap =
        ImmutableMap.of(
            "inline-file", FileSource.ofByteString(ByteString.copyFromUtf8("TEST_CONTENT")));

    GroupKey groupKey = GroupKey.newBuilder().setGroupName(TEST_GROUP).build();

    writePendingFileGroup(getPendingKey(groupKey), existingPendingFileGroup);
    writeDownloadedFileGroup(getDownloadedKey(groupKey), existingDownloadedFileGroup);

    writeSharedFiles(
        sharedFilesMetadata,
        existingPendingFileGroup,
        ImmutableList.of(FileStatus.DOWNLOAD_IN_PROGRESS));
    writeSharedFiles(
        sharedFilesMetadata,
        existingDownloadedFileGroup,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE));

    // TODO: remove once SFM can perform import
    // write inline file as downloaded so FGM can find it
    NewFileKey newInlineFileKey =
        SharedFilesMetadata.createKeyFromDataFile(
            updatedDataFileList.get(0), existingDownloadedFileGroup.getAllowedReadersEnum());
    SharedFile newInlineSharedFile =
        SharedFile.newBuilder()
            .setFileName(updatedDataFileList.get(0).getFileId())
            .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
            .build();
    sharedFilesMetadata.write(newInlineFileKey, newInlineSharedFile).get();

    fileGroupManager
        .importFilesIntoFileGroup(
            groupKey,
            /* buildId= */ 10,
            /* variantId= */ "",
            updatedDataFileList,
            inlineFileMap,
            /* customPropertyOptional= */ Optional.absent(),
            noCustomValidation())
        .get();

    // Check that downloaded file group now contains the merged file group and pending group remains
    // the same.
    DataFileGroupInternal downloadedFileGroupAfterImport =
        fileGroupsMetadata.read(getDownloadedKey(groupKey)).get();
    assertThat(downloadedFileGroupAfterImport.getBuildId())
        .isEqualTo(existingDownloadedFileGroup.getBuildId());
    assertThat(downloadedFileGroupAfterImport.getFileCount()).isEqualTo(2);
    assertThat(downloadedFileGroupAfterImport.getFileList())
        .containsExactly(existingDownloadedFileGroup.getFile(0), updatedDataFileList.get(0));
    assertThat(fileGroupsMetadata.read(getPendingKey(groupKey)).get())
        .isEqualTo(existingPendingFileGroup);
  }

  @Test
  public void testImportFilesIntoFileGroup_whenMatchesPendingButNotDownloaded_importsToPending()
      throws Exception {
    // Set up an existing pending file group, an existing downloaded file group and a new file
    // group that matches the pending file group
    DataFileGroupInternal existingPendingFileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1);
    DataFileGroupInternal existingDownloadedFileGroup =
        existingPendingFileGroup.toBuilder()
            .clearFile()
            .addFile(MddTestUtil.createDataFile("downloaded-file", 0))
            .setBuildId(10)
            .build();
    ImmutableList<DataFile> updatedDataFileList =
        ImmutableList.of(
            DataFile.newBuilder()
                .setFileId("inline-file")
                .setChecksum("abc")
                .setUrlToDownload("inlinefile:abc")
                .build());
    ImmutableMap<String, FileSource> inlineFileMap =
        ImmutableMap.of(
            "inline-file", FileSource.ofByteString(ByteString.copyFromUtf8("TEST_CONTENT")));

    GroupKey groupKey = GroupKey.newBuilder().setGroupName(TEST_GROUP).build();

    writePendingFileGroup(getPendingKey(groupKey), existingPendingFileGroup);
    writeDownloadedFileGroup(getDownloadedKey(groupKey), existingDownloadedFileGroup);

    writeSharedFiles(
        sharedFilesMetadata,
        existingPendingFileGroup,
        ImmutableList.of(FileStatus.DOWNLOAD_IN_PROGRESS));
    writeSharedFiles(
        sharedFilesMetadata,
        existingDownloadedFileGroup,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE));

    // TODO: remove once SFM can perform import
    // write inline file as downloaded so FGM can find it
    NewFileKey newInlineFileKey =
        SharedFilesMetadata.createKeyFromDataFile(
            updatedDataFileList.get(0), existingPendingFileGroup.getAllowedReadersEnum());
    SharedFile newInlineSharedFile =
        SharedFile.newBuilder()
            .setFileName(updatedDataFileList.get(0).getFileId())
            .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
            .build();
    sharedFilesMetadata.write(newInlineFileKey, newInlineSharedFile).get();

    fileGroupManager
        .importFilesIntoFileGroup(
            groupKey,
            /* buildId= */ 0,
            /* variantId= */ "",
            updatedDataFileList,
            inlineFileMap,
            /* customPropertyOptional= */ Optional.absent(),
            noCustomValidation())
        .get();

    // Check that pending file group now contains the merged file group and downloaded group remains
    // the same.
    DataFileGroupInternal pendingFileGroupAfterImport =
        fileGroupsMetadata.read(getPendingKey(groupKey)).get();
    assertThat(pendingFileGroupAfterImport.getBuildId())
        .isEqualTo(existingPendingFileGroup.getBuildId());
    assertThat(pendingFileGroupAfterImport.getFileCount()).isEqualTo(2);
    assertThat(pendingFileGroupAfterImport.getFileList())
        .containsExactly(existingPendingFileGroup.getFile(0), updatedDataFileList.get(0));
    assertThat(fileGroupsMetadata.read(getDownloadedKey(groupKey)).get())
        .isEqualTo(existingDownloadedFileGroup);
  }

  @Test
  public void testImportFilesIntoFileGroup_whenPerformingImport_choosesFileSourceById()
      throws Exception {
    // Use mockSharedFileManager to check startImport call
    resetFileGroupManager(fileGroupsMetadata, mockSharedFileManager);

    FileSource testFileSource =
        FileSource.ofByteString(ByteString.copyFromUtf8("TEST_FILE_SOURCE"));

    // Setup file group with inline file to import
    DataFileGroupInternal fileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 0).toBuilder()
            .addFile(
                DataFile.newBuilder()
                    .setFileId("inline-file")
                    .setChecksum("abc")
                    .setUrlToDownload("inlinefile:sha1:abc")
                    .build())
            .build();

    GroupKey groupKey = GroupKey.newBuilder().setGroupName(TEST_GROUP).build();
    NewFileKey inlineFileKey =
        SharedFilesMetadata.createKeyFromDataFile(
            fileGroup.getFile(0), fileGroup.getAllowedReadersEnum());

    writePendingFileGroup(getPendingKey(groupKey), fileGroup);

    // Setup mock SFM with successful calls
    when(mockSharedFileManager.reserveFileEntry(inlineFileKey))
        .thenReturn(Futures.immediateFuture(true));
    when(mockSharedFileManager.getFileStatus(inlineFileKey))
        .thenReturn(Futures.immediateFuture(FileStatus.DOWNLOAD_IN_PROGRESS));
    when(mockSharedFileManager.startImport(
            groupKeyCaptor.capture(),
            eq(fileGroup.getFile(0)),
            eq(inlineFileKey),
            any(),
            fileSourceCaptor.capture()))
        .thenReturn(Futures.immediateVoidFuture());

    fileGroupManager
        .importFilesIntoFileGroup(
            groupKey,
            /* buildId= */ 0,
            /* variantId= */ "",
            /* updatedDataFileList= */ ImmutableList.of(),
            /* inlineFileMap= */ ImmutableMap.of("inline-file", testFileSource),
            /* customPropertyOptional= */ Optional.absent(),
            noCustomValidation())
        .get();

    // Check that SFM startImport was called with expected inputs
    verify(mockSharedFileManager, times(1)).startImport(any(), any(), any(), any(), any());
    assertThat(groupKeyCaptor.getValue().getGroupName()).isEqualTo(TEST_GROUP);
    assertThat(fileSourceCaptor.getValue()).isEqualTo(testFileSource);
  }

  @Test
  public void testImportFilesIntoFileGroup_whenFileAlreadyDownloaded_doesNotAttemptToImport()
      throws Exception {
    // Use mockSharedFileManager to check startImport call
    resetFileGroupManager(fileGroupsMetadata, mockSharedFileManager);

    FileSource testFileSource =
        FileSource.ofByteString(ByteString.copyFromUtf8("TEST_FILE_SOURCE"));

    // Create an existing downloaded file group and attempt to import again with a given source.
    // Since the file is already marked DOWNLOAD_COMPLETE, the import should not be invoked.
    DataFileGroupInternal existingDownloadedFileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 0).toBuilder()
            .addFile(
                DataFile.newBuilder()
                    .setFileId("inline-file")
                    .setChecksum("abc")
                    .setUrlToDownload("inlinefile:abc")
                    .build())
            .build();

    GroupKey groupKey = GroupKey.newBuilder().setGroupName(TEST_GROUP).build();
    NewFileKey inlineFileKey =
        SharedFilesMetadata.createKeyFromDataFile(
            existingDownloadedFileGroup.getFile(0),
            existingDownloadedFileGroup.getAllowedReadersEnum());

    writeDownloadedFileGroup(getDownloadedKey(groupKey), existingDownloadedFileGroup);

    // Setup mock SFM with successful calls
    when(mockSharedFileManager.reserveFileEntry(inlineFileKey))
        .thenReturn(Futures.immediateFuture(true));
    when(mockSharedFileManager.getFileStatus(inlineFileKey))
        .thenReturn(Futures.immediateFuture(FileStatus.DOWNLOAD_COMPLETE));

    fileGroupManager
        .importFilesIntoFileGroup(
            groupKey,
            /* buildId= */ 0,
            /* variantId= */ "",
            /* updatedDataFileList= */ ImmutableList.of(),
            /* inlineFileMap= */ ImmutableMap.of("inline-file", testFileSource),
            /* customPropertyOptional= */ Optional.absent(),
            noCustomValidation())
        .get();

    // Check that SFM startImport was not called
    verify(mockSharedFileManager, times(0)).startImport(any(), any(), any(), any(), any());
  }

  @Test
  public void testImportFilesIntoFileGroups_whenFileSourceNotProvided_fails() throws Exception {
    // create a file group added to MDD with an inline file and check that import call fails if
    // source is not provided.
    DataFileGroupInternal existingFileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 0).toBuilder()
            .addFile(
                DataFile.newBuilder()
                    .setFileId("inline-file")
                    .setChecksum("abc")
                    .setUrlToDownload("inlinefile:abc")
                    .build())
            .build();

    GroupKey groupKey = GroupKey.newBuilder().setGroupName(TEST_GROUP).build();

    writePendingFileGroup(getPendingKey(groupKey), existingFileGroup);

    writeSharedFiles(
        sharedFilesMetadata, existingFileGroup, ImmutableList.of(FileStatus.DOWNLOAD_IN_PROGRESS));

    ExecutionException ex =
        assertThrows(
            ExecutionException.class,
            () ->
                fileGroupManager
                    .importFilesIntoFileGroup(
                        groupKey,
                        /* buildId= */ 0,
                        /* variantId= */ "",
                        /* updatedDataFileList= */ ImmutableList.of(),
                        /* inlineFileMap= */ ImmutableMap.of(),
                        /* customPropertyOptional= */ Optional.absent(),
                        noCustomValidation())
                    .get());
    assertThat(ex).hasCauseThat().isInstanceOf(AggregateException.class);
    AggregateException aex = (AggregateException) ex.getCause();
    assertThat(aex.getFailures()).hasSize(1);
    assertThat(aex.getFailures().get(0)).isInstanceOf(DownloadException.class);
    DownloadException dex = (DownloadException) aex.getFailures().get(0);
    assertThat(dex.getDownloadResultCode())
        .isEqualTo(DownloadResultCode.MISSING_INLINE_FILE_SOURCE);
  }

  @Test
  public void testImportFilesIntoFileGroup_whenImportFails_preventsMetadataUpdate()
      throws Exception {
    // Use mockSharedFileManager to mock a failure for an import
    resetFileGroupManager(fileGroupsMetadata, mockSharedFileManager);

    FileSource testFileSource1 =
        FileSource.ofByteString(ByteString.copyFromUtf8("TEST_FILE_SOURCE_1"));
    FileSource testFileSource2 =
        FileSource.ofByteString(ByteString.copyFromUtf8("TEST_FILE_SOURCE_2"));

    // Setup empty file group
    DataFileGroupInternal fileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 0);

    // Setup list of files that should be imported
    ImmutableList<DataFile> updatedDataFileList =
        ImmutableList.of(
            DataFile.newBuilder()
                .setFileId("inline-file-1")
                .setChecksum("abc")
                .setUrlToDownload("inlinefile:sha1:abc")
                .build(),
            DataFile.newBuilder()
                .setFileId("inline-file-2")
                .setChecksum("def")
                .setUrlToDownload("inlinefile:sha1:def")
                .build());

    GroupKey groupKey = GroupKey.newBuilder().setGroupName(TEST_GROUP).build();
    NewFileKey inlineFileKey1 =
        SharedFilesMetadata.createKeyFromDataFile(
            updatedDataFileList.get(0), fileGroup.getAllowedReadersEnum());
    NewFileKey inlineFileKey2 =
        SharedFilesMetadata.createKeyFromDataFile(
            updatedDataFileList.get(1), fileGroup.getAllowedReadersEnum());

    writeDownloadedFileGroup(getDownloadedKey(groupKey), fileGroup);

    // Setup mock calls to SFM
    when(mockSharedFileManager.reserveFileEntry(any())).thenReturn(Futures.immediateFuture(true));

    // Mock that inline file 1 completed, but inline file 2 failed
    when(mockSharedFileManager.getFileStatus(inlineFileKey1))
        .thenReturn(Futures.immediateFuture(FileStatus.DOWNLOAD_IN_PROGRESS));
    when(mockSharedFileManager.getFileStatus(inlineFileKey2))
        .thenReturn(Futures.immediateFuture(FileStatus.DOWNLOAD_FAILED));
    when(mockSharedFileManager.startImport(
            any(), eq(updatedDataFileList.get(0)), eq(inlineFileKey1), any(), any()))
        .thenReturn(Futures.immediateVoidFuture());
    when(mockSharedFileManager.startImport(
            any(), eq(updatedDataFileList.get(1)), eq(inlineFileKey2), any(), any()))
        .thenReturn(
            Futures.immediateFailedFuture(
                DownloadException.builder()
                    .setDownloadResultCode(DownloadResultCode.DOWNLOAD_TRANSFORM_IO_ERROR)
                    .build()));

    ExecutionException ex =
        assertThrows(
            ExecutionException.class,
            () ->
                fileGroupManager
                    .importFilesIntoFileGroup(
                        groupKey,
                        /* buildId= */ 0,
                        /* variantId= */ "",
                        updatedDataFileList,
                        /* inlineFileMap= */ ImmutableMap.of(
                            "inline-file-1", testFileSource1, "inline-file-2", testFileSource2),
                        /* customPropertyOptional= */ Optional.absent(),
                        noCustomValidation())
                    .get());

    // Check for expected cause
    assertThat(ex).hasCauseThat().isInstanceOf(AggregateException.class);
    AggregateException aex = (AggregateException) ex.getCause();
    assertThat(aex.getFailures()).hasSize(1);
    assertThat(aex.getFailures().get(0)).isInstanceOf(DownloadException.class);
    DownloadException dex = (DownloadException) aex.getFailures().get(0);
    assertThat(dex.getDownloadResultCode())
        .isEqualTo(DownloadResultCode.DOWNLOAD_TRANSFORM_IO_ERROR);

    // Check that existing (empty) group remains in metadata. iow, the files from
    // updatedDataFileList were not added since the import failed.
    DataFileGroupInternal existingFileGroup =
        fileGroupsMetadata.read(getDownloadedKey(groupKey)).get();
    assertThat(existingFileGroup.getFileList()).isEmpty();

    // Check that SFM startImport was called with expected inputs
    verify(mockSharedFileManager, times(2)).startImport(any(), any(), any(), any(), any());
  }

  @Test
  public void testImportFilesIntoFileGroup_skipsSideloadedFile() throws Exception {
    // Create sideloaded group with inline file
    DataFileGroupInternal sideloadedGroup =
        DataFileGroupInternal.newBuilder()
            .setGroupName(TEST_GROUP)
            .addFile(
                DataFile.newBuilder()
                    .setFileId("sideloaded_file")
                    .setUrlToDownload("file:/test")
                    .setChecksumType(DataFile.ChecksumType.NONE)
                    .build())
            .addFile(
                DataFile.newBuilder()
                    .setFileId("inline_file")
                    .setUrlToDownload("inlinefile:sha1:checksum")
                    .setChecksum("checksum")
                    .build())
            .build();
    ImmutableMap<String, FileSource> inlineFileMap =
        ImmutableMap.of(
            "inline_file", FileSource.ofByteString(ByteString.copyFromUtf8("TEST_CONTENT")));
    NewFileKey inlineFileKey =
        SharedFilesMetadata.createKeyFromDataFile(
            sideloadedGroup.getFile(1), sideloadedGroup.getAllowedReadersEnum());

    // Write group as pending since we are waiting on inline file
    writePendingFileGroup(testKey, sideloadedGroup);

    // Write inline file as succeeded so we skip SFM's import call
    SharedFile inlineSharedFile =
        SharedFile.newBuilder()
            .setFileName(sideloadedGroup.getFile(1).getFileId())
            .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
            .build();
    sharedFilesMetadata.write(inlineFileKey, inlineSharedFile).get();

    fileGroupManager
        .importFilesIntoFileGroup(
            testKey,
            sideloadedGroup.getBuildId(),
            sideloadedGroup.getVariantId(),
            /* updatedDataFileList= */ ImmutableList.of(),
            inlineFileMap,
            /* customPropertyOptional= */ Optional.absent(),
            noCustomValidation())
        .get();

    assertThat(readPendingFileGroup(testKey)).isNull();
    assertThat(readDownloadedFileGroup(testKey)).isNotNull();
  }

  @Test
  public void testDownloadPendingGroup_success() throws Exception {
    // Write 1 group to the pending shared prefs.
    DataFileGroupInternal fileGroup =
        createDataFileGroup(
            TEST_GROUP,
            /* fileCount= */ 2,
            /* downloadAttemptCount= */ 3,
            /* newFilesReceivedTimestamp= */ testClock.currentTimeMillis() - 500L);
    ExtraHttpHeader extraHttpHeader =
        ExtraHttpHeader.newBuilder().setKey("user-agent").setValue("mdd-downloader").build();

    fileGroup =
        fileGroup.toBuilder()
            .setOwnerPackage(context.getPackageName())
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .setTrafficTag(TRAFFIC_TAG)
            .addGroupExtraHttpHeaders(extraHttpHeader)
            .build();
    writePendingFileGroup(testKey, fileGroup);

    writeSharedFiles(
        sharedFilesMetadata,
        fileGroup,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE, FileStatus.DOWNLOAD_COMPLETE));

    fileGroupManager
        .downloadFileGroup(testKey, DownloadConditions.getDefaultInstance(), noCustomValidation())
        .get();

    // Verify that pending key is removed if download is complete.
    assertThat(readPendingFileGroup(testKey)).isNull();

    // Verify that downloaded key is written into metadata if download is complete.
    assertThat(readDownloadedFileGroup(testKey)).isNotNull();
    verify(mockLogger)
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            TEST_GROUP,
            0,
            /* buildId= */ 0,
            /* variantId= */ "");
    verify(mockLogger)
        .logMddDownloadResult(
            MddDownloadResult.Code.SUCCESS,
            createFileGroupDetails(fileGroup)
                .setOwnerPackage(context.getPackageName())
                .clearFileCount()
                .build());
    verify(mockLogger)
        .logMddDownloadLatency(
            createFileGroupDetails(fileGroup).build(),
            createMddDownloadLatency(
                /* downloadAttemptCount= */ 4,
                /* downloadLatencyMs= */ 0L,
                /* totalLatencyMs= */ 500L));
  }

  @Test
  public void testDownloadPendingGroup_withFailingCustomValidator() throws Exception {
    // Write 1 group to the pending shared prefs.
    DataFileGroupInternal fileGroup =
        createDataFileGroup(
            TEST_GROUP,
            /* fileCount= */ 2,
            /* downloadAttemptCount= */ 3,
            /* newFilesReceivedTimestamp= */ testClock.currentTimeMillis() - 500L);
    ExtraHttpHeader extraHttpHeader =
        ExtraHttpHeader.newBuilder().setKey("user-agent").setValue("mdd-downloader").build();

    fileGroup =
        fileGroup.toBuilder()
            .setOwnerPackage(context.getPackageName())
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .setTrafficTag(TRAFFIC_TAG)
            .addGroupExtraHttpHeaders(extraHttpHeader)
            .build();
    writePendingFileGroup(testKey, fileGroup);

    writeSharedFiles(
        sharedFilesMetadata,
        fileGroup,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE, FileStatus.DOWNLOAD_COMPLETE));

    AsyncFunction<DataFileGroupInternal, Boolean> failingValidator =
        unused -> Futures.immediateFuture(false);
    ListenableFuture<DataFileGroupInternal> downloadFuture =
        fileGroupManager.downloadFileGroup(
            testKey, DownloadConditions.getDefaultInstance(), failingValidator);

    ExecutionException exception = assertThrows(ExecutionException.class, downloadFuture::get);
    assertThat(exception).hasCauseThat().isInstanceOf(DownloadException.class);
    DownloadException cause = (DownloadException) exception.getCause();
    assertThat(cause).isNotNull();
    assertThat(cause).hasMessageThat().contains("CUSTOM_FILEGROUP_VALIDATION_FAILED");

    // Verify that pending key was removed. This will ensure the files are eligible for garbage
    // collection.
    assertThat(readPendingFileGroup(testKey)).isNull();

    verify(mockLogger)
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            TEST_GROUP,
            /* fileGroupVersionNumber= */ 0,
            /* buildId= */ 0,
            /* variantId= */ "");

    ArgumentCaptor<MddDownloadResult.Code> resultCodeCaptor =
        ArgumentCaptor.forClass(MddDownloadResult.Code.class);
    ArgumentCaptor<DataDownloadFileGroupStats> groupDetailsCaptor =
        ArgumentCaptor.forClass(DataDownloadFileGroupStats.class);
    verify(mockLogger)
        .logMddDownloadResult(resultCodeCaptor.capture(), groupDetailsCaptor.capture());

    // Also clearing the file group version number becaused it ends up not being attached since
    // the pending group was removed.
    DataDownloadFileGroupStats expectedGroupDetails =
        createFileGroupDetails(fileGroup).clearFileCount().clearFileGroupVersionNumber().build();

    assertThat(resultCodeCaptor.getAllValues())
        .containsExactly(MddDownloadResult.Code.CUSTOM_FILEGROUP_VALIDATION_FAILED);
    assertThat(groupDetailsCaptor.getAllValues()).containsExactly(expectedGroupDetails);
  }

  @Test
  public void testDownloadFileGroup_failed() throws Exception {
    // Write 1 group to the pending shared prefs.
    DataFileGroupInternal fileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2).toBuilder()
            .setVariantId("test-variant")
            .setBuildId(10)
            .build();
    NewFileKey[] keys = MddTestUtil.createFileKeysForDataFileGroupInternal(fileGroup);
    fileGroup =
        fileGroup.toBuilder()
            .setOwnerPackage(context.getPackageName())
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .build();
    writePendingFileGroup(testKey, fileGroup);

    // Not all files are downloaded.
    writeSharedFiles(
        sharedFilesMetadata,
        fileGroup,
        ImmutableList.of(FileStatus.SUBSCRIBED, FileStatus.SUBSCRIBED));

    // First file failed.
    Uri failingFileUri =
        DirectoryUtil.getOnDeviceUri(
            context,
            keys[0].getAllowedReaders(),
            fileGroup.getFile(0).getFileId(),
            fileGroup.getFile(0).getChecksum(),
            mockSilentFeedback,
            /* instanceId= */ Optional.absent(),
            false);
    fileDownloadFails(keys[0], failingFileUri, DownloadResultCode.LOW_DISK_ERROR);

    // Second file succeeded.
    Uri succeedingFileUri =
        DirectoryUtil.getOnDeviceUri(
            context,
            keys[1].getAllowedReaders(),
            fileGroup.getFile(1).getFileId(),
            fileGroup.getFile(1).getChecksum(),
            mockSilentFeedback,
            /* instanceId= */ Optional.absent(),
            false);
    fileDownloadSucceeds(keys[1], succeedingFileUri);

    ListenableFuture<DataFileGroupInternal> downloadFuture =
        fileGroupManager.downloadFileGroup(
            testKey, DownloadConditions.getDefaultInstance(), noCustomValidation());

    ExecutionException exception = assertThrows(ExecutionException.class, downloadFuture::get);
    assertThat(exception).hasCauseThat().isInstanceOf(AggregateException.class);
    AggregateException cause = (AggregateException) exception.getCause();
    assertThat(cause).isNotNull();
    ImmutableList<Throwable> failures = cause.getFailures();
    assertThat(failures).hasSize(1);
    assertThat(failures.get(0)).isInstanceOf(DownloadException.class);
    assertThat(failures.get(0)).hasMessageThat().contains("LOW_DISK_ERROR");

    // Verify that the pending group is still part of pending groups prefs.
    assertThat(readPendingFileGroup(testKey)).isNotNull();

    // Verify that the pending group is not changed from pending to downloaded.
    assertThat(readDownloadedFileGroup(testKey)).isNull();

    verify(mockLogger)
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            TEST_GROUP,
            /* fileGroupVersionNumber= */ 0,
            /* buildId= */ 10,
            /* variantId= */ "test-variant");

    ArgumentCaptor<MddDownloadResult.Code> resultCodeCaptor =
        ArgumentCaptor.forClass(MddDownloadResult.Code.class);
    ArgumentCaptor<DataDownloadFileGroupStats> groupDetailsCaptor =
        ArgumentCaptor.forClass(DataDownloadFileGroupStats.class);
    verify(mockLogger)
        .logMddDownloadResult(resultCodeCaptor.capture(), groupDetailsCaptor.capture());

    DataDownloadFileGroupStats expectedGroupDetails =
        createFileGroupDetails(fileGroup).clearFileCount().build();

    assertThat(resultCodeCaptor.getAllValues())
        .containsExactly(MddDownloadResult.Code.LOW_DISK_ERROR);
    assertThat(groupDetailsCaptor.getAllValues()).containsExactly(expectedGroupDetails);
  }

  @Test
  public void testDownloadFileGroup_failedWithMultipleExceptions() throws Exception {
    // Write 1 group to the pending shared prefs.
    DataFileGroupInternal fileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 3);
    NewFileKey[] keys = MddTestUtil.createFileKeysForDataFileGroupInternal(fileGroup);
    fileGroup =
        fileGroup.toBuilder()
            .setOwnerPackage(context.getPackageName())
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .build();
    writePendingFileGroup(testKey, fileGroup);

    // Not all files are downloaded.
    writeSharedFiles(
        sharedFilesMetadata,
        fileGroup,
        ImmutableList.of(FileStatus.SUBSCRIBED, FileStatus.SUBSCRIBED, FileStatus.SUBSCRIBED));

    // First file succeeded.
    Uri succeedingFileUri =
        DirectoryUtil.getOnDeviceUri(
            context,
            keys[0].getAllowedReaders(),
            fileGroup.getFile(0).getFileId(),
            fileGroup.getFile(0).getChecksum(),
            mockSilentFeedback,
            /* instanceId= */ Optional.absent(),
            false);
    fileDownloadSucceeds(keys[0], succeedingFileUri);

    // Second file failed with download transform I/O error.
    Uri failingFileUri1 =
        DirectoryUtil.getOnDeviceUri(
            context,
            keys[1].getAllowedReaders(),
            fileGroup.getFile(1).getFileId(),
            fileGroup.getFile(1).getChecksum(),
            mockSilentFeedback,
            /* instanceId= */ Optional.absent(),
            false);
    fileDownloadFails(keys[1], failingFileUri1, DownloadResultCode.DOWNLOAD_TRANSFORM_IO_ERROR);

    // Third file failed with android downloader http error.
    Uri failingFileUri2 =
        DirectoryUtil.getOnDeviceUri(
            context,
            keys[2].getAllowedReaders(),
            fileGroup.getFile(2).getFileId(),
            fileGroup.getFile(2).getChecksum(),
            mockSilentFeedback,
            /* instanceId= */ Optional.absent(),
            false);
    fileDownloadFails(keys[2], failingFileUri2, DownloadResultCode.ANDROID_DOWNLOADER_HTTP_ERROR);

    ListenableFuture<DataFileGroupInternal> downloadFuture =
        fileGroupManager.downloadFileGroup(
            testKey, DownloadConditions.getDefaultInstance(), noCustomValidation());

    // Ensure that all exceptions are aggregated.
    ExecutionException exception = assertThrows(ExecutionException.class, downloadFuture::get);
    assertThat(exception).hasCauseThat().isInstanceOf(AggregateException.class);
    AggregateException cause = (AggregateException) exception.getCause();
    assertThat(cause).isNotNull();
    ImmutableList<Throwable> failures = cause.getFailures();
    assertThat(failures).hasSize(2);
    assertThat(failures.get(0)).isInstanceOf(DownloadException.class);
    assertThat(failures.get(0)).hasMessageThat().contains("DOWNLOAD_TRANSFORM_IO_ERROR");
    assertThat(failures.get(1)).isInstanceOf(DownloadException.class);
    assertThat(failures.get(1)).hasMessageThat().contains("ANDROID_DOWNLOADER_HTTP_ERROR");

    // Verify that the pending group is still part of pending groups prefs.
    assertThat(readPendingFileGroup(testKey)).isNotNull();

    // Verify that the pending group is not changed from pending to downloaded.
    assertThat(readDownloadedFileGroup(testKey)).isNull();

    verify(mockLogger)
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            TEST_GROUP,
            /* fileGroupVersionNumber= */ 0,
            /* buildId= */ 0,
            /* variantId= */ "");

    ArgumentCaptor<MddDownloadResult.Code> resultCodeCaptor =
        ArgumentCaptor.forClass(MddDownloadResult.Code.class);
    ArgumentCaptor<DataDownloadFileGroupStats> groupDetailsCaptor =
        ArgumentCaptor.forClass(DataDownloadFileGroupStats.class);
    verify(mockLogger, times(2))
        .logMddDownloadResult(resultCodeCaptor.capture(), groupDetailsCaptor.capture());

    DataDownloadFileGroupStats expectedGroupDetails =
        createFileGroupDetails(fileGroup).clearFileCount().build();

    assertThat(resultCodeCaptor.getAllValues())
        .containsExactly(
            MddDownloadResult.Code.DOWNLOAD_TRANSFORM_IO_ERROR,
            MddDownloadResult.Code.ANDROID_DOWNLOADER_HTTP_ERROR);
    assertThat(groupDetailsCaptor.getAllValues())
        .containsExactly(expectedGroupDetails, expectedGroupDetails);
  }

  @Test
  public void testDownloadFileGroup_failedWithUnknownError() throws Exception {
    // Write 1 group to the pending shared prefs.
    DataFileGroupInternal fileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    NewFileKey[] keys = MddTestUtil.createFileKeysForDataFileGroupInternal(fileGroup);
    fileGroup =
        fileGroup.toBuilder()
            .setOwnerPackage(context.getPackageName())
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .build();
    writePendingFileGroup(testKey, fileGroup);

    writeSharedFiles(
        sharedFilesMetadata,
        fileGroup,
        ImmutableList.of(FileStatus.SUBSCRIBED, FileStatus.DOWNLOAD_COMPLETE));

    // First file failed.
    Uri failingFileUri =
        DirectoryUtil.getOnDeviceUri(
            context,
            keys[0].getAllowedReaders(),
            fileGroup.getFile(0).getFileId(),
            fileGroup.getFile(0).getChecksum(),
            mockSilentFeedback,
            /* instanceId= */ Optional.absent(),
            false);
    // The file status is set to DOWNLOAD_FAILED but the downloader returns an immediateVoidFuture.
    // An UNKNOWN_ERROR is logged.
    fileDownloadFails(keys[0], failingFileUri, /* failureCode= */ null);

    ListenableFuture<DataFileGroupInternal> downloadFuture =
        fileGroupManager.downloadFileGroup(
            testKey, DownloadConditions.getDefaultInstance(), noCustomValidation());

    ExecutionException exception = assertThrows(ExecutionException.class, downloadFuture::get);
    assertThat(exception).hasMessageThat().contains("UNKNOWN_ERROR");

    // Verify that the pending group is still part of pending groups prefs.
    assertThat(readPendingFileGroup(testKey)).isNotNull();

    // Verify that the pending group is not changed from pending to downloaded.
    assertThat(readDownloadedFileGroup(testKey)).isNull();

    verify(mockLogger)
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            TEST_GROUP,
            /* fileGroupVersionNumber= */ 0,
            /* buildId= */ 0,
            /* variantId= */ "");
  }

  @Test
  public void testDownloadFileGroup_pending() throws Exception {
    // Write 1 group to the pending shared prefs.
    DataFileGroupInternal fileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    fileGroup =
        fileGroup.toBuilder()
            .setOwnerPackage(context.getPackageName())
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .build();
    writePendingFileGroup(testKey, fileGroup);

    // Not all files are downloaded.
    writeSharedFiles(
        sharedFilesMetadata,
        fileGroup,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE, FileStatus.SUBSCRIBED));

    when(mockDownloader.startDownloading(
            any(String.class),
            any(GroupKey.class),
            anyInt(),
            anyLong(),
            any(String.class),
            any(Uri.class),
            any(String.class),
            anyInt(),
            any(DownloadConditions.class),
            isA(DownloaderCallbackImpl.class),
            anyInt(),
            anyList()))
        .thenReturn(Futures.immediateVoidFuture());

    ListenableFuture<DataFileGroupInternal> downloadFuture =
        fileGroupManager.downloadFileGroup(
            testKey, DownloadConditions.getDefaultInstance(), noCustomValidation());

    ExecutionException exception = assertThrows(ExecutionException.class, downloadFuture::get);
    assertThat(exception).hasMessageThat().contains("UNKNOWN_ERROR");

    // Verify that the pending group is still part of pending groups prefs.
    assertThat(readPendingFileGroup(testKey)).isNotNull();

    // Verify that the pending group is not changed from pending to downloaded.
    assertThat(readDownloadedFileGroup(testKey)).isNull();

    verify(mockLogger)
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            TEST_GROUP,
            /* fileGroupVersionNumber= */ 0,
            /* buildId= */ 0,
            /* variantId= */ "");
    verify(mockLogger)
        .logMddDownloadResult(
            MddDownloadResult.Code.UNKNOWN_ERROR,
            createFileGroupDetails(fileGroup)
                .setOwnerPackage(context.getPackageName())
                .clearFileCount()
                .build());
  }

  @Test
  public void testDownloadFileGroup_alreadyDownloaded() throws Exception {
    // Write 1 group to the downloaded shared prefs.
    DataFileGroupInternal fileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    fileGroup =
        fileGroup.toBuilder()
            .setOwnerPackage(context.getPackageName())
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .build();
    writeDownloadedFileGroup(testKey, fileGroup);

    List<GroupKey> originalKeys = fileGroupsMetadata.getAllGroupKeys().get();

    ListenableFuture<DataFileGroupInternal> downloadFuture =
        fileGroupManager.downloadFileGroup(
            testKey, DownloadConditions.getDefaultInstance(), noCustomValidation());

    assertThat(downloadFuture.get()).isEqualTo(fileGroup);

    // Verify that the downloaded group is still part of downloaded groups prefs.
    DataFileGroupInternal downloadedGroup = readDownloadedFileGroup(testKey);
    assertThat(downloadedGroup).isEqualTo(fileGroup);

    // Verify that no group metadata is written or removed.
    assertThat(originalKeys).isEqualTo(fileGroupsMetadata.getAllGroupKeys().get());
  }

  @Test
  public void testDownloadFileGroup_nullDownloadCondition() throws Exception {
    DownloadConditions downloadConditions =
        DownloadConditions.newBuilder()
            .setDeviceNetworkPolicy(DeviceNetworkPolicy.DOWNLOAD_ONLY_ON_WIFI)
            .setDeviceStoragePolicy(DeviceStoragePolicy.BLOCK_DOWNLOAD_IN_LOW_STORAGE)
            .build();

    // Write 1 group to the pending shared prefs.
    DataFileGroupInternal fileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2).toBuilder()
            .setDownloadConditions(downloadConditions)
            .build();
    writePendingFileGroup(testKey, fileGroup);

    writeSharedFiles(
        sharedFilesMetadata,
        fileGroup,
        ImmutableList.of(FileStatus.SUBSCRIBED, FileStatus.SUBSCRIBED));

    ArgumentCaptor<DownloadConditions> downloadConditionsCaptor =
        ArgumentCaptor.forClass(DownloadConditions.class);
    when(mockDownloader.startDownloading(
            any(String.class),
            any(GroupKey.class),
            anyInt(),
            anyLong(),
            any(String.class),
            any(Uri.class),
            any(String.class),
            anyInt(),
            downloadConditionsCaptor.capture(),
            isA(DownloaderCallbackImpl.class),
            anyInt(),
            anyList()))
        .then(
            new Answer<ListenableFuture<Void>>() {
              @Override
              public ListenableFuture<Void> answer(InvocationOnMock invocation) throws Throwable {
                writeSharedFiles(
                    sharedFilesMetadata,
                    fileGroup,
                    ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE, FileStatus.DOWNLOAD_COMPLETE));
                return Futures.immediateVoidFuture();
              }
            });

    DataFileGroupInternal updatedFileGroup =
        fileGroup.toBuilder()
            .setBookkeeping(
                DataFileGroupBookkeeping.newBuilder()
                    .setDownloadStartedCount(1)
                    .setGroupDownloadStartedTimestampInMillis(1000L))
            .build();

    // Calling with DownloadConditions = null will use the config from server.
    assertThat(
            fileGroupManager
                .downloadFileGroup(testKey, null /*downloadConditions*/, noCustomValidation())
                .get())
        .isEqualTo(updatedFileGroup);
    assertThat(downloadConditionsCaptor.getValue()).isEqualTo(downloadConditions);
  }

  @Test
  public void testDownloadFileGroup_nonNullDownloadCondition() throws Exception {
    DownloadConditions downloadConditions =
        DownloadConditions.newBuilder()
            .setDeviceNetworkPolicy(DeviceNetworkPolicy.DOWNLOAD_ONLY_ON_WIFI)
            .setDeviceStoragePolicy(DeviceStoragePolicy.BLOCK_DOWNLOAD_IN_LOW_STORAGE)
            .build();

    // Write 1 group to the pending shared prefs.
    DataFileGroupInternal fileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2).toBuilder()
            .setDownloadConditions(downloadConditions)
            .build();
    writePendingFileGroup(testKey, fileGroup);

    writeSharedFiles(
        sharedFilesMetadata,
        fileGroup,
        ImmutableList.of(FileStatus.SUBSCRIBED, FileStatus.SUBSCRIBED));

    ArgumentCaptor<DownloadConditions> downloadConditionsCaptor =
        ArgumentCaptor.forClass(DownloadConditions.class);
    when(mockDownloader.startDownloading(
            any(String.class),
            any(GroupKey.class),
            anyInt(),
            anyLong(),
            any(String.class),
            any(Uri.class),
            any(String.class),
            anyInt(),
            downloadConditionsCaptor.capture(),
            isA(DownloaderCallbackImpl.class),
            anyInt(),
            anyList()))
        .then(
            new Answer<ListenableFuture<Void>>() {
              @Override
              public ListenableFuture<Void> answer(InvocationOnMock invocation) throws Throwable {
                writeSharedFiles(
                    sharedFilesMetadata,
                    fileGroup,
                    ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE, FileStatus.DOWNLOAD_COMPLETE));
                return Futures.immediateVoidFuture();
              }
            });

    DownloadConditions downloadConditions2 =
        DownloadConditions.newBuilder()
            .setDeviceNetworkPolicy(DeviceNetworkPolicy.DOWNLOAD_ON_ANY_NETWORK)
            .setDeviceStoragePolicy(DeviceStoragePolicy.BLOCK_DOWNLOAD_IN_LOW_STORAGE)
            .build();

    DataFileGroupInternal updatedFileGroup =
        fileGroup.toBuilder()
            .setBookkeeping(
                DataFileGroupBookkeeping.newBuilder()
                    .setDownloadStartedCount(1)
                    .setGroupDownloadStartedTimestampInMillis(1000L))
            .build();

    // downloadConditions2 will override the pendingGroup.downloadConditions
    assertThat(
            fileGroupManager
                .downloadFileGroup(testKey, downloadConditions2, noCustomValidation())
                .get())
        .isEqualTo(updatedFileGroup);
    assertThat(downloadConditionsCaptor.getValue()).isEqualTo(downloadConditions2);
  }

  @Test
  public void testDownloadFileGroup_notFoundGroup() throws Exception {
    // Mock FileGroupsMetadata to test failure scenario.
    resetFileGroupManager(mockFileGroupsMetadata, sharedFileManager);
    // Can't find the group.
    ArgumentCaptor<GroupKey> groupKeyCaptor = ArgumentCaptor.forClass(GroupKey.class);
    when(mockFileGroupsMetadata.read(groupKeyCaptor.capture()))
        .thenReturn(Futures.immediateFuture(null));

    // Download not-found group will lead to failed future.
    ExecutionException exception =
        assertThrows(
            ExecutionException.class,
            () ->
                fileGroupManager
                    .downloadFileGroup(testKey, null /*downloadConditions*/, noCustomValidation())
                    .get());
    assertThat(exception).hasCauseThat().isInstanceOf(DownloadException.class);

    // Make sure that file group manager attempted to read both pending key and downloaded key.
    assertThat(groupKeyCaptor.getAllValues())
        .containsAtLeast(getPendingKey(testKey), getDownloadedKey(testKey));
  }

  @Test
  public void testDownloadFileGroup_downloadStartedTimestampAbsent() throws Exception {
    // Write 1 group to the pending shared prefs.
    DataFileGroupInternal fileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    fileGroup =
        fileGroup.toBuilder()
            .setOwnerPackage(context.getPackageName())
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .setTrafficTag(TRAFFIC_TAG)
            .build();
    writePendingFileGroup(testKey, fileGroup);

    writeSharedFiles(
        sharedFilesMetadata,
        fileGroup,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE, FileStatus.DOWNLOAD_COMPLETE));

    fileGroupManager
        .downloadFileGroup(testKey, DownloadConditions.getDefaultInstance(), noCustomValidation())
        .get();

    DataFileGroupBookkeeping bookkeeping = readDownloadedFileGroup(testKey).getBookkeeping();
    assertThat(bookkeeping.hasGroupDownloadStartedTimestampInMillis()).isTrue();
    // Make sure that the download started timestamp is set to current time.
    assertThat(bookkeeping.getGroupDownloadStartedTimestampInMillis())
        .isEqualTo(testClock.currentTimeMillis());
    // Make sure that the download started count is accumulated.
    assertThat(bookkeeping.getDownloadStartedCount()).isEqualTo(1);

    verify(mockLogger)
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            fileGroup.getGroupName(),
            fileGroup.getFileGroupVersionNumber(),
            /* buildId= */ 0,
            /* variantId= */ "");
  }

  @Test
  public void testDownloadFileGroup_downloadStartedTimestampPresent() throws Exception {
    // Write 1 group to the pending shared prefs.
    DataFileGroupInternal fileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);

    fileGroup =
        fileGroup.toBuilder()
            .setOwnerPackage(context.getPackageName())
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .setTrafficTag(TRAFFIC_TAG)
            .setBookkeeping(
                DataFileGroupBookkeeping.newBuilder()
                    .setGroupDownloadStartedTimestampInMillis(123456)
                    .setDownloadStartedCount(2))
            .build();
    writePendingFileGroup(testKey, fileGroup);

    writeSharedFiles(
        sharedFilesMetadata,
        fileGroup,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE, FileStatus.DOWNLOAD_COMPLETE));

    fileGroupManager
        .downloadFileGroup(testKey, DownloadConditions.getDefaultInstance(), noCustomValidation())
        .get();

    DataFileGroupBookkeeping bookkeeping = readDownloadedFileGroup(testKey).getBookkeeping();
    assertThat(bookkeeping.hasGroupDownloadStartedTimestampInMillis()).isTrue();
    // Make sure that the download started timestamp is not changed.
    assertThat(bookkeeping.getGroupDownloadStartedTimestampInMillis()).isEqualTo(123456);
    // Make sure that the download started count is accumulated.
    assertThat(bookkeeping.getDownloadStartedCount()).isEqualTo(3);

    verify(mockLogger, never())
        .logEventSampled(
            eq(MddClientEvent.Code.EVENT_CODE_UNSPECIFIED),
            any(String.class),
            anyInt(),
            anyLong(),
            any(String.class));
  }

  @Test
  public void testDownloadFileGroup_updateBookkeepingOnDownloadFailed() throws Exception {
    // Mock FileGroupsMetadata to test failure scenario.
    resetFileGroupManager(mockFileGroupsMetadata, sharedFileManager);
    // Write 1 group to the pending shared prefs.
    DataFileGroupInternal fileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    fileGroup =
        fileGroup.toBuilder()
            .setOwnerPackage(context.getPackageName())
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .setTrafficTag(TRAFFIC_TAG)
            .build();
    GroupKey pendingKey = testKey.toBuilder().setDownloaded(false).build();
    when(mockFileGroupsMetadata.read(pendingKey)).thenReturn(Futures.immediateFuture(fileGroup));

    // All files are downloaded.
    writeSharedFiles(
        sharedFilesMetadata,
        fileGroup,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE, FileStatus.DOWNLOAD_COMPLETE));

    ArgumentCaptor<DataFileGroupInternal> fileGroupCaptor =
        ArgumentCaptor.forClass(DataFileGroupInternal.class);
    when(mockFileGroupsMetadata.write(eq(pendingKey), fileGroupCaptor.capture()))
        .thenReturn(Futures.immediateFuture(false));

    ExecutionException executionException =
        assertThrows(
            ExecutionException.class,
            () ->
                fileGroupManager
                    .downloadFileGroup(
                        testKey, DownloadConditions.getDefaultInstance(), noCustomValidation())
                    .get());
    assertThat(executionException).hasCauseThat().isInstanceOf(DownloadException.class);
    DownloadException downloadException = (DownloadException) executionException.getCause();
    assertThat(downloadException).hasCauseThat().isInstanceOf(IOException.class);
    assertThat(downloadException.getDownloadResultCode())
        .isEqualTo(DownloadResultCode.UNABLE_TO_UPDATE_GROUP_METADATA_ERROR);

    DataFileGroupBookkeeping bookkeeping = fileGroupCaptor.getValue().getBookkeeping();
    assertThat(bookkeeping.hasGroupDownloadStartedTimestampInMillis()).isTrue();
    assertThat(bookkeeping.getGroupDownloadStartedTimestampInMillis())
        .isEqualTo(testClock.currentTimeMillis());

    verify(mockLogger, never())
        .logEventSampled(
            eq(MddClientEvent.Code.EVENT_CODE_UNSPECIFIED),
            any(String.class),
            anyInt(),
            anyLong(),
            any(String.class));
    verify(mockLogger).logEventSampled(MddClientEvent.Code.EVENT_CODE_UNSPECIFIED);
  }

  @Test
  public void testDownloadToBeSharedPendingGroup_success_lowSdk_notShared() throws Exception {
    ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.R - 1);
    // Write 1 group to the pending shared prefs.
    DataFileGroupInternal fileGroup =
        createDataFileGroup(
            TEST_GROUP,
            /* fileCount= */ 0,
            /* downloadAttemptCount= */ 3,
            /* newFilesReceivedTimestamp= */ testClock.currentTimeMillis() - 500L);
    ExtraHttpHeader extraHttpHeader =
        ExtraHttpHeader.newBuilder().setKey("user-agent").setValue("mdd-downloader").build();

    fileGroup =
        fileGroup.toBuilder()
            .setOwnerPackage(context.getPackageName())
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .setTrafficTag(TRAFFIC_TAG)
            .addGroupExtraHttpHeaders(extraHttpHeader)
            .addFile(0, MddTestUtil.createSharedDataFile(TEST_GROUP, 0))
            .addFile(1, MddTestUtil.createDataFile(TEST_GROUP, 1))
            .build();
    writePendingFileGroup(testKey, fileGroup);

    writeSharedFiles(
        sharedFilesMetadata,
        fileGroup,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE, FileStatus.DOWNLOAD_COMPLETE));

    fileGroupManager
        .downloadFileGroup(testKey, DownloadConditions.getDefaultInstance(), noCustomValidation())
        .get();

    // Verify that pending key is removed if download is complete.
    assertThat(readPendingFileGroup(testKey)).isNull();

    // Verify that downloaded key is written into metadata if download is complete.
    assertThat(readDownloadedFileGroup(testKey)).isNotNull();
    verify(mockLogger)
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            TEST_GROUP,
            /* fileGroupVersionNumber= */ 0,
            /* buildId= */ 0,
            /* variantId= */ "");
    verify(mockLogger)
        .logMddDownloadResult(
            MddDownloadResult.Code.SUCCESS,
            createFileGroupDetails(fileGroup)
                .setOwnerPackage(context.getPackageName())
                .clearFileCount()
                .build());

    verify(mockLogger)
        .logMddDownloadLatency(
            createFileGroupDetails(fileGroup).build(),
            createMddDownloadLatency(
                /* downloadAttemptCount= */ 4,
                /* downloadLatencyMs= */ 0L,
                /* totalLatencyMs= */ 500L));

    // exists only called once in tryToShareBeforeDownload
    verify(mockBackend, never()).exists(any());
    // openForWrite is called in tryToShareBeforeDownload for copying the file and acquiring the
    // lease.
    verify(mockBackend, never()).openForWrite(any());
  }

  @Test
  public void testDownloadFileGroup_success_oneFileAndroidSharedAndDownloaded() throws Exception {
    // Write 1 group to the pending shared prefs.
    DataFileGroupInternal fileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2).toBuilder()
            .setVariantId("test-variant")
            .setBuildId(10)
            .build();
    NewFileKey[] keys = MddTestUtil.createFileKeysForDataFileGroupInternal(fileGroup);
    ExtraHttpHeader extraHttpHeader =
        ExtraHttpHeader.newBuilder().setKey("user-agent").setValue("mdd-downloader").build();

    fileGroup =
        fileGroup.toBuilder()
            .setOwnerPackage(context.getPackageName())
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .setTrafficTag(TRAFFIC_TAG)
            .addGroupExtraHttpHeaders(extraHttpHeader)
            .build();
    writePendingFileGroup(testKey, fileGroup);

    writeSharedFiles(
        sharedFilesMetadata,
        fileGroup,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE, FileStatus.DOWNLOAD_COMPLETE),
        /* androidShared */ ImmutableList.of(true, false));

    SharedFile file0 = sharedFileManager.getSharedFile(keys[0]).get();
    SharedFile file1 = sharedFileManager.getSharedFile(keys[1]).get();
    Uri blobUri = DirectoryUtil.getBlobUri(context, file0.getAndroidSharingChecksum());
    Uri leaseUri =
        DirectoryUtil.getBlobStoreLeaseUri(context, file0.getAndroidSharingChecksum(), 0);

    fileGroupManager
        .downloadFileGroup(testKey, DownloadConditions.getDefaultInstance(), noCustomValidation())
        .get();

    // Verify that pending key is removed if download is complete.
    assertThat(readPendingFileGroup(testKey)).isNull();

    // Verify that downloaded key is written into metadata if download is complete.
    assertThat(readDownloadedFileGroup(testKey)).isNotNull();
    verify(mockLogger)
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            TEST_GROUP,
            /* fileGroupVersionNumber= */ 0,
            /* buildId= */ 10,
            /* variantId= */ "test-variant");

    verify(mockBackend, never()).exists(blobUri);
    verify(mockBackend, never()).openForWrite(blobUri);
    verify(mockBackend, never()).openForWrite(leaseUri);

    assertThat(sharedFileManager.getSharedFile(keys[0]).get()).isEqualTo(file0);
    assertThat(sharedFileManager.getSharedFile(keys[1]).get()).isEqualTo(file1);

    ArgumentCaptor<Void> mddAndroidSharingLogArgumentCaptor = ArgumentCaptor.forClass(Void.class);

    verify(mockLogger).logMddAndroidSharingLog(mddAndroidSharingLogArgumentCaptor.capture());
    Void mddAndroidSharingLog = null;
    Void expectedLog = null;
    assertThat(mddAndroidSharingLog).isEqualTo(expectedLog);
  }

  @Test
  public void testDownloadFileGroup_pending_oneBlobExistsBeforeDownload() throws Exception {
    // Write 1 group to the pending shared prefs.
    DataFileGroupInternal fileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 0);
    ExtraHttpHeader extraHttpHeader =
        ExtraHttpHeader.newBuilder().setKey("user-agent").setValue("mdd-downloader").build();

    fileGroup =
        fileGroup.toBuilder()
            .setOwnerPackage(context.getPackageName())
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .setExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS)
            .setTrafficTag(TRAFFIC_TAG)
            .addGroupExtraHttpHeaders(extraHttpHeader)
            .addFile(0, MddTestUtil.createSharedDataFile(TEST_GROUP, 0))
            .addFile(1, MddTestUtil.createDataFile(TEST_GROUP, 1))
            .build();

    NewFileKey[] keys = MddTestUtil.createFileKeysForDataFileGroupInternal(fileGroup);
    writePendingFileGroup(testKey, fileGroup);

    writeSharedFiles(
        sharedFilesMetadata,
        fileGroup,
        ImmutableList.of(FileStatus.SUBSCRIBED, FileStatus.SUBSCRIBED));

    // File that can be shared
    DataFile file = fileGroup.getFile(0);
    Uri blobUri = DirectoryUtil.getBlobUri(context, file.getAndroidSharingChecksum());
    Uri leaseUri =
        DirectoryUtil.getBlobStoreLeaseUri(
            context, file.getAndroidSharingChecksum(), FILE_GROUP_EXPIRATION_DATE_SECS);
    // The file is available in the blob storage
    when(mockBackend.exists(blobUri)).thenReturn(true);

    // First file's download succeeds
    Uri onDeviceuri =
        DirectoryUtil.getOnDeviceUri(
            context,
            keys[0].getAllowedReaders(),
            file.getFileId(),
            keys[0].getChecksum(),
            mockSilentFeedback,
            /* instanceId= */ Optional.absent(),
            /* androidShared= */ false);
    fileDownloadSucceeds(keys[0], onDeviceuri);

    // Second file's download succeeds
    onDeviceuri =
        DirectoryUtil.getOnDeviceUri(
            context,
            keys[1].getAllowedReaders(),
            fileGroup.getFile(1).getFileId(),
            keys[1].getChecksum(),
            mockSilentFeedback,
            /* instanceId= */ Optional.absent(),
            /* androidShared= */ false);
    fileDownloadSucceeds(keys[1], onDeviceuri);

    fileGroupManager
        .downloadFileGroup(testKey, DownloadConditions.getDefaultInstance(), noCustomValidation())
        .get();

    // Verify that the pending group is not part of pending groups prefs.
    assertThat(readPendingFileGroup(testKey)).isNull();

    // Verify that the downloaded group is still part of downloaded groups prefs.
    assertThat(readDownloadedFileGroup(testKey)).isNotNull();

    verify(mockLogger)
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            TEST_GROUP,
            /* fileGroupVersionNumber= */ 0,
            /* buildId= */ 0,
            /* variantId= */ "");

    verify(mockBackend).exists(blobUri);
    // openForWrite is called only once in tryToShareBeforeDownload for acquiring the lease.
    verify(mockBackend, never()).openForWrite(blobUri);
    verify(mockBackend).openForWrite(leaseUri);

    SharedFile expectedSharedFile0 =
        SharedFile.newBuilder()
            .setFileName("android_shared_sha256_1230")
            .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
            .setAndroidShared(true)
            .setAndroidSharingChecksum("sha256_1230")
            .setMaxExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS)
            .build();
    SharedFile expectedSharedFile1 =
        SharedFile.newBuilder()
            .setFileName(fileGroup.getFile(1).getFileId())
            .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
            .setMaxExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS)
            .build();
    assertThat(sharedFileManager.getSharedFile(keys[0]).get()).isEqualTo(expectedSharedFile0);
    assertThat(sharedFileManager.getSharedFile(keys[1]).get()).isEqualTo(expectedSharedFile1);

    ArgumentCaptor<Void> mddAndroidSharingLogArgumentCaptor = ArgumentCaptor.forClass(Void.class);

    verify(mockLogger).logMddAndroidSharingLog(mddAndroidSharingLogArgumentCaptor.capture());
    Void mddAndroidSharingLog = null;
    Void expectedLog = null;
    assertThat(mddAndroidSharingLog).isEqualTo(expectedLog);
  }

  @Test
  public void testDownloadFileGroup_pending_oneBlobExistsAfterDownload() throws Exception {
    // Write 1 group to the pending shared prefs.
    DataFileGroupInternal tmpFileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 0);
    ExtraHttpHeader extraHttpHeader =
        ExtraHttpHeader.newBuilder().setKey("user-agent").setValue("mdd-downloader").build();

    final DataFileGroupInternal fileGroup =
        tmpFileGroup.toBuilder()
            .setOwnerPackage(context.getPackageName())
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .setExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS)
            .setTrafficTag(TRAFFIC_TAG)
            .addGroupExtraHttpHeaders(extraHttpHeader)
            .addFile(0, MddTestUtil.createSharedDataFile(TEST_GROUP, 0))
            .addFile(1, MddTestUtil.createDataFile(TEST_GROUP, 1))
            .build();

    NewFileKey[] keys = MddTestUtil.createFileKeysForDataFileGroupInternal(fileGroup);
    writePendingFileGroup(testKey, fileGroup);

    writeSharedFiles(
        sharedFilesMetadata,
        fileGroup,
        ImmutableList.of(FileStatus.SUBSCRIBED, FileStatus.DOWNLOAD_COMPLETE));

    // File that can be shared
    DataFile file = fileGroup.getFile(0);
    Uri blobUri = DirectoryUtil.getBlobUri(context, file.getAndroidSharingChecksum());
    Uri leaseUri =
        DirectoryUtil.getBlobStoreLeaseUri(
            context, file.getAndroidSharingChecksum(), FILE_GROUP_EXPIRATION_DATE_SECS);

    // The file isn't available in the blob storage when tryToShareBeforeDownload is called
    when(mockBackend.exists(blobUri)).thenReturn(false);

    simulateDownload(file, file.getFileId());
    Uri onDeviceuri =
        DirectoryUtil.getOnDeviceUri(
            context,
            keys[0].getAllowedReaders(),
            file.getFileId(),
            keys[0].getChecksum(),
            mockSilentFeedback,
            /* instanceId= */ Optional.absent(),
            /* androidShared= */ false);
    assertThat(fileStorage.exists(onDeviceuri)).isTrue();

    when(mockDownloader.startDownloading(
            any(String.class),
            any(GroupKey.class),
            anyInt(),
            anyLong(),
            any(String.class),
            eq(onDeviceuri),
            any(String.class),
            anyInt(),
            any(DownloadConditions.class),
            isA(DownloaderCallbackImpl.class),
            anyInt(),
            anyList()))
        .then(
            new Answer<ListenableFuture<Void>>() {
              @Override
              public ListenableFuture<Void> answer(InvocationOnMock invocation) throws Throwable {
                // The file now exists in the shared storage
                when(mockBackend.exists(blobUri)).thenReturn(true);
                writeSharedFiles(
                    sharedFilesMetadata,
                    fileGroup,
                    ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE, FileStatus.DOWNLOAD_COMPLETE));
                return Futures.immediateVoidFuture();
              }
            });

    fileGroupManager
        .downloadFileGroup(testKey, DownloadConditions.getDefaultInstance(), noCustomValidation())
        .get();

    // Verify that pending key is removed if download is complete.
    assertThat(readPendingFileGroup(testKey)).isNull();

    // Verify that downloaded key is written into metadata if download is complete.
    assertThat(readDownloadedFileGroup(testKey)).isNotNull();
    verify(mockLogger)
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            TEST_GROUP,
            /* fileGroupVersionNumber= */ 0,
            /* buildId= */ 0,
            /* variantId= */ "");

    // exists called once in tryToShareBeforeDownload and once in tryToShareAfterDownload
    verify(mockBackend, times(2)).exists(blobUri);
    // openForWrite is called only once in tryToShareAfterDownload for acquiring the lease.
    verify(mockBackend, never()).openForWrite(blobUri);
    verify(mockBackend).openForWrite(leaseUri);

    SharedFile expectedSharedFile0 =
        SharedFile.newBuilder()
            .setFileName("android_shared_sha256_1230")
            .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
            .setAndroidShared(true)
            .setAndroidSharingChecksum("sha256_1230")
            .setMaxExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS)
            .build();
    SharedFile expectedSharedFile1 =
        SharedFile.newBuilder()
            .setFileName(fileGroup.getFile(1).getFileId())
            .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
            .setMaxExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS)
            .build();
    assertThat(sharedFileManager.getSharedFile(keys[0]).get()).isEqualTo(expectedSharedFile0);
    assertThat(sharedFileManager.getSharedFile(keys[1]).get()).isEqualTo(expectedSharedFile1);

    // Local copy has not been deleted.
    assertThat(fileStorage.exists(onDeviceuri)).isTrue();

    ArgumentCaptor<Void> mddAndroidSharingLogArgumentCaptor = ArgumentCaptor.forClass(Void.class);

    verify(mockLogger).logMddAndroidSharingLog(mddAndroidSharingLogArgumentCaptor.capture());
    Void mddAndroidSharingLog = null;
    Void expectedLog = null;
    assertThat(mddAndroidSharingLog).isEqualTo(expectedLog);
  }

  @Test
  public void testDownloadFileGroup_success_oneFileCanBeCopiedBeforeDownload() throws Exception {
    File tempFile = folder.newFile("blobFile");
    // Write 1 group to the pending shared prefs.
    DataFileGroupInternal fileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 0);
    ExtraHttpHeader extraHttpHeader =
        ExtraHttpHeader.newBuilder().setKey("user-agent").setValue("mdd-downloader").build();

    fileGroup =
        fileGroup.toBuilder()
            .setOwnerPackage(context.getPackageName())
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .setTrafficTag(TRAFFIC_TAG)
            .addGroupExtraHttpHeaders(extraHttpHeader)
            .addFile(0, MddTestUtil.createSharedDataFile(TEST_GROUP, 0))
            .addFile(1, MddTestUtil.createDataFile(TEST_GROUP, 1))
            .build();
    NewFileKey[] keys = MddTestUtil.createFileKeysForDataFileGroupInternal(fileGroup);
    writePendingFileGroup(testKey, fileGroup);

    // All files are downloaded.
    writeSharedFiles(
        sharedFilesMetadata,
        fileGroup,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE, FileStatus.DOWNLOAD_COMPLETE));

    DataFile file = fileGroup.getFile(0);
    Uri blobUri = DirectoryUtil.getBlobUri(context, file.getAndroidSharingChecksum());
    Uri leaseUri = DirectoryUtil.getBlobStoreLeaseUri(context, file.getAndroidSharingChecksum(), 0);
    // The file isn't available yet in the blob storage
    when(mockBackend.exists(blobUri)).thenReturn(false);
    // File that can be copied to the blob storage
    when(mockBackend.openForWrite(blobUri)).thenReturn(new FileOutputStream(tempFile));

    File onDeviceFile = simulateDownload(file, fileGroup.getFile(0).getFileId());
    Uri onDeviceuri =
        DirectoryUtil.getOnDeviceUri(
            context,
            keys[0].getAllowedReaders(),
            fileGroup.getFile(0).getFileId(),
            keys[0].getChecksum(),
            mockSilentFeedback,
            /* instanceId= */ Optional.absent(),
            /* androidShared= */ false);
    assertThat(fileStorage.exists(onDeviceuri)).isTrue();

    fileGroupManager
        .downloadFileGroup(testKey, DownloadConditions.getDefaultInstance(), noCustomValidation())
        .get();

    // Verify that pending key is removed if download is complete.
    assertThat(readPendingFileGroup(testKey)).isNull();

    // Verify that downloaded key is written into metadata if download is complete.
    assertThat(readDownloadedFileGroup(testKey)).isNotNull();
    verify(mockLogger)
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            TEST_GROUP,
            /* fileGroupVersionNumber= */ 0,
            /* buildId= */ 0,
            /* variantId= */ "");

    // exists only called once in tryToShareBeforeDownload
    verify(mockBackend).exists(blobUri);
    // openForWrite is called in tryToShareBeforeDownload for copying the file and acquiring the
    // lease.
    verify(mockBackend).openForWrite(blobUri);
    verify(mockBackend).openForWrite(leaseUri);

    SharedFile expectedSharedFile0 =
        SharedFile.newBuilder()
            .setFileName("android_shared_sha256_1230")
            .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
            .setAndroidShared(true)
            .setAndroidSharingChecksum("sha256_1230")
            .setMaxExpirationDateSecs(0)
            .build();
    SharedFile expectedSharedFile1 =
        SharedFile.newBuilder()
            .setFileName(fileGroup.getFile(1).getFileId())
            .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
            .build();
    assertThat(sharedFileManager.getSharedFile(keys[0]).get()).isEqualTo(expectedSharedFile0);
    assertThat(sharedFileManager.getSharedFile(keys[1]).get()).isEqualTo(expectedSharedFile1);

    assertThat(fileStorage.exists(onDeviceuri)).isTrue();
    onDeviceFile.delete();

    ArgumentCaptor<Void> mddAndroidSharingLogArgumentCaptor = ArgumentCaptor.forClass(Void.class);

    verify(mockLogger).logMddAndroidSharingLog(mddAndroidSharingLogArgumentCaptor.capture());
    Void mddAndroidSharingLog = null;
    Void expectedLog = null;
    assertThat(mddAndroidSharingLog).isEqualTo(expectedLog);
  }

  @Test
  public void testDownloadFileGroup_oneFileCanBeCopiedAfterDownload() throws Exception {
    File tempFile = folder.newFile("blobFile");
    // Write 1 group to the pending shared prefs.
    DataFileGroupInternal tmpFfileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 0);
    ExtraHttpHeader extraHttpHeader =
        ExtraHttpHeader.newBuilder().setKey("user-agent").setValue("mdd-downloader").build();

    final DataFileGroupInternal fileGroup =
        tmpFfileGroup.toBuilder()
            .setOwnerPackage(context.getPackageName())
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .setTrafficTag(TRAFFIC_TAG)
            .addGroupExtraHttpHeaders(extraHttpHeader)
            .addFile(0, MddTestUtil.createSharedDataFile(TEST_GROUP, 0))
            .addFile(1, MddTestUtil.createDataFile(TEST_GROUP, 1))
            .build();
    NewFileKey[] keys = MddTestUtil.createFileKeysForDataFileGroupInternal(fileGroup);
    writePendingFileGroup(testKey, fileGroup);

    writeSharedFiles(
        sharedFilesMetadata,
        fileGroup,
        ImmutableList.of(FileStatus.SUBSCRIBED, FileStatus.DOWNLOAD_COMPLETE));

    // File that can be copied to the blob storage
    DataFile file = fileGroup.getFile(0);
    Uri blobUri = DirectoryUtil.getBlobUri(context, file.getAndroidSharingChecksum());
    Uri leaseUri = DirectoryUtil.getBlobStoreLeaseUri(context, file.getAndroidSharingChecksum(), 0);
    // The file is available in the blob storage
    when(mockBackend.exists(blobUri)).thenReturn(false);

    simulateDownload(file, file.getFileId());
    Uri onDeviceuri =
        DirectoryUtil.getOnDeviceUri(
            context,
            keys[0].getAllowedReaders(),
            file.getFileId(),
            keys[0].getChecksum(),
            mockSilentFeedback,
            /* instanceId= */ Optional.absent(),
            /* androidShared= */ false);
    assertThat(fileStorage.exists(onDeviceuri)).isTrue();

    when(mockDownloader.startDownloading(
            any(String.class),
            any(GroupKey.class),
            anyInt(),
            anyLong(),
            any(String.class),
            eq(onDeviceuri),
            any(String.class),
            anyInt(),
            any(DownloadConditions.class),
            isA(DownloaderCallbackImpl.class),
            anyInt(),
            anyList()))
        .then(
            new Answer<ListenableFuture<Void>>() {
              @Override
              public ListenableFuture<Void> answer(InvocationOnMock invocation) throws Throwable {
                // The file will be copied in tryToShareAfterDownload
                when(mockBackend.openForWrite(blobUri)).thenReturn(new FileOutputStream(tempFile));
                writeSharedFiles(
                    sharedFilesMetadata,
                    fileGroup,
                    ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE, FileStatus.DOWNLOAD_COMPLETE));
                return Futures.immediateVoidFuture();
              }
            });

    fileGroupManager
        .downloadFileGroup(testKey, DownloadConditions.getDefaultInstance(), noCustomValidation())
        .get();

    // Verify that pending key is removed if download is complete.
    assertThat(readPendingFileGroup(testKey)).isNull();

    // Verify that downloaded key is written into metadata if download is complete.
    assertThat(readDownloadedFileGroup(testKey)).isNotNull();
    verify(mockLogger)
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            TEST_GROUP,
            /* fileGroupVersionNumber= */ 0,
            /* buildId= */ 0,
            /* variantId= */ "");

    // exists only called once in tryToShareBeforeDownload, once in tryToShareAfterDownload
    verify(mockBackend, times(2)).exists(blobUri);
    //  File copied once in tryToShareAfterDownload
    verify(mockBackend).openForWrite(blobUri);
    verify(mockBackend).openForWrite(leaseUri);

    SharedFile expectedSharedFile0 =
        SharedFile.newBuilder()
            .setFileName("android_shared_sha256_1230")
            .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
            .setAndroidShared(true)
            .setAndroidSharingChecksum("sha256_1230")
            .setMaxExpirationDateSecs(0)
            .build();
    SharedFile expectedSharedFile1 =
        SharedFile.newBuilder()
            .setFileName(fileGroup.getFile(1).getFileId())
            .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
            .build();
    assertThat(sharedFileManager.getSharedFile(keys[0]).get()).isEqualTo(expectedSharedFile0);
    assertThat(sharedFileManager.getSharedFile(keys[1]).get()).isEqualTo(expectedSharedFile1);

    // Local copy has not been deleted.
    assertThat(fileStorage.exists(onDeviceuri)).isTrue();

    ArgumentCaptor<Void> mddAndroidSharingLogArgumentCaptor = ArgumentCaptor.forClass(Void.class);

    verify(mockLogger).logMddAndroidSharingLog(mddAndroidSharingLogArgumentCaptor.capture());
    Void mddAndroidSharingLog = null;
    Void expectedLog = null;
    assertThat(mddAndroidSharingLog).isEqualTo(expectedLog);
  }

  @Test
  public void testDownloadFileGroup_nonToBeSharedFile_neverShared() throws Exception {
    // Write 1 group to the pending shared prefs.
    DataFileGroupInternal tmpFileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1);
    ExtraHttpHeader extraHttpHeader =
        ExtraHttpHeader.newBuilder().setKey("user-agent").setValue("mdd-downloader").build();

    final DataFileGroupInternal fileGroup =
        tmpFileGroup.toBuilder()
            .setOwnerPackage(context.getPackageName())
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .setExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS)
            .setTrafficTag(TRAFFIC_TAG)
            .addGroupExtraHttpHeaders(extraHttpHeader)
            .build();
    NewFileKey[] keys = MddTestUtil.createFileKeysForDataFileGroupInternal(fileGroup);
    writePendingFileGroup(testKey, fileGroup);

    writeSharedFiles(sharedFilesMetadata, fileGroup, ImmutableList.of(FileStatus.SUBSCRIBED));

    DataFile file = fileGroup.getFile(0);
    File onDeviceFile = simulateDownload(file, file.getFileId());
    Uri onDeviceuri =
        DirectoryUtil.getOnDeviceUri(
            context,
            keys[0].getAllowedReaders(),
            file.getFileId(),
            keys[0].getChecksum(),
            mockSilentFeedback,
            /* instanceId= */ Optional.absent(),
            /* androidShared= */ false);
    assertThat(fileStorage.exists(onDeviceuri)).isTrue();

    fileDownloadSucceeds(keys[0], onDeviceuri);

    fileGroupManager
        .downloadFileGroup(testKey, DownloadConditions.getDefaultInstance(), noCustomValidation())
        .get();

    // Verify that pending key is removed if download is complete.
    assertThat(readPendingFileGroup(testKey)).isNull();

    // Verify that downloaded key is written into metadata if download is complete.
    assertThat(readDownloadedFileGroup(testKey)).isNotNull();
    verify(mockLogger)
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            TEST_GROUP,
            /* fileGroupVersionNumber= */ 0,
            /* buildId= */ 0,
            /* variantId= */ "");

    verify(mockBackend, never()).exists(any());
    verify(mockBackend, never()).openForWrite(any());

    SharedFile expectedSharedFile =
        SharedFile.newBuilder()
            .setFileName(file.getFileId())
            .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
            .setMaxExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS)
            .build();
    assertThat(sharedFileManager.getSharedFile(keys[0]).get()).isEqualTo(expectedSharedFile);

    assertThat(fileStorage.exists(onDeviceuri)).isTrue();
    onDeviceFile.delete();
  }

  @Test
  public void testDownloadFileGroup_androidSharingFails() throws Exception {
    // Write 1 group to the pending shared prefs.
    DataFileGroupInternal tmpFileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 0);
    ExtraHttpHeader extraHttpHeader =
        ExtraHttpHeader.newBuilder().setKey("user-agent").setValue("mdd-downloader").build();

    final DataFileGroupInternal fileGroup =
        tmpFileGroup.toBuilder()
            .setOwnerPackage(context.getPackageName())
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .setTrafficTag(TRAFFIC_TAG)
            .addGroupExtraHttpHeaders(extraHttpHeader)
            .addFile(0, MddTestUtil.createDataFile(TEST_GROUP, 0))
            .addFile(1, MddTestUtil.createSharedDataFile(TEST_GROUP, 1))
            .build();
    NewFileKey[] keys = MddTestUtil.createFileKeysForDataFileGroupInternal(fileGroup);
    writePendingFileGroup(testKey, fileGroup);

    writeSharedFiles(
        sharedFilesMetadata,
        fileGroup,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE, FileStatus.DOWNLOAD_COMPLETE));

    // Second file fails with file storage I/O exception when called from tryToShareBeforeDownload
    // and tryToShareAfterDownload.
    DataFile file = fileGroup.getFile(1);
    Uri blobUri = DirectoryUtil.getBlobUri(context, file.getAndroidSharingChecksum());
    when(mockBackend.exists(blobUri)).thenThrow(new IOException());

    // Any error during sharing doesn't stop the download: the file will be stored locally.
    fileGroupManager
        .downloadFileGroup(testKey, DownloadConditions.getDefaultInstance(), noCustomValidation())
        .get();

    // Verify that pending key is removed if download is complete.
    assertThat(readPendingFileGroup(testKey)).isNull();

    // Verify that downloaded key is written into metadata if download is complete.
    assertThat(readDownloadedFileGroup(testKey)).isNotNull();

    verify(mockBackend, times(2)).exists(blobUri);
    verify(mockBackend, never()).openForWrite(any());

    SharedFile expectedSharedFile0 =
        SharedFile.newBuilder()
            .setFileName(fileGroup.getFile(0).getFileId())
            .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
            .build();
    SharedFile expectedSharedFile1 =
        expectedSharedFile0.toBuilder().setFileName(fileGroup.getFile(1).getFileId()).build();
    assertThat(sharedFileManager.getSharedFile(keys[0]).get()).isEqualTo(expectedSharedFile0);
    assertThat(sharedFileManager.getSharedFile(keys[1]).get()).isEqualTo(expectedSharedFile1);

    verify(mockLogger)
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            TEST_GROUP,
            /* fileGroupVersionNumber= */ 0,
            /* buildId= */ 0,
            /* variantId= */ "");
    verify(mockLogger)
        .logMddDownloadResult(
            MddDownloadResult.Code.SUCCESS,
            DataDownloadFileGroupStats.newBuilder()
                .setFileGroupName(TEST_GROUP)
                .setOwnerPackage(context.getPackageName())
                .setFileGroupVersionNumber(0)
                .setBuildId(0)
                .setVariantId("")
                .build());

    ArgumentCaptor<Void> mddAndroidSharingLogArgumentCaptor = ArgumentCaptor.forClass(Void.class);

    verify(mockLogger, times(2))
        .logMddAndroidSharingLog(mddAndroidSharingLogArgumentCaptor.capture());
    Void mddAndroidSharingLogBeforeAndAfterDownload = null;
    assertThat(mddAndroidSharingLogArgumentCaptor.getAllValues())
        .containsExactly(
            mddAndroidSharingLogBeforeAndAfterDownload, mddAndroidSharingLogBeforeAndAfterDownload);
  }

  @Test
  public void testDownloadFileGroup_skipsSideloadedFiles() throws Exception {
    // Create sideloaded group with normal file
    DataFileGroupInternal sideloadedGroup =
        DataFileGroupInternal.newBuilder()
            .setGroupName(TEST_GROUP)
            .addFile(
                DataFile.newBuilder()
                    .setFileId("sideloaded_file")
                    .setUrlToDownload("file:/test")
                    .setChecksumType(DataFile.ChecksumType.NONE)
                    .build())
            .addFile(
                DataFile.newBuilder()
                    .setFileId("normal_file")
                    .setUrlToDownload("https://url.to.download")
                    .setChecksumType(DataFile.ChecksumType.NONE)
                    .build())
            .build();
    NewFileKey normalFileKey =
        SharedFilesMetadata.createKeyFromDataFile(
            sideloadedGroup.getFile(1), sideloadedGroup.getAllowedReadersEnum());

    // Write group as pending since we are waiting on normal file
    writePendingFileGroup(testKey, sideloadedGroup);
    SharedFile normalSharedFile =
        SharedFile.newBuilder()
            .setFileName(sideloadedGroup.getFile(1).getFileId())
            .setFileStatus(FileStatus.SUBSCRIBED)
            .build();
    sharedFilesMetadata.write(normalFileKey, normalSharedFile).get();

    // Mock that download of normal file succeeds
    Uri normalFileUri =
        DirectoryUtil.getOnDeviceUri(
            context,
            normalFileKey.getAllowedReaders(),
            sideloadedGroup.getFile(1).getFileId(),
            sideloadedGroup.getFile(1).getChecksum(),
            mockSilentFeedback,
            /* instanceId= */ Optional.absent(),
            false);
    fileDownloadSucceeds(normalFileKey, normalFileUri);

    fileGroupManager
        .downloadFileGroup(testKey, DownloadConditions.getDefaultInstance(), noCustomValidation())
        .get();

    assertThat(readPendingFileGroup(testKey)).isNull();
    assertThat(readDownloadedFileGroup(testKey)).isNotNull();

    verify(mockDownloader)
        .startDownloading(
            eq(sideloadedGroup.getFile(1).getChecksum()),
            eq(testKey),
            anyInt(),
            anyLong(),
            any(String.class),
            eq(normalFileUri),
            eq(sideloadedGroup.getFile(1).getUrlToDownload()),
            anyInt(),
            any(),
            any(),
            anyInt(),
            anyList());
  }

  @Test
  public void testDownloadFileGroup_whenMultipleVariantsExist_downloadsSpecifiedVariant()
      throws Exception {
    GroupKey defaultGroupKey =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP)
            .setOwnerPackage(context.getPackageName())
            .build();
    GroupKey enGroupKey = defaultGroupKey.toBuilder().setVariantId("en").build();

    DataFileGroupInternal defaultFileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1);
    // Create EN with custom file ids so it doesn't overlap with the default file group.
    DataFileGroupInternal enFileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 0).toBuilder()
            .addFile(MddTestUtil.createDataFile("en", 0))
            .addFile(MddTestUtil.createDataFile("en", 1))
            .build();

    writePendingFileGroup(getPendingKey(defaultGroupKey), defaultFileGroup);
    writePendingFileGroup(getPendingKey(enGroupKey), enFileGroup);

    writeSharedFiles(
        sharedFilesMetadata, defaultFileGroup, ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE));

    fileGroupManager
        .downloadFileGroup(
            defaultGroupKey, DownloadConditions.getDefaultInstance(), noCustomValidation())
        .get();

    // Verify that correct group was downloaded
    assertThat(readPendingFileGroup(defaultGroupKey)).isNull();
    assertThat(readDownloadedFileGroup(defaultGroupKey)).isNotNull();

    assertThat(readPendingFileGroup(enGroupKey)).isNotNull();
    assertThat(readDownloadedFileGroup(enGroupKey)).isNull();

    // Attempt to download en group and check that it is now downloaded
    writeSharedFiles(
        sharedFilesMetadata,
        enFileGroup,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE, FileStatus.DOWNLOAD_COMPLETE));

    fileGroupManager
        .downloadFileGroup(
            enGroupKey, DownloadConditions.getDefaultInstance(), noCustomValidation())
        .get();

    // Verify that correct group was downloaded
    assertThat(readPendingFileGroup(defaultGroupKey)).isNull();
    assertThat(readDownloadedFileGroup(defaultGroupKey)).isNotNull();

    assertThat(readPendingFileGroup(enGroupKey)).isNull();
    assertThat(readDownloadedFileGroup(enGroupKey)).isNotNull();
  }

  @Test
  public void testDownloadAllPendingGroups_onWifi() throws Exception {
    // Write 3 groups to the pending shared prefs.
    // MDD successfully downloaded filegroup1, partially downloaded filegroup2 and failed to
    // download filegroup3.
    DataFileGroupInternal fileGroup1 =
        createDataFileGroup(
            TEST_GROUP,
            /* fileCount= */ 2,
            /* downloadAttemptCount= */ 7,
            /* newFilesReceivedTimestamp= */ testClock.currentTimeMillis() - 500L);
    fileGroup1 =
        fileGroup1.toBuilder()
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .build();
    writePendingFileGroup(testKey, fileGroup1);
    // All files are downloaded.
    writeSharedFiles(
        sharedFilesMetadata,
        fileGroup1,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE, FileStatus.DOWNLOAD_COMPLETE));

    DataFileGroupInternal fileGroup2 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP_2, 2);
    fileGroup2 =
        fileGroup2.toBuilder()
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .build();
    writePendingFileGroup(testKey2, fileGroup2);
    // Not all files are downloaded.
    writeSharedFiles(
        sharedFilesMetadata,
        fileGroup2,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE, FileStatus.DOWNLOAD_IN_PROGRESS));

    GroupKey expectedKey2 = testKey2.toBuilder().setDownloaded(false).build();
    // The file status isn't changed to DOWNLOAD_COMPLETE, it remains DOWNLOAD_IN_PROGRESS.
    //  An UNKNOWN_ERROR is logged.
    when(mockDownloader.getInProgressFuture(any(String.class), any(Uri.class)))
        .thenReturn(Futures.immediateFuture(Optional.of(Futures.immediateVoidFuture())));

    DataFileGroupInternal tmpFileGroup3 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP_3, 2);
    final DataFileGroupInternal fileGroup3 =
        tmpFileGroup3.toBuilder()
            .setDownloadConditions(
                DownloadConditions.newBuilder().setDownloadFirstOnWifiPeriodSecs(1000000))
            .build();
    writePendingFileGroup(testKey3, fileGroup3);
    // Not all files are downloaded.
    writeSharedFiles(
        sharedFilesMetadata,
        fileGroup3,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE, FileStatus.DOWNLOAD_FAILED));

    GroupKey expectedKey3 = testKey3.toBuilder().setDownloaded(false).build();
    // One file fails, status is DOWNLOAD_FAILED but the downloader returns an
    // immediateVoidFuture. An UNKNOWN_ERROR is logged.
    when(mockDownloader.startDownloading(
            any(String.class),
            eq(expectedKey3),
            anyInt(),
            anyLong(),
            any(String.class),
            any(Uri.class),
            any(String.class),
            anyInt(),
            any(DownloadConditions.class),
            isA(DownloaderCallbackImpl.class),
            anyInt(),
            anyList()))
        .then(
            new Answer<ListenableFuture<Void>>() {
              @Override
              public ListenableFuture<Void> answer(InvocationOnMock invocation) throws Throwable {
                writeSharedFiles(
                    sharedFilesMetadata,
                    fileGroup3,
                    ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE, FileStatus.DOWNLOAD_FAILED));
                return Futures.immediateVoidFuture();
              }
            });

    fileGroupManager.scheduleAllPendingGroupsForDownload(true, noCustomValidation()).get();

    verify(mockLogger)
        .logMddDownloadLatency(
            createFileGroupDetails(fileGroup1).build(),
            createMddDownloadLatency(
                /* downloadAttemptCount= */ 8,
                /* downloadLatencyMs= */ 0L,
                /* totalLatencyMs= */ 500L));
    verify(mockLogger)
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            TEST_GROUP_2,
            /* fileGroupVersionNumber= */ 0,
            /* buildId= */ 0,
            /* variantId= */ "");
    verify(mockLogger)
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            TEST_GROUP_3,
            /* fileGroupVersionNumber= */ 0,
            /* buildId= */ 0,
            /* variantId= */ "");

    // Make sure that the successful download of fileGroup1, the failed downloads of fileGroup2 and
    // fileGroup3 are all logged to clearcut.
    verify(mockLogger)
        .logMddDownloadResult(
            MddDownloadResult.Code.SUCCESS,
            createFileGroupDetails(fileGroup1)
                .setOwnerPackage(context.getPackageName())
                .clearFileCount()
                .build());
    verify(mockLogger)
        .logMddDownloadResult(
            MddDownloadResult.Code.UNKNOWN_ERROR,
            createFileGroupDetails(fileGroup2)
                .setOwnerPackage(context.getPackageName())
                .clearFileCount()
                .build());

    verify(mockLogger)
        .logMddDownloadResult(
            MddDownloadResult.Code.UNKNOWN_ERROR,
            createFileGroupDetails(fileGroup3)
                .setOwnerPackage(context.getPackageName())
                .clearFileCount()
                .build());
  }

  @Test
  public void testDownloadAllPendingGroups_withoutWifi() throws Exception {
    // Write 2 groups to the pending shared prefs.
    DataFileGroupInternal fileGroup1 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    fileGroup1 =
        fileGroup1.toBuilder()
            .setDownloadConditions(
                DownloadConditions.newBuilder()
                    .setDeviceNetworkPolicy(DeviceNetworkPolicy.DOWNLOAD_ON_ANY_NETWORK))
            .build();
    writePendingFileGroup(testKey, fileGroup1);
    writeSharedFiles(
        sharedFilesMetadata,
        fileGroup1,
        ImmutableList.of(FileStatus.DOWNLOAD_IN_PROGRESS, FileStatus.DOWNLOAD_IN_PROGRESS));

    DataFileGroupInternal fileGroup2 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP_2, 3);
    writePendingFileGroup(testKey2, fileGroup2);
    writeSharedFiles(
        sharedFilesMetadata,
        fileGroup2,
        ImmutableList.of(
            FileStatus.DOWNLOAD_IN_PROGRESS,
            FileStatus.DOWNLOAD_IN_PROGRESS,
            FileStatus.DOWNLOAD_IN_PROGRESS));

    fileGroupManager.scheduleAllPendingGroupsForDownload(false, noCustomValidation()).get();

    // Only the files in the first group will be downloaded.
    verify(mockDownloader, times(2)).getInProgressFuture(any(String.class), any(Uri.class));

    verifyNoMoreInteractions(mockDownloader);
  }

  @Test
  public void testDownloadAllPendingGroups_wifiFirst_without_Wifi() throws Exception {
    DataFileGroupInternal dataFileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2).toBuilder()
            .setDownloadConditions(
                DownloadConditions.newBuilder()
                    .setDeviceNetworkPolicy(
                        DeviceNetworkPolicy.DOWNLOAD_FIRST_ON_WIFI_THEN_ON_ANY_NETWORK)
                    .setDownloadFirstOnWifiPeriodSecs(10))
            .build();

    testClock.set(1000);

    {
      // Check that pending groups contain the added file group.
      assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup).get()).isTrue();
      verifyAddGroupForDownloadWritesMetadata(testKey, dataFileGroup, 1000);
    }

    {
      // Set time so that it has not passed the wifi only period.
      testClock.set(2000);
      fileGroupManager.scheduleAllPendingGroupsForDownload(false, noCustomValidation()).get();
    }

    {
      // Set time so that it has passed the wifi only period.
      testClock.set(2000 + 10 * 1000);
      ArgumentCaptor<DownloadConditions> downloadConditionCaptor =
          ArgumentCaptor.forClass(DownloadConditions.class);
      when(mockDownloader.startDownloading(
              any(String.class),
              any(GroupKey.class),
              anyInt(),
              anyLong(),
              any(String.class),
              any(Uri.class),
              any(String.class),
              anyInt(),
              downloadConditionCaptor.capture(),
              isA(DownloaderCallbackImpl.class),
              anyInt(),
              anyList()))
          .thenReturn(Futures.immediateVoidFuture());

      fileGroupManager.scheduleAllPendingGroupsForDownload(false, noCustomValidation()).get();

      // verify that the group's DeviceNetworkPolicy changes to
      // DeviceNetworkPolicy.DOWNLOAD_ON_ANY_NETWORK
      assertThat(downloadConditionCaptor.getValue().getDeviceNetworkPolicy())
          .isEqualTo(DeviceNetworkPolicy.DOWNLOAD_ON_ANY_NETWORK);
    }
  }

  @Test
  public void testDownloadAllPendingGroups_startDownloadFails() throws Exception {
    // Write 2 groups to the pending shared prefs.
    DataFileGroupInternal fileGroup1 =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2).toBuilder()
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .build();
    writePendingFileGroup(testKey, fileGroup1);
    writeSharedFiles(
        sharedFilesMetadata,
        fileGroup1,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE, FileStatus.SUBSCRIBED));
    NewFileKey[] keys1 = MddTestUtil.createFileKeysForDataFileGroupInternal(fileGroup1);

    DataFileGroupInternal fileGroup2 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP_2, 1);
    writePendingFileGroup(testKey2, fileGroup2);
    writeSharedFiles(
        sharedFilesMetadata, fileGroup2, ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE));

    // Make the download call fail for one of the files in first group.
    Uri failingFileUri =
        DirectoryUtil.getOnDeviceUri(
            context,
            keys1[1].getAllowedReaders(),
            fileGroup1.getFile(1).getFileId(),
            fileGroup1.getFile(1).getChecksum(),
            mockSilentFeedback,
            /* instanceId= */ Optional.absent(),
            false);
    fileDownloadFails(keys1[1], failingFileUri, DownloadResultCode.LOW_DISK_ERROR);

    fileGroupManager.scheduleAllPendingGroupsForDownload(true, noCustomValidation()).get();

    verify(mockLogger)
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            TEST_GROUP,
            /* fileGroupVersionNumber= */ 0,
            /* buildId= */ 0,
            /* variantId= */ "");
    verify(mockLogger)
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            TEST_GROUP_2,
            /* fileGroupVersionNumber= */ 0,
            /* buildId= */ 0,
            /* variantId= */ "");

    verify(mockLogger)
        .logMddDownloadResult(
            MddDownloadResult.Code.LOW_DISK_ERROR,
            createFileGroupDetails(fileGroup1)
                .clearFileCount()
                .setOwnerPackage(context.getPackageName())
                .build());

    verify(mockLogger)
        .logMddDownloadResult(
            MddDownloadResult.Code.SUCCESS,
            createFileGroupDetails(fileGroup2)
                .clearFileCount()
                .setOwnerPackage(context.getPackageName())
                .build());
  }

  // case 1: the file is already shared in the blob storage.
  @Test
  public void tryToShareBeforeDownload_alreadyShared() throws Exception {
    DataFileGroupInternal fileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2).toBuilder()
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .build();

    DataFile file = MddTestUtil.createSharedDataFile("fileId", 0);
    NewFileKey newFileKey =
        SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS);
    // Set the file metadata as already downloaded and shared
    SharedFile existingDownloadedSharedFile =
        SharedFile.newBuilder()
            .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
            .setFileName("")
            .setAndroidShared(true)
            .setAndroidSharingChecksum(file.getAndroidSharingChecksum())
            .setMaxExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS)
            .build();
    sharedFilesMetadata.write(newFileKey, existingDownloadedSharedFile).get();

    fileGroupManager.tryToShareBeforeDownload(fileGroup, file, newFileKey).get();

    verify(mockBackend, never()).exists(any());
    // openForWrite isn't called to update the lease because the current fileGroup's expiration date
    // is < maxExpirationDate.
    verify(mockBackend, never()).openForWrite(any());

    assertThat(sharedFileManager.getSharedFile(newFileKey).get())
        .isEqualTo(existingDownloadedSharedFile);

    ArgumentCaptor<Void> mddAndroidSharingLogArgumentCaptor = ArgumentCaptor.forClass(Void.class);

    verify(mockLogger).logMddAndroidSharingLog(mddAndroidSharingLogArgumentCaptor.capture());
    Void mddAndroidSharingLog = null;
    assertThat(mddAndroidSharingLogArgumentCaptor.getAllValues())
        .containsExactly(mddAndroidSharingLog);
    verifyNoMoreInteractions(mockLogger);
  }

  // case 2a: the to-be-shared file is available in the blob storage.
  @Test
  public void tryToShareBeforeDownload_toBeSharedFile_blobExists() throws Exception {
    // Create a file group with expiration date smaller than the expiration date of the existing
    // SharedFile.
    DataFileGroupInternal fileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2).toBuilder()
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .setExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS - 1)
            .build();

    // Create a to-be-shared file
    DataFile file = MddTestUtil.createSharedDataFile("fileId", 0);
    NewFileKey newFileKey =
        SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS);
    // Set the file metadata as download pending and non shared
    SharedFile existingSharedFile =
        SharedFile.newBuilder()
            .setFileStatus(FileStatus.DOWNLOAD_IN_PROGRESS)
            .setFileName("fileName")
            .setAndroidShared(false)
            .setMaxExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS)
            .build();

    sharedFilesMetadata.write(newFileKey, existingSharedFile).get();

    Uri blobUri = DirectoryUtil.getBlobUri(context, file.getAndroidSharingChecksum());
    Uri leaseUri =
        DirectoryUtil.getBlobStoreLeaseUri(
            context, file.getAndroidSharingChecksum(), FILE_GROUP_EXPIRATION_DATE_SECS);
    // The file is available in the blob storage
    when(mockBackend.exists(blobUri)).thenReturn(true);

    fileGroupManager.tryToShareBeforeDownload(fileGroup, file, newFileKey).get();

    // openForWrite is called only once for acquiring the lease.
    verify(mockBackend, never()).openForWrite(blobUri);
    verify(mockBackend).openForWrite(leaseUri);
    SharedFile sharedFile = sharedFileManager.getSharedFile(newFileKey).get();
    // Verify that the SharedFile retains the longest expiration date after the download.
    assertThat(sharedFile.getMaxExpirationDateSecs()).isEqualTo(FILE_GROUP_EXPIRATION_DATE_SECS);
    assertThat(sharedFile.getAndroidShared()).isTrue();
    assertThat(sharedFileManager.getOnDeviceUri(newFileKey).get()).isEqualTo(blobUri);

    ArgumentCaptor<Void> mddAndroidSharingLogArgumentCaptor = ArgumentCaptor.forClass(Void.class);

    verify(mockLogger).logMddAndroidSharingLog(mddAndroidSharingLogArgumentCaptor.capture());
    Void mddAndroidSharingLog = null;
    assertThat(mddAndroidSharingLogArgumentCaptor.getAllValues())
        .containsExactly(mddAndroidSharingLog);
    verifyNoMoreInteractions(mockLogger);
  }

  // case 3: the to-be-shared file is available in the local storage.
  @Test
  public void tryToShareBeforeDownload_toBeSharedFile_canBeCopied() throws Exception {
    File tempFile = folder.newFile("blobFile");
    // Create a file group with expiration date bigger than the expiration date of the existing
    // SharedFile.
    DataFileGroupInternal fileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2).toBuilder()
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .setExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS + 1)
            .build();

    // Create a to-be-shared file
    DataFile file = MddTestUtil.createSharedDataFile("fileId", 0);
    NewFileKey newFileKey =
        SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS);
    // Set the file metadata as downloaded and non shared
    SharedFile existingSharedFile =
        SharedFile.newBuilder()
            .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
            .setFileName("fileName")
            .setAndroidShared(false)
            .setMaxExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS)
            .build();
    sharedFilesMetadata.write(newFileKey, existingSharedFile).get();

    Uri blobUri = DirectoryUtil.getBlobUri(context, file.getAndroidSharingChecksum());
    Uri leaseUri =
        DirectoryUtil.getBlobStoreLeaseUri(
            context, file.getAndroidSharingChecksum(), FILE_GROUP_EXPIRATION_DATE_SECS + 1);
    // The file isn't available in the blob storage
    when(mockBackend.exists(blobUri)).thenReturn(false);
    when(mockBackend.openForWrite(blobUri)).thenReturn(new FileOutputStream(tempFile));

    File onDeviceFile = simulateDownload(file, existingSharedFile.getFileName());
    Uri onDeviceuri =
        DirectoryUtil.getOnDeviceUri(
            context,
            newFileKey.getAllowedReaders(),
            existingSharedFile.getFileName(),
            newFileKey.getChecksum(),
            mockSilentFeedback,
            /* instanceId= */ Optional.absent(),
            /* androidShared= */ false);
    assertThat(fileStorage.exists(onDeviceuri)).isTrue();

    fileGroupManager.tryToShareBeforeDownload(fileGroup, file, newFileKey).get();

    // openForWrite is called once for writing the blob, once for acquiring the lease.
    verify(mockBackend).openForWrite(blobUri);
    verify(mockBackend).openForWrite(leaseUri);

    SharedFile sharedFile = sharedFileManager.getSharedFile(newFileKey).get();
    // Verify that the SharedFile has updated its expiration date after the download.
    assertThat(sharedFile.getMaxExpirationDateSecs())
        .isEqualTo(FILE_GROUP_EXPIRATION_DATE_SECS + 1);
    assertThat(sharedFile.getAndroidShared()).isTrue();
    assertThat(sharedFileManager.getOnDeviceUri(newFileKey).get()).isEqualTo(blobUri);

    // The local copy will be deleted in daily maintance
    assertThat(fileStorage.exists(onDeviceuri)).isTrue();
    onDeviceFile.delete();

    ArgumentCaptor<Void> mddAndroidSharingLogArgumentCaptor = ArgumentCaptor.forClass(Void.class);

    verify(mockLogger).logMddAndroidSharingLog(mddAndroidSharingLogArgumentCaptor.capture());

    Void mddAndroidSharingLog = null;
    assertThat(mddAndroidSharingLogArgumentCaptor.getAllValues())
        .containsExactly(mddAndroidSharingLog);
    verifyNoMoreInteractions(mockLogger);
  }

  // The file can't be shared and isn't available locally.
  @Test
  public void tryToShareBeforeDownload_toBeSharedFile_cannotBeShared_neverDownloaded()
      throws Exception {
    DataFileGroupInternal fileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2).toBuilder()
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .build();

    DataFile file = MddTestUtil.createSharedDataFile("fileId", 0);
    NewFileKey newFileKey =
        SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS);
    SharedFile existingSharedFile =
        SharedFile.newBuilder()
            .setFileStatus(FileStatus.DOWNLOAD_IN_PROGRESS)
            .setFileName("fileName")
            .setAndroidShared(false)
            .build();
    sharedFilesMetadata.write(newFileKey, existingSharedFile).get();

    Uri blobUri = DirectoryUtil.getBlobUri(context, file.getAndroidSharingChecksum());
    // The file isn't available in the blob storage
    when(mockBackend.exists(blobUri)).thenReturn(false);

    fileGroupManager.tryToShareBeforeDownload(fileGroup, file, newFileKey).get();

    // We never acquire the lease nor update the max expiration date.
    verify(mockBackend).exists(blobUri);
    verify(mockBackend, never()).openForWrite(any());

    SharedFile sharedFile = sharedFileManager.getSharedFile(newFileKey).get();
    assertThat(sharedFile).isEqualTo(existingSharedFile);

    verifyNoInteractions(mockLogger);
  }

  // case 4: the non-to-be-shared file can't be shared and is available in the local storage.
  @Test
  public void tryToShareBeforeDownload_nonToBeSharedFile_alreadyDownloaded_cannotBeShared()
      throws Exception {
    DataFileGroupInternal fileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2).toBuilder()
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .build();
    // non-to-be-shared file with ChecksumType SHA1
    DataFile file = MddTestUtil.createDataFile("fileId", 0);
    NewFileKey newFileKey =
        SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS);
    // Set the file metadata downloaded and non shared
    SharedFile existingSharedFile =
        SharedFile.newBuilder()
            .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
            .setFileName("fileName")
            .setAndroidShared(false)
            .setMaxExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS)
            .build();
    sharedFilesMetadata.write(newFileKey, existingSharedFile).get();

    fileGroupManager.tryToShareBeforeDownload(fileGroup, file, newFileKey).get();

    verify(mockBackend, never()).exists(any());
    // We never acquire the lease since the file can't be shared.
    verify(mockBackend, never()).openForWrite(any());

    SharedFile sharedFile = sharedFileManager.getSharedFile(newFileKey).get();
    assertThat(sharedFile).isEqualTo(existingSharedFile);

    verify(mockSharedFileManager, never())
        .setAndroidSharedDownloadedFileEntry(any(), any(), anyLong());

    verifyNoInteractions(mockLogger);
  }

  @Test
  public void tryToShareBeforeDownload_blobUriNotSupported() throws Exception {
    // FileStorage without BlobStoreBackend
    fileStorage =
        new SynchronousFileStorage(Arrays.asList(AndroidFileBackend.builder(context).build()));
    fileGroupManager =
        new FileGroupManager(
            context,
            mockLogger,
            mockSilentFeedback,
            fileGroupsMetadata,
            sharedFileManager,
            testClock,
            Optional.of(mockAccountSource),
            SEQUENTIAL_CONTROL_EXECUTOR,
            Optional.absent(),
            fileStorage,
            downloadStageManager,
            flags);

    DataFileGroupInternal fileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 0).toBuilder()
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .build();

    // Create a to-be-shared file
    DataFile file = MddTestUtil.createSharedDataFile("fileId", 0);
    NewFileKey newFileKey =
        SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS);
    // Set the file metadata as download completed and non shared
    SharedFile existingSharedFile =
        SharedFile.newBuilder()
            .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
            .setFileName("fileName")
            .setAndroidShared(false)
            .setMaxExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS)
            .build();

    sharedFilesMetadata.write(newFileKey, existingSharedFile).get();

    fileGroupManager.tryToShareBeforeDownload(fileGroup, file, newFileKey).get();

    verify(mockBackend, never()).openForWrite(any());

    SharedFile sharedFile = sharedFileManager.getSharedFile(newFileKey).get();
    assertThat(sharedFile).isEqualTo(existingSharedFile);

    ArgumentCaptor<Void> mddAndroidSharingLogArgumentCaptor = ArgumentCaptor.forClass(Void.class);
    verify(mockLogger).logMddAndroidSharingLog(mddAndroidSharingLogArgumentCaptor.capture());

    Void mddAndroidSharingLog = null;
    assertThat(mddAndroidSharingLogArgumentCaptor.getAllValues())
        .containsExactly(mddAndroidSharingLog);
    verifyNoMoreInteractions(mockLogger);
  }

  @Test
  public void tryToShareBeforeDownload_setAndroidSharedDownloadedFileEntryReturnsFalse()
      throws Exception {
    // Mock SharedFileManager to test failure scenario.
    resetFileGroupManager(fileGroupsMetadata, mockSharedFileManager);
    DataFileGroupInternal fileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2).toBuilder()
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .build();

    // Create a to-be-shared file
    DataFile file = MddTestUtil.createSharedDataFile("fileId", 0);
    NewFileKey newFileKey =
        SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS);

    SharedFile existingSharedFile =
        SharedFile.newBuilder()
            .setFileStatus(FileStatus.DOWNLOAD_IN_PROGRESS)
            .setFileName("fileName")
            .setAndroidShared(false)
            .setMaxExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS)
            .build();

    when(mockSharedFileManager.getSharedFile(newFileKey))
        .thenReturn(Futures.immediateFuture(existingSharedFile));
    Uri blobUri = DirectoryUtil.getBlobUri(context, file.getAndroidSharingChecksum());
    Uri leaseUri =
        DirectoryUtil.getBlobStoreLeaseUri(
            context, file.getAndroidSharingChecksum(), FILE_GROUP_EXPIRATION_DATE_SECS);
    // The file is available in the blob storage
    when(mockBackend.exists(blobUri)).thenReturn(true);

    // Last operation fails
    when(mockSharedFileManager.setAndroidSharedDownloadedFileEntry(
            newFileKey, file.getAndroidSharingChecksum(), FILE_GROUP_EXPIRATION_DATE_SECS))
        .thenReturn(Futures.immediateFuture(false));

    fileGroupManager.tryToShareBeforeDownload(fileGroup, file, newFileKey).get();

    // openForWrite is called only once for acquiring the lease.
    verify(mockBackend, never()).openForWrite(blobUri);
    verify(mockBackend).openForWrite(leaseUri);

    ArgumentCaptor<Void> mddAndroidSharingLogArgumentCaptor = ArgumentCaptor.forClass(Void.class);

    verify(mockLogger).logMddAndroidSharingLog(mddAndroidSharingLogArgumentCaptor.capture());
    Void mddAndroidSharingLog = null;
    assertThat(mddAndroidSharingLogArgumentCaptor.getAllValues())
        .containsExactly(mddAndroidSharingLog);
    verifyNoMoreInteractions(mockLogger);
  }

  @Test
  public void tryToShareBeforeDownload_blobExistsThrowsIOException() throws Exception {
    DataFileGroupInternal fileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2).toBuilder()
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .build();

    // Create a to-be-shared file
    DataFile file = MddTestUtil.createSharedDataFile("fileId", 0);
    NewFileKey newFileKey =
        SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS);

    SharedFile existingSharedFile =
        SharedFile.newBuilder()
            .setFileStatus(FileStatus.DOWNLOAD_IN_PROGRESS)
            .setFileName("fileName")
            .setAndroidShared(false)
            .setMaxExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS)
            .build();
    sharedFilesMetadata.write(newFileKey, existingSharedFile).get();

    Uri blobUri = DirectoryUtil.getBlobUri(context, file.getAndroidSharingChecksum());
    Uri leaseUri =
        DirectoryUtil.getBlobStoreLeaseUri(
            context, file.getAndroidSharingChecksum(), FILE_GROUP_EXPIRATION_DATE_SECS);

    when(mockBackend.exists(blobUri)).thenReturn(true);
    when(mockBackend.openForWrite(leaseUri)).thenThrow(new IOException());

    fileGroupManager.tryToShareBeforeDownload(fileGroup, file, newFileKey).get();

    SharedFile sharedFile = sharedFileManager.getSharedFile(newFileKey).get();
    assertThat(sharedFile).isEqualTo(existingSharedFile);

    ArgumentCaptor<Void> mddAndroidSharingLogArgumentCaptor = ArgumentCaptor.forClass(Void.class);

    verify(mockLogger).logMddAndroidSharingLog(mddAndroidSharingLogArgumentCaptor.capture());

    Void mddAndroidSharingLog = null;
    assertThat(mddAndroidSharingLogArgumentCaptor.getAllValues())
        .containsExactly(mddAndroidSharingLog);
    verifyNoMoreInteractions(mockLogger);
  }

  @Test
  public void tryToShareBeforeDownload_fileStorageThrowsLimitExceededException() throws Exception {
    // Create a file group with expiration date bigger than the expiration date of the existing
    // SharedFile.
    DataFileGroupInternal fileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2).toBuilder()
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .setExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS + 1)
            .build();

    // Create a to-be-shared file
    DataFile file = MddTestUtil.createSharedDataFile("fileId", 0);
    NewFileKey newFileKey =
        SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS);

    SharedFile existingSharedFile =
        SharedFile.newBuilder()
            .setFileStatus(FileStatus.DOWNLOAD_IN_PROGRESS)
            .setFileName("fileName")
            .setAndroidShared(false)
            .setMaxExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS)
            .build();
    sharedFilesMetadata.write(newFileKey, existingSharedFile).get();

    Uri blobUri = DirectoryUtil.getBlobUri(context, file.getAndroidSharingChecksum());
    Uri leaseUri =
        DirectoryUtil.getBlobStoreLeaseUri(
            context, file.getAndroidSharingChecksum(), FILE_GROUP_EXPIRATION_DATE_SECS + 1);
    // The file is available in the blob storage
    when(mockBackend.exists(blobUri)).thenReturn(true);
    // Writing the lease throws an exception
    when(mockBackend.openForWrite(leaseUri)).thenThrow(new LimitExceededException());

    fileGroupManager.tryToShareBeforeDownload(fileGroup, file, newFileKey).get();

    verify(mockBackend, never()).openForWrite(blobUri);
    verify(mockBackend).openForWrite(leaseUri);

    SharedFile sharedFile = sharedFileManager.getSharedFile(newFileKey).get();
    // Since there was an exception, the existing shared file didn't update the expiration date.
    assertThat(sharedFile).isEqualTo(existingSharedFile);

    ArgumentCaptor<Void> mddAndroidSharingLogArgumentCaptor = ArgumentCaptor.forClass(Void.class);
    verify(mockLogger).logMddAndroidSharingLog(mddAndroidSharingLogArgumentCaptor.capture());

    Void mddAndroidSharingLog = null;
    assertThat(mddAndroidSharingLogArgumentCaptor.getAllValues())
        .containsExactly(mddAndroidSharingLog);
    verifyNoMoreInteractions(mockLogger);
  }

  @Test
  public void tryToShareAfterDownload_alreadyShared_sameFileGroup() throws Exception {
    DataFileGroupInternal fileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1).toBuilder()
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .build();

    DataFile file = MddTestUtil.createSharedDataFile("fileId", 0);
    NewFileKey newFileKey =
        SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS);
    SharedFile existingSharedFile =
        SharedFile.newBuilder()
            .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
            .setFileName("fileName")
            .setAndroidShared(true)
            .setAndroidSharingChecksum(file.getAndroidSharingChecksum())
            .setMaxExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS)
            .build();
    sharedFilesMetadata.write(newFileKey, existingSharedFile).get();

    fileGroupManager.tryToShareAfterDownload(fileGroup, file, newFileKey).get();

    verify(mockBackend, never()).exists(any());
    verify(mockBackend, never()).openForWrite(any());

    SharedFile sharedFile = sharedFileManager.getSharedFile(newFileKey).get();
    assertThat(sharedFile).isEqualTo(existingSharedFile);

    verifyNoInteractions(mockLogger);
  }

  @Test
  public void tryToShareAfterDownload_alreadyShared_differentFileGroup() throws Exception {
    // Create a file group with expiration date bigger than the expiration date of the existing
    // SharedFile.
    DataFileGroupInternal fileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1).toBuilder()
            .setExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS)
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .build();

    DataFile file = MddTestUtil.createSharedDataFile("fileId", 0);
    NewFileKey newFileKey =
        SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS);
    SharedFile existingSharedFile =
        SharedFile.newBuilder()
            .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
            .setFileName("fileName")
            .setAndroidShared(true)
            .setAndroidSharingChecksum(file.getAndroidSharingChecksum())
            .setMaxExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS - 1)
            .build();
    sharedFilesMetadata.write(newFileKey, existingSharedFile).get();

    Uri blobUri = DirectoryUtil.getBlobUri(context, file.getAndroidSharingChecksum());
    Uri leaseUri =
        DirectoryUtil.getBlobStoreLeaseUri(
            context, file.getAndroidSharingChecksum(), FILE_GROUP_EXPIRATION_DATE_SECS);

    fileGroupManager.tryToShareAfterDownload(fileGroup, file, newFileKey).get();

    // openForWrite is called only once for acquiring the lease.
    verify(mockBackend, never()).exists(any());
    verify(mockBackend).openForWrite(leaseUri);

    SharedFile sharedFile = sharedFileManager.getSharedFile(newFileKey).get();
    // Verify that the SharedFile has updated its expiration date after the download.
    assertThat(sharedFile.getMaxExpirationDateSecs()).isEqualTo(FILE_GROUP_EXPIRATION_DATE_SECS);
    assertThat(sharedFile.getAndroidShared()).isTrue();
    assertThat(sharedFileManager.getOnDeviceUri(newFileKey).get()).isEqualTo(blobUri);

    ArgumentCaptor<Void> mddAndroidSharingLogArgumentCaptor = ArgumentCaptor.forClass(Void.class);

    verify(mockLogger).logMddAndroidSharingLog(mddAndroidSharingLogArgumentCaptor.capture());
    Void mddAndroidSharingLog = null;
    assertThat(mddAndroidSharingLogArgumentCaptor.getAllValues())
        .containsExactly(mddAndroidSharingLog);
    verifyNoMoreInteractions(mockLogger);
  }

  @Test
  public void tryToShareAfterDownload_toBeSharedFile_blobExists() throws Exception {
    // Create a file group with expiration date bigger than the expiration date of the existing
    // SharedFile.
    DataFileGroupInternal fileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1).toBuilder()
            .setExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS)
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .build();

    DataFile file = MddTestUtil.createSharedDataFile("fileId", 0);
    NewFileKey newFileKey =
        SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS);
    SharedFile existingSharedFile =
        SharedFile.newBuilder()
            .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
            .setFileName("fileName")
            .setAndroidShared(false)
            .build();
    sharedFilesMetadata.write(newFileKey, existingSharedFile).get();

    Uri blobUri = DirectoryUtil.getBlobUri(context, file.getAndroidSharingChecksum());
    Uri leaseUri =
        DirectoryUtil.getBlobStoreLeaseUri(
            context, file.getAndroidSharingChecksum(), FILE_GROUP_EXPIRATION_DATE_SECS);
    // The file is available in the blob storage
    when(mockBackend.exists(blobUri)).thenReturn(true);

    simulateDownload(file, existingSharedFile.getFileName());
    Uri onDeviceuri =
        DirectoryUtil.getOnDeviceUri(
            context,
            newFileKey.getAllowedReaders(),
            existingSharedFile.getFileName(),
            newFileKey.getChecksum(),
            mockSilentFeedback,
            /* instanceId= */ Optional.absent(),
            false);
    assertThat(fileStorage.exists(onDeviceuri)).isTrue();

    fileGroupManager.tryToShareAfterDownload(fileGroup, file, newFileKey).get();

    verify(mockBackend).exists(blobUri);
    verify(mockBackend).openForWrite(leaseUri);

    SharedFile sharedFile = sharedFileManager.getSharedFile(newFileKey).get();
    // Verify that the SharedFile has updated its expiration date after the download.
    assertThat(sharedFile.getMaxExpirationDateSecs()).isEqualTo(FILE_GROUP_EXPIRATION_DATE_SECS);
    assertThat(sharedFile.getAndroidShared()).isTrue();
    assertThat(sharedFileManager.getOnDeviceUri(newFileKey).get()).isEqualTo(blobUri);

    // Local copy has not been deleted.
    assertThat(fileStorage.exists(onDeviceuri)).isTrue();

    ArgumentCaptor<Void> mddAndroidSharingLogArgumentCaptor = ArgumentCaptor.forClass(Void.class);

    verify(mockLogger).logMddAndroidSharingLog(mddAndroidSharingLogArgumentCaptor.capture());
    Void mddAndroidSharingLog = null;
    assertThat(mddAndroidSharingLogArgumentCaptor.getAllValues())
        .containsExactly(mddAndroidSharingLog);
    verifyNoMoreInteractions(mockLogger);
  }

  @Test
  public void tryToShareAfterDownload_toBeSharedFile_canBeCopied() throws Exception {
    // Create a file group with expiration date bigger than the expiration date of the existing
    // SharedFile.
    File tempFile = folder.newFile("blobFile");
    DataFileGroupInternal fileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1).toBuilder()
            .setExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS)
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .build();

    DataFile file = MddTestUtil.createSharedDataFile("fileId", 0);
    NewFileKey newFileKey =
        SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS);
    SharedFile existingSharedFile =
        SharedFile.newBuilder()
            .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
            .setFileName("fileName")
            .setAndroidShared(false)
            .build();
    sharedFilesMetadata.write(newFileKey, existingSharedFile).get();

    Uri blobUri = DirectoryUtil.getBlobUri(context, file.getAndroidSharingChecksum());
    Uri leaseUri =
        DirectoryUtil.getBlobStoreLeaseUri(
            context, file.getAndroidSharingChecksum(), FILE_GROUP_EXPIRATION_DATE_SECS);
    // The file isn't available yet in the blob storage
    when(mockBackend.exists(blobUri)).thenReturn(false);
    when(mockBackend.openForWrite(blobUri)).thenReturn(new FileOutputStream(tempFile));

    simulateDownload(file, existingSharedFile.getFileName());
    Uri onDeviceuri =
        DirectoryUtil.getOnDeviceUri(
            context,
            newFileKey.getAllowedReaders(),
            existingSharedFile.getFileName(),
            newFileKey.getChecksum(),
            mockSilentFeedback,
            /* instanceId= */ Optional.absent(),
            false);
    assertThat(fileStorage.exists(onDeviceuri)).isTrue();

    fileGroupManager.tryToShareAfterDownload(fileGroup, file, newFileKey).get();

    // openForWrite is called once for writing the blob, once for acquiring the lease.
    verify(mockBackend).openForWrite(blobUri);
    verify(mockBackend).openForWrite(leaseUri);

    SharedFile sharedFile = sharedFileManager.getSharedFile(newFileKey).get();
    // Verify that the SharedFile has updated its expiration date after the download.
    assertThat(sharedFile.getMaxExpirationDateSecs()).isEqualTo(FILE_GROUP_EXPIRATION_DATE_SECS);
    assertThat(sharedFile.getAndroidShared()).isTrue();
    assertThat(sharedFileManager.getOnDeviceUri(newFileKey).get()).isEqualTo(blobUri);

    // Local copy has not been deleted.
    assertThat(fileStorage.exists(onDeviceuri)).isTrue();

    ArgumentCaptor<Void> mddAndroidSharingLogArgumentCaptor = ArgumentCaptor.forClass(Void.class);

    verify(mockLogger).logMddAndroidSharingLog(mddAndroidSharingLogArgumentCaptor.capture());
    Void mddAndroidSharingLog = null;
    assertThat(mddAndroidSharingLogArgumentCaptor.getAllValues())
        .containsExactly(mddAndroidSharingLog);
    verifyNoMoreInteractions(mockLogger);
  }

  @Test
  public void tryToShareAfterDownload_nonToBeSharedFile_neverShared() throws Exception {
    // Create a file group with expiration date bigger than the expiration date of the existing
    // SharedFile.
    DataFileGroupInternal fileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1).toBuilder()
            .setExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS)
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .build();

    DataFile file = MddTestUtil.createDataFile("fileId", 0);
    NewFileKey newFileKey =
        SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS);
    SharedFile existingSharedFile =
        SharedFile.newBuilder()
            .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
            .setFileName("fileName")
            .setAndroidShared(false)
            .build();
    sharedFilesMetadata.write(newFileKey, existingSharedFile).get();

    File onDeviceFile = simulateDownload(file, existingSharedFile.getFileName());
    Uri onDeviceuri =
        DirectoryUtil.getOnDeviceUri(
            context,
            newFileKey.getAllowedReaders(),
            existingSharedFile.getFileName(),
            newFileKey.getChecksum(),
            mockSilentFeedback,
            /* instanceId= */ Optional.absent(),
            false);
    assertThat(fileStorage.exists(onDeviceuri)).isTrue();

    fileGroupManager.tryToShareAfterDownload(fileGroup, file, newFileKey).get();

    verify(mockBackend, never()).exists(any());
    verify(mockBackend, never()).openForWrite(any());

    SharedFile sharedFile = sharedFileManager.getSharedFile(newFileKey).get();
    // Verify that the SharedFile has updated its expiration date after the download.
    SharedFile expectedSharedFile =
        existingSharedFile.toBuilder()
            .setMaxExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS)
            .build();
    assertThat(sharedFile).isEqualTo(expectedSharedFile);

    // Local copy still available.
    assertThat(fileStorage.exists(onDeviceuri)).isTrue();
    onDeviceFile.delete();

    verifyNoInteractions(mockLogger);
  }

  @Test
  public void tryToShareAfterDownload_toBeSharedFile_neverShared() throws Exception {
    // Create a file group with expiration date bigger than the expiration date of the existing
    // SharedFile.
    DataFileGroupInternal fileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1).toBuilder()
            .setExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS)
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .build();

    DataFile file = MddTestUtil.createSharedDataFile("fileId", 0);
    // This should never happened in a real scenario.
    file = file.toBuilder().setAndroidSharingChecksum("").build();

    NewFileKey newFileKey =
        SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS);
    SharedFile existingSharedFile =
        SharedFile.newBuilder()
            .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
            .setFileName("fileName")
            .setAndroidShared(false)
            .build();
    sharedFilesMetadata.write(newFileKey, existingSharedFile).get();

    File onDeviceFile = simulateDownload(file, existingSharedFile.getFileName());
    Uri onDeviceuri =
        DirectoryUtil.getOnDeviceUri(
            context,
            newFileKey.getAllowedReaders(),
            existingSharedFile.getFileName(),
            newFileKey.getChecksum(),
            mockSilentFeedback,
            /* instanceId= */ Optional.absent(),
            false);
    assertThat(fileStorage.exists(onDeviceuri)).isTrue();

    fileGroupManager.tryToShareAfterDownload(fileGroup, file, newFileKey).get();

    verify(mockBackend, never()).exists(any());
    verify(mockBackend, never()).openForWrite(any());

    SharedFile sharedFile = sharedFileManager.getSharedFile(newFileKey).get();
    // Verify that the SharedFile has updated its expiration date after the download.
    SharedFile expectedSharedFile =
        existingSharedFile.toBuilder()
            .setMaxExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS)
            .build();
    assertThat(sharedFile).isEqualTo(expectedSharedFile);

    // Local copy still available.
    assertThat(fileStorage.exists(onDeviceuri)).isTrue();
    onDeviceFile.delete();

    ArgumentCaptor<Void> mddAndroidSharingLogArgumentCaptor = ArgumentCaptor.forClass(Void.class);
    verify(mockLogger).logMddAndroidSharingLog(mddAndroidSharingLogArgumentCaptor.capture());

    Void mddAndroidSharingLog = null;
    assertThat(mddAndroidSharingLogArgumentCaptor.getAllValues())
        .containsExactly(mddAndroidSharingLog);

    verifyNoMoreInteractions(mockLogger);
  }

  @Test
  public void tryToShareAfterDownload_blobUriNotSupported() throws Exception {
    // FileStorage without BlobStoreBackend
    fileStorage =
        new SynchronousFileStorage(Arrays.asList(AndroidFileBackend.builder(context).build()));
    fileGroupManager =
        new FileGroupManager(
            context,
            mockLogger,
            mockSilentFeedback,
            fileGroupsMetadata,
            sharedFileManager,
            testClock,
            Optional.of(mockAccountSource),
            SEQUENTIAL_CONTROL_EXECUTOR,
            Optional.absent(),
            fileStorage,
            downloadStageManager,
            flags);

    DataFileGroupInternal fileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 0).toBuilder()
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .build();

    // Create a to-be-shared file
    DataFile file = MddTestUtil.createSharedDataFile("fileId", 0);
    NewFileKey newFileKey =
        SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS);
    // Set the file metadata as download completed and non shared
    SharedFile existingSharedFile =
        SharedFile.newBuilder()
            .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
            .setFileName("fileName")
            .setAndroidShared(false)
            .setMaxExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS)
            .build();
    sharedFilesMetadata.write(newFileKey, existingSharedFile).get();

    fileGroupManager.tryToShareAfterDownload(fileGroup, file, newFileKey).get();

    verify(mockBackend, never()).openForWrite(any());

    SharedFile sharedFile = sharedFileManager.getSharedFile(newFileKey).get();
    assertThat(sharedFile).isEqualTo(existingSharedFile);

    ArgumentCaptor<Void> mddAndroidSharingLogArgumentCaptor = ArgumentCaptor.forClass(Void.class);
    verify(mockLogger).logMddAndroidSharingLog(mddAndroidSharingLogArgumentCaptor.capture());

    Void mddAndroidSharingLog = null;
    assertThat(mddAndroidSharingLogArgumentCaptor.getAllValues())
        .containsExactly(mddAndroidSharingLog);
    verifyNoMoreInteractions(mockLogger);
  }

  @Test
  public void tryToShareAfterDownload_nonExistentFile() throws Exception {
    DataFileGroupInternal fileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1).toBuilder()
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .build();

    DataFile file = MddTestUtil.createSharedDataFile("fileId", 0);
    NewFileKey newFileKey =
        SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS);

    ListenableFuture<Void> tryToShareFuture =
        fileGroupManager.tryToShareAfterDownload(fileGroup, file, newFileKey);

    ExecutionException exception = assertThrows(ExecutionException.class, tryToShareFuture::get);
    assertThat(exception).hasCauseThat().isInstanceOf(SharedFileMissingException.class);

    ArgumentCaptor<Void> mddAndroidSharingLogArgumentCaptor = ArgumentCaptor.forClass(Void.class);
    verify(mockLogger).logMddAndroidSharingLog(mddAndroidSharingLogArgumentCaptor.capture());

    Void mddAndroidSharingLog = null;
    assertThat(mddAndroidSharingLogArgumentCaptor.getAllValues())
        .containsExactly(mddAndroidSharingLog);
    verifyNoMoreInteractions(mockLogger);

    verify(mockBackend, never()).exists(any());
    verify(mockBackend, never()).openForWrite(any());
    verify(mockSharedFileManager, never())
        .setAndroidSharedDownloadedFileEntry(any(), any(), anyLong());
    verify(mockSharedFileManager, never()).updateMaxExpirationDateSecs(newFileKey, 0);
  }

  @Test
  public void tryToShareAfterDownload_updateMaxExpirationDateSecsReturnsFalse() throws Exception {
    // Mock SharedFileManager to test failure scenario.
    resetFileGroupManager(fileGroupsMetadata, mockSharedFileManager);
    // Create a file group with expiration date bigger than the expiration date of the existing
    // SharedFile.
    DataFileGroupInternal fileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1).toBuilder()
            .setExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS)
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .build();

    DataFile file = MddTestUtil.createDataFile("fileId", 0);
    NewFileKey newFileKey =
        SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS);
    SharedFile existingSharedFile =
        SharedFile.newBuilder()
            .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
            .setFileName("fileName")
            .setAndroidShared(false)
            .build();

    when(mockSharedFileManager.getSharedFile(newFileKey))
        .thenReturn(Futures.immediateFuture(existingSharedFile));
    when(mockSharedFileManager.updateMaxExpirationDateSecs(
            newFileKey, FILE_GROUP_EXPIRATION_DATE_SECS))
        .thenReturn(Futures.immediateFuture(false));

    File onDeviceFile = simulateDownload(file, existingSharedFile.getFileName());
    Uri onDeviceuri =
        DirectoryUtil.getOnDeviceUri(
            context,
            newFileKey.getAllowedReaders(),
            existingSharedFile.getFileName(),
            newFileKey.getChecksum(),
            mockSilentFeedback,
            /* instanceId= */ Optional.absent(),
            false);
    assertThat(fileStorage.exists(onDeviceuri)).isTrue();

    fileGroupManager.tryToShareAfterDownload(fileGroup, file, newFileKey).get();

    verify(mockBackend, never()).exists(any());
    verify(mockBackend, never()).openForWrite(any());
    verify(mockSharedFileManager, never())
        .setAndroidSharedDownloadedFileEntry(any(), any(), anyLong());
    verify(mockSharedFileManager)
        .updateMaxExpirationDateSecs(newFileKey, FILE_GROUP_EXPIRATION_DATE_SECS);
    assertThat(fileStorage.exists(onDeviceuri)).isTrue();
    onDeviceFile.delete();

    ArgumentCaptor<Void> mddAndroidSharingLogArgumentCaptor = ArgumentCaptor.forClass(Void.class);
    verify(mockLogger).logMddAndroidSharingLog(mddAndroidSharingLogArgumentCaptor.capture());
    Void mddAndroidSharingLog = null;
    assertThat(mddAndroidSharingLogArgumentCaptor.getAllValues())
        .containsExactly(mddAndroidSharingLog);
    verifyNoMoreInteractions(mockLogger);
  }

  @Test
  public void tryToShareAfterDownload_setAndroidSharedDownloadedFileEntryReturnsFalse()
      throws Exception {
    // Mock SharedFileManager to test failure scenario.
    resetFileGroupManager(fileGroupsMetadata, mockSharedFileManager);
    DataFileGroupInternal fileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2).toBuilder()
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .build();

    // Create a to-be-shared file
    DataFile file = MddTestUtil.createSharedDataFile("fileId", 0);
    NewFileKey newFileKey =
        SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS);

    SharedFile existingSharedFile =
        SharedFile.newBuilder()
            .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
            .setFileName("fileName")
            .setAndroidShared(false)
            .setMaxExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS)
            .build();

    when(mockSharedFileManager.getSharedFile(newFileKey))
        .thenReturn(Futures.immediateFuture(existingSharedFile));
    Uri blobUri = DirectoryUtil.getBlobUri(context, file.getAndroidSharingChecksum());
    Uri leaseUri =
        DirectoryUtil.getBlobStoreLeaseUri(
            context, file.getAndroidSharingChecksum(), FILE_GROUP_EXPIRATION_DATE_SECS);
    // The file is available in the blob storage
    when(mockBackend.exists(blobUri)).thenReturn(true);

    // Last operation fails
    when(mockSharedFileManager.setAndroidSharedDownloadedFileEntry(
            newFileKey, file.getAndroidSharingChecksum(), FILE_GROUP_EXPIRATION_DATE_SECS))
        .thenReturn(Futures.immediateFuture(false));
    when(mockSharedFileManager.updateMaxExpirationDateSecs(newFileKey, 0))
        .thenReturn(Futures.immediateFuture(true));

    File onDeviceFile = simulateDownload(file, existingSharedFile.getFileName());
    Uri onDeviceuri =
        DirectoryUtil.getOnDeviceUri(
            context,
            newFileKey.getAllowedReaders(),
            existingSharedFile.getFileName(),
            newFileKey.getChecksum(),
            mockSilentFeedback,
            /* instanceId= */ Optional.absent(),
            false);
    assertThat(fileStorage.exists(onDeviceuri)).isTrue();

    fileGroupManager.tryToShareAfterDownload(fileGroup, file, newFileKey).get();

    verify(mockBackend).exists(blobUri);
    // openForWrite is called only once for acquiring the lease.
    verify(mockBackend, never()).openForWrite(blobUri);
    verify(mockBackend).openForWrite(leaseUri);
    verify(mockSharedFileManager).updateMaxExpirationDateSecs(newFileKey, 0);
    assertThat(fileStorage.exists(onDeviceuri)).isTrue();
    onDeviceFile.delete();

    ArgumentCaptor<Void> mddAndroidSharingLogArgumentCaptor = ArgumentCaptor.forClass(Void.class);
    verify(mockLogger).logMddAndroidSharingLog(mddAndroidSharingLogArgumentCaptor.capture());

    Void mddAndroidSharingLog = null;
    assertThat(mddAndroidSharingLogArgumentCaptor.getAllValues())
        .containsExactly(mddAndroidSharingLog);
    verifyNoMoreInteractions(mockLogger);
  }

  @Test
  public void tryToShareAfterDownload_copyBlobThrowsIOException() throws Exception {
    DataFileGroupInternal fileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2).toBuilder()
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .build();

    // Create a to-be-shared file
    DataFile file = MddTestUtil.createSharedDataFile("fileId", 0);
    NewFileKey newFileKey =
        SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS);

    SharedFile existingSharedFile =
        SharedFile.newBuilder()
            .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
            .setFileName("fileName")
            .setAndroidShared(false)
            .setMaxExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS)
            .build();
    sharedFilesMetadata.write(newFileKey, existingSharedFile).get();

    Uri blobUri = DirectoryUtil.getBlobUri(context, file.getAndroidSharingChecksum());
    Uri leaseUri =
        DirectoryUtil.getBlobStoreLeaseUri(
            context, file.getAndroidSharingChecksum(), FILE_GROUP_EXPIRATION_DATE_SECS);
    // The file isn't available in the blob storage
    when(mockBackend.exists(blobUri)).thenReturn(false);
    // Copying the blob throws an exception
    when(mockBackend.openForWrite(blobUri)).thenThrow(new IOException());

    File onDeviceFile = simulateDownload(file, existingSharedFile.getFileName());
    Uri onDeviceuri =
        DirectoryUtil.getOnDeviceUri(
            context,
            newFileKey.getAllowedReaders(),
            existingSharedFile.getFileName(),
            newFileKey.getChecksum(),
            mockSilentFeedback,
            /* instanceId= */ Optional.absent(),
            false);
    assertThat(fileStorage.exists(onDeviceuri)).isTrue();

    fileGroupManager.tryToShareAfterDownload(fileGroup, file, newFileKey).get();

    verify(mockBackend, never()).openForWrite(leaseUri);

    SharedFile sharedFile = sharedFileManager.getSharedFile(newFileKey).get();
    assertThat(sharedFile).isEqualTo(existingSharedFile);

    // Local copy still available.
    assertThat(fileStorage.exists(onDeviceuri)).isTrue();
    onDeviceFile.delete();

    ArgumentCaptor<Void> mddAndroidSharingLogArgumentCaptor = ArgumentCaptor.forClass(Void.class);
    verify(mockLogger).logMddAndroidSharingLog(mddAndroidSharingLogArgumentCaptor.capture());

    Void mddAndroidSharingLog = null;
    assertThat(mddAndroidSharingLogArgumentCaptor.getAllValues())
        .containsExactly(mddAndroidSharingLog);
    verifyNoMoreInteractions(mockLogger);
  }

  @Test
  public void tryToShareAfterDownload_fileStorageThrowsLimitExceededException() throws Exception {
    // Create a file group with expiration date bigger than the expiration date of the existing
    // SharedFile.
    DataFileGroupInternal fileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2).toBuilder()
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .setExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS + 1)
            .build();

    // Create a to-be-shared file
    DataFile file = MddTestUtil.createSharedDataFile("fileId", 0);
    NewFileKey newFileKey =
        SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS);

    SharedFile existingSharedFile =
        SharedFile.newBuilder()
            .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
            .setFileName("fileName")
            .setAndroidShared(false)
            .setMaxExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS)
            .build();
    sharedFilesMetadata.write(newFileKey, existingSharedFile).get();

    Uri blobUri = DirectoryUtil.getBlobUri(context, file.getAndroidSharingChecksum());
    Uri leaseUri =
        DirectoryUtil.getBlobStoreLeaseUri(
            context, file.getAndroidSharingChecksum(), FILE_GROUP_EXPIRATION_DATE_SECS + 1);
    // The file is available in the blob storage
    when(mockBackend.exists(blobUri)).thenReturn(true);

    File onDeviceFile = simulateDownload(file, existingSharedFile.getFileName());
    Uri onDeviceuri =
        DirectoryUtil.getOnDeviceUri(
            context,
            newFileKey.getAllowedReaders(),
            existingSharedFile.getFileName(),
            newFileKey.getChecksum(),
            mockSilentFeedback,
            /* instanceId= */ Optional.absent(),
            false);
    assertThat(fileStorage.exists(onDeviceuri)).isTrue();

    // Writing the lease throws an exception
    when(mockBackend.openForWrite(leaseUri)).thenThrow(new LimitExceededException());

    fileGroupManager.tryToShareAfterDownload(fileGroup, file, newFileKey).get();

    verify(mockBackend, never()).openForWrite(blobUri);
    verify(mockBackend).openForWrite(leaseUri);

    SharedFile sharedFile = sharedFileManager.getSharedFile(newFileKey).get();
    // Even if there was an exception, the SharedFile has updated its expiration date after the
    // download.
    SharedFile expectedSharedFile =
        existingSharedFile.toBuilder()
            .setMaxExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS + 1)
            .build();
    assertThat(sharedFile).isEqualTo(expectedSharedFile);

    // Local copy still available.
    assertThat(fileStorage.exists(onDeviceuri)).isTrue();
    onDeviceFile.delete();

    ArgumentCaptor<Void> mddAndroidSharingLogArgumentCaptor = ArgumentCaptor.forClass(Void.class);
    verify(mockLogger).logMddAndroidSharingLog(mddAndroidSharingLogArgumentCaptor.capture());

    Void mddAndroidSharingLog = null;
    assertThat(mddAndroidSharingLogArgumentCaptor.getAllValues())
        .containsExactly(mddAndroidSharingLog);
    verifyNoMoreInteractions(mockLogger);
  }

  @Test
  public void testVerifyPendingGroupDownloaded() throws Exception {
    // Write 2 groups to the pending shared prefs.
    DataFileGroupInternal fileGroup1 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    writePendingFileGroup(testKey, fileGroup1);
    DataFileGroupInternal fileGroup2 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP_2, 2);
    writePendingFileGroup(testKey2, fileGroup2);

    // Make the verify download call fail for one file in the first group.
    writeSharedFiles(
        sharedFilesMetadata,
        fileGroup1,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE, FileStatus.DOWNLOAD_IN_PROGRESS));
    writeSharedFiles(
        sharedFilesMetadata,
        fileGroup2,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE, FileStatus.DOWNLOAD_COMPLETE));

    testClock.set(/* millis */ 1000);

    assertThat(
            fileGroupManager
                .verifyGroupDownloaded(
                    testKey,
                    fileGroup1,
                    /* removePendingVersion= */ true,
                    noCustomValidation(),
                    DownloadStateLogger.forDownload(mockLogger))
                .get())
        .isEqualTo(GroupDownloadStatus.PENDING);
    assertThat(
            fileGroupManager
                .verifyGroupDownloaded(
                    testKey2,
                    fileGroup2,
                    /* removePendingVersion= */ true,
                    noCustomValidation(),
                    DownloadStateLogger.forDownload(mockLogger))
                .get())
        .isEqualTo(GroupDownloadStatus.DOWNLOADED);

    // Verify that the pending group is still part of pending groups prefs.
    DataFileGroupInternal pendingGroup1 = readPendingFileGroup(testKey);
    assertThat(pendingGroup1).isEqualTo(fileGroup1);

    // Verify that the pending group is not written into metadata.
    assertThat(readDownloadedFileGroup(testKey)).isNull();

    fileGroup2 = FileGroupUtil.setDownloadedTimestampInMillis(fileGroup2, 1000);

    // Verify that the completely downloaded group is written into metadata.
    DataFileGroupInternal downloadedGroup2 = readDownloadedFileGroup(testKey2);
    assertThat(downloadedGroup2).isEqualTo(fileGroup2);

    verify(mockLogger)
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            TEST_GROUP,
            /* fileGroupVersionNumber= */ 0,
            /* buildId= */ 0,
            /* variantId= */ "");
    verify(mockLogger)
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            TEST_GROUP_2,
            /* fileGroupVersionNumber= */ 0,
            /* buildId= */ 0,
            /* variantId= */ "");
  }

  @Test
  public void testVerifyAllPendingGroupsDownloaded() throws Exception {
    // Write 2 groups to the pending shared prefs.
    DataFileGroupInternal fileGroup1 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    writePendingFileGroup(testKey, fileGroup1);
    DataFileGroupInternal fileGroup2 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP_2, 2);
    writePendingFileGroup(testKey2, fileGroup2);

    // Make the verify download call fail for one file in the first group.
    writeSharedFiles(
        sharedFilesMetadata,
        fileGroup1,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE, FileStatus.DOWNLOAD_IN_PROGRESS));
    writeSharedFiles(
        sharedFilesMetadata,
        fileGroup2,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE, FileStatus.DOWNLOAD_COMPLETE));

    testClock.set(/* millis */ 1000);
    fileGroupManager.verifyAllPendingGroupsDownloaded(noCustomValidation()).get();

    // Verify that the pending group is still part of pending groups prefs.
    DataFileGroupInternal pendingGroup1 = readPendingFileGroup(testKey);
    MddTestUtil.assertMessageEquals(fileGroup1, pendingGroup1);

    // Verify that the pending group is not written into metadata.
    assertThat(readDownloadedFileGroup(testKey)).isNull();

    fileGroup2 = FileGroupUtil.setDownloadedTimestampInMillis(fileGroup2, 1000);

    // Verify that the completely downloaded group is written into metadata.
    DataFileGroupInternal downloadedGroup2 = readDownloadedFileGroup(testKey2);
    assertThat(downloadedGroup2).isEqualTo(fileGroup2);

    verify(mockLogger)
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            TEST_GROUP,
            /* fileGroupVersionNumber= */ 0,
            /* buildId= */ 0,
            /* variantId= */ "");
    verify(mockLogger)
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            TEST_GROUP_2,
            /* fileGroupVersionNumber= */ 0,
            /* buildId= */ 0,
            /* variantId= */ "");
  }

  @Test
  public void testVerifyAllPendingGroupsDownloaded_existingDownloadedGroup() throws Exception {
    // Write 2 groups to the pending shared prefs.
    DataFileGroupInternal fileGroup1 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    writePendingFileGroup(testKey, fileGroup1);
    DataFileGroupInternal fileGroup2 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP_2, 2);
    writePendingFileGroup(testKey2, fileGroup2);

    // Also write 2 groups to the downloaded shared prefs.
    // fileGroup3 is the downloaded version if fileGroup1.
    DataFileGroupInternal fileGroup3 =
        MddTestUtil.createDownloadedDataFileGroupInternal(TEST_GROUP, 1);
    writeDownloadedFileGroup(testKey, fileGroup3);
    DataFileGroupInternal fileGroup4 =
        MddTestUtil.createDownloadedDataFileGroupInternal(TEST_GROUP_3, 2);
    writeDownloadedFileGroup(testKey3, fileGroup4);

    // All file are downloaded.
    writeSharedFiles(
        sharedFilesMetadata,
        fileGroup1,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE, FileStatus.DOWNLOAD_COMPLETE));
    writeSharedFiles(
        sharedFilesMetadata,
        fileGroup2,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE, FileStatus.DOWNLOAD_COMPLETE));
    writeSharedFiles(
        sharedFilesMetadata, fileGroup3, ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE));
    writeSharedFiles(
        sharedFilesMetadata,
        fileGroup4,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE, FileStatus.DOWNLOAD_COMPLETE));

    testClock.set(/* millis */ 1000);
    fileGroupManager.verifyAllPendingGroupsDownloaded(noCustomValidation()).get();

    // Verify that pending key is removed if the group is downloaded.
    assertThat(readPendingFileGroup(testKey)).isNull();
    assertThat(readPendingFileGroup(testKey2)).isNull();
    assertThat(readPendingFileGroup(testKey3)).isNull();

    fileGroup1 = FileGroupUtil.setDownloadedTimestampInMillis(fileGroup1, 1000);
    fileGroup2 = FileGroupUtil.setDownloadedTimestampInMillis(fileGroup2, 1000);

    // Verify that pending group is marked as downloaded group.
    DataFileGroupInternal downloadedGroup1 = readDownloadedFileGroup(testKey);
    assertThat(downloadedGroup1).isEqualTo(fileGroup1);
    DataFileGroupInternal downloadedGroup2 = readDownloadedFileGroup(testKey2);
    assertThat(downloadedGroup2).isEqualTo(fileGroup2);
    DataFileGroupInternal downloadedGroup4 = readDownloadedFileGroup(testKey3);
    assertThat(downloadedGroup4).isEqualTo(fileGroup4);

    verify(mockLogger)
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            TEST_GROUP,
            /* fileGroupVersionNumber= */ 0,
            /* buildId= */ 0,
            /* variantId= */ "");
    verify(mockLogger)
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            TEST_GROUP_2,
            /* fileGroupVersionNumber= */ 0,
            /* buildId= */ 0,
            /* variantId= */ "");

    // fileGroup3 should have been scheduled for deletion.
    fileGroup3 =
        fileGroup3.toBuilder()
            .setBookkeeping(DataFileGroupBookkeeping.newBuilder().setStaleExpirationDate(1).build())
            .build();
    assertThat(fileGroupsMetadata.getAllStaleGroups().get()).containsExactly(fileGroup3);
  }

  @Test
  public void testGroupDownloadFailed() throws Exception {
    // Write 2 groups to the pending shared prefs.
    DataFileGroupInternal fileGroup1 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    writePendingFileGroup(testKey, fileGroup1);
    DataFileGroupInternal fileGroup2 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP_2, 2);
    writePendingFileGroup(testKey2, fileGroup2);

    // Make the second file of the first group fail.
    writeSharedFiles(
        sharedFilesMetadata,
        fileGroup1,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE, FileStatus.DOWNLOAD_FAILED));
    writeSharedFiles(
        sharedFilesMetadata,
        fileGroup2,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE, FileStatus.DOWNLOAD_COMPLETE));

    fileGroupManager.verifyAllPendingGroupsDownloaded(noCustomValidation()).get();

    // Verify that pending key is removed if download is complete.
    assertThat(readPendingFileGroup(testKey)).isEqualTo(fileGroup1);
    assertThat(readPendingFileGroup(testKey2)).isNull();

    // Verify that downloaded key is written into metadata if download is complete.
    fileGroup2 = FileGroupUtil.setDownloadedTimestampInMillis(fileGroup2, 1000);
    DataFileGroupInternal downloadedGroup2 = readDownloadedFileGroup(testKey2);
    assertThat(downloadedGroup2).isEqualTo(fileGroup2);

    verify(mockLogger)
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            TEST_GROUP,
            /* fileGroupVersionNumber= */ 0,
            /* buildId= */ 0,
            /* variantId= */ "");
    verify(mockLogger)
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            TEST_GROUP_2,
            /* fileGroupVersionNumber= */ 0,
            /* buildId= */ 0,
            /* variantId= */ "");
  }

  @Test
  public void testDeleteUninstalledAppGroups_noUninstalledApps() throws Exception {
    PackageManager packageManager = context.getPackageManager();
    final PackageInfo packageInfo = new PackageInfo();
    packageInfo.packageName = context.getPackageName();
    packageInfo.lastUpdateTime = System.currentTimeMillis();
    Shadows.shadowOf(packageManager).addPackage(packageInfo);

    DataFileGroupInternal fileGroup1 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    writePendingFileGroup(testKey, fileGroup1);

    DataFileGroupInternal fileGroup2 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP_2, 2);
    writePendingFileGroup(testKey2, fileGroup2);

    fileGroupManager.deleteUninstalledAppGroups().get();

    assertThat(readPendingFileGroup(testKey)).isEqualTo(fileGroup1);
    assertThat(readPendingFileGroup(testKey2)).isEqualTo(fileGroup2);
  }

  @Test
  public void testDeleteUninstalledAppGroups_uninstalledApp() throws Exception {
    PackageManager packageManager = context.getPackageManager();
    final PackageInfo packageInfo = new PackageInfo();
    packageInfo.packageName = context.getPackageName();
    packageInfo.lastUpdateTime = System.currentTimeMillis();
    Shadows.shadowOf(packageManager).addPackage(packageInfo);

    DataFileGroupInternal fileGroup1 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    writePendingFileGroup(testKey, fileGroup1);
    DataFileGroupInternal fileGroup2 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP_2, 2);
    GroupKey uninstalledAppKey =
        GroupKey.newBuilder().setGroupName(TEST_GROUP_2).setOwnerPackage("uninstalled.app").build();
    writeDownloadedFileGroup(uninstalledAppKey, fileGroup2);

    assertThat(readPendingFileGroup(testKey)).isEqualTo(fileGroup1);
    assertThat(readDownloadedFileGroup(uninstalledAppKey)).isEqualTo(fileGroup2);

    fileGroupManager.deleteUninstalledAppGroups().get();

    assertThat(readPendingFileGroup(testKey)).isEqualTo(fileGroup1);
    assertThat(readDownloadedFileGroup(uninstalledAppKey)).isNull();
  }

  @Test
  public void testDeleteRemovedAccountGroups_noRemovedAccounts() throws Exception {
    Account account1 = new Account("name1", "type1");
    Account account2 = new Account("name2", "type2");

    when(mockAccountSource.getAllAccounts()).thenReturn(ImmutableList.of(account1, account2));

    DataFileGroupInternal fileGroup1 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    GroupKey key1 =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP)
            .setOwnerPackage(context.getPackageName())
            .setAccount(AccountUtil.serialize(account1))
            .build();

    DataFileGroupInternal fileGroup2 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP_2, 2);
    GroupKey key2 =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP_2)
            .setOwnerPackage(context.getPackageName())
            .setAccount(AccountUtil.serialize(account2))
            .build();

    DataFileGroupInternal fileGroup3 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP_3, 2);
    GroupKey key3 =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP_3)
            .setOwnerPackage(context.getPackageName())
            .build();

    writeDownloadedFileGroup(key1, fileGroup1);
    writeDownloadedFileGroup(key2, fileGroup2);
    writeDownloadedFileGroup(key3, fileGroup3);

    fileGroupManager.deleteRemovedAccountGroups().get();

    assertThat(fileGroupsMetadata.getAllGroupKeys().get())
        .containsExactly(getDownloadedKey(key1), getDownloadedKey(key2), getDownloadedKey(key3));

    verifyNoInteractions(mockLogger);
  }

  @Test
  public void testDeleteRemovedAccountGroups_removedAccounts() throws Exception {
    Account account1 = new Account("name1", "type1");
    Account account2 = new Account("name2", "type2");

    when(mockAccountSource.getAllAccounts()).thenReturn(ImmutableList.of(account1));

    DataFileGroupInternal fileGroup1 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    GroupKey key1 =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP)
            .setOwnerPackage(context.getPackageName())
            .setAccount(AccountUtil.serialize(account1))
            .build();

    DataFileGroupInternal fileGroup2 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP_2, 2);
    GroupKey key2 =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP_2)
            .setOwnerPackage(context.getPackageName())
            .setAccount(AccountUtil.serialize(account2))
            .build();

    DataFileGroupInternal fileGroup3 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP_3, 2);
    GroupKey key3 =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP_3)
            .setOwnerPackage(context.getPackageName())
            .build();

    writeDownloadedFileGroup(key1, fileGroup1);
    writeDownloadedFileGroup(key2, fileGroup2);
    writeDownloadedFileGroup(key3, fileGroup3);

    fileGroupManager.deleteRemovedAccountGroups().get();

    assertThat(fileGroupsMetadata.getAllGroupKeys().get())
        .containsExactly(getDownloadedKey(key1), getDownloadedKey(key3));

    verify(mockLogger)
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            TEST_GROUP_2,
            /* fileGroupVersionNumber= */ 0,
            /* buildId= */ 0,
            /* variantId= */ "");
    verifyNoMoreInteractions(mockLogger);
  }

  @Test
  public void testLogAndDeleteForMissingSharedFiles() throws Exception {
    resetFileGroupManager(fileGroupsMetadata, mockSharedFileManager);

    GroupKey downloadedGroupKeyWithFileMissing =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP)
            .setOwnerPackage(context.getPackageName())
            .setDownloaded(true)
            .build();
    DataFileGroupInternal fileGroup1 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    writeDownloadedFileGroup(downloadedGroupKeyWithFileMissing, fileGroup1);
    NewFileKey[] keys = MddTestUtil.createFileKeysForDataFileGroupInternal(fileGroup1);
    when(mockSharedFileManager.reVerifyFile(eq(keys[0]), eq(fileGroup1.getFile(0))))
        .thenReturn(Futures.immediateFailedFuture(new SharedFileMissingException()));
    when(mockSharedFileManager.reVerifyFile(eq(keys[1]), eq(fileGroup1.getFile(1))))
        .thenReturn(Futures.immediateFailedFuture(new SharedFileMissingException()));

    GroupKey pendingGroupKeyWithFileMissing =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP_2)
            .setOwnerPackage(context.getPackageName())
            .setDownloaded(false)
            .build();
    DataFileGroupInternal fileGroup2 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP_2, 2);
    writePendingFileGroup(pendingGroupKeyWithFileMissing, fileGroup2);
    // Write only the first file metadata.
    NewFileKey[] keys2 = MddTestUtil.createFileKeysForDataFileGroupInternal(fileGroup2);
    when(mockSharedFileManager.reVerifyFile(eq(keys2[0]), eq(fileGroup2.getFile(0))))
        .thenReturn(Futures.immediateFailedFuture(new SharedFileMissingException()));
    // mockSharedFileManager returns "OK" when verifying second file.

    GroupKey groupKeyWithNoFileMissing =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP_3)
            .setOwnerPackage(context.getPackageName())
            .setDownloaded(true)
            .build();
    DataFileGroupInternal fileGroup3 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP_3, 2);
    writeDownloadedFileGroup(groupKeyWithNoFileMissing, fileGroup3);
    // mockSharedFileManager always returns "OK" when verifying files.

    fileGroupManager.logAndDeleteForMissingSharedFiles().get();

    if (flags.deleteFileGroupsWithFilesMissing()) {
      assertThat(fileGroupsMetadata.getAllGroupKeys().get())
          .containsExactly(groupKeyWithNoFileMissing);
    } else {
      assertThat(fileGroupsMetadata.getAllGroupKeys().get())
          .containsExactly(
              downloadedGroupKeyWithFileMissing,
              pendingGroupKeyWithFileMissing,
              groupKeyWithNoFileMissing);
    }

    verify(mockLogger, times(2))
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            TEST_GROUP,
            /* fileGroupVersionNumber= */ 0,
            /* buildId= */ 0,
            /* variantId= */ "");
    verify(mockLogger)
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            TEST_GROUP_2,
            /* fileGroupVersionNumber= */ 0,
            /* buildId= */ 0,
            /* variantId= */ "");
    verifyNoMoreInteractions(mockLogger);
  }

  @Test
  public void getOnDeviceUri_shortcutsForSideloadedFiles_delegatesToSharedFileManagerOtherwise()
      throws Exception {
    // Ensure that sideloading is turned off
    flags.enableSideloading = Optional.of(true);

    // Create mixed group
    DataFileGroupInternal sideloadedGroup =
        DataFileGroupInternal.newBuilder()
            .setGroupName(TEST_GROUP)
            .addFile(
                DataFile.newBuilder()
                    .setFileId("sideloaded_file")
                    .setUrlToDownload("file:/test")
                    .setChecksumType(DataFile.ChecksumType.NONE)
                    .build())
            .addFile(
                DataFile.newBuilder()
                    .setFileId("standard_file")
                    .setUrlToDownload("https://url.to.download")
                    .setChecksumType(DataFile.ChecksumType.NONE)
                    .build())
            .addFile(
                DataFile.newBuilder()
                    .setFileId("inline_file")
                    .setUrlToDownload("inlinefile:sha1:checksum")
                    .setChecksum("checksum")
                    .build())
            .build();

    // Write shared files so shared file manager can get uris
    NewFileKey[] newFileKeys = MddTestUtil.createFileKeysForDataFileGroupInternal(sideloadedGroup);

    sharedFilesMetadata
        .write(
            newFileKeys[1],
            SharedFile.newBuilder()
                .setFileName(sideloadedGroup.getFile(1).getFileId())
                .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
                .build())
        .get();
    sharedFilesMetadata
        .write(
            newFileKeys[2],
            SharedFile.newBuilder()
                .setFileName(sideloadedGroup.getFile(2).getFileId())
                .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
                .build())
        .get();

    assertThat(
            fileGroupManager
                .getOnDeviceUri(sideloadedGroup.getFile(0), sideloadedGroup)
                .get()
                .getScheme())
        .isEqualTo("file");
    assertThat(
            fileGroupManager
                .getOnDeviceUri(sideloadedGroup.getFile(1), sideloadedGroup)
                .get()
                .getScheme())
        .isEqualTo("android");
    assertThat(
            fileGroupManager
                .getOnDeviceUri(sideloadedGroup.getFile(2), sideloadedGroup)
                .get()
                .getScheme())
        .isEqualTo("android");
  }

  @Test
  public void testAddGroupForDownload_withExperimentationConfig() throws Exception {
    flags.enableDownloadStageExperimentIdPropagation = Optional.of(true);

    Long buildId = 999L;
    Integer experimentId = 12345;
    DataFileGroupInternal dataFileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2).toBuilder()
            .setBuildId(buildId)
            .build();

    assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup).get()).isTrue();
  }

  @Test
  public void testAddGroupForDownload_withExperimentationConfig_overwritesPendingExperimentIds()
      throws Exception {
    flags.enableDownloadStageExperimentIdPropagation = Optional.of(true);

    long buildId = 999L;
    long buildId2 = 100L;
    int experimentId = 12345;
    int experimentId2 = 23456;

    DataFileGroupInternal dataFileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2).toBuilder()
            .setBuildId(buildId)
            .build();

    DataFileGroupInternal dataFileGroup2 =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2).toBuilder()
            .setBuildId(buildId2)
            .build();

    assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup).get()).isTrue();
    // Overwrite the group. The old experiment id should be deleted and the new experiment id should
    // be populated.
    assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup2).get()).isTrue();
  }

  @Test
  public void testDownloadPendingGroup_withExperimentationConfig_updatesExperimentIdToDownloaded()
      throws Exception {
    flags.enableDownloadStageExperimentIdPropagation = Optional.of(true);

    int experimentIdDownloading = 12345;
    int experimentIdDownloaded = 23456;
    long buildId = 999L;

    ExtraHttpHeader extraHttpHeader =
        ExtraHttpHeader.newBuilder().setKey("user-agent").setValue("mdd-downloader").build();

    // Write 1 group to the pending shared prefs.
    DataFileGroupInternal fileGroup =
        createDataFileGroup(
                TEST_GROUP,
                /* fileCount= */ 2,
                /* downloadAttemptCount= */ 3,
                /* newFilesReceivedTimestamp= */ testClock.currentTimeMillis() - 500L)
            .toBuilder()
            .setBuildId(buildId)
            .setOwnerPackage(context.getPackageName())
            .setDownloadConditions(DownloadConditions.getDefaultInstance())
            .setTrafficTag(TRAFFIC_TAG)
            .addGroupExtraHttpHeaders(extraHttpHeader)
            .build();

    writePendingFileGroup(testKey, fileGroup);

    writeSharedFiles(
        sharedFilesMetadata,
        fileGroup,
        ImmutableList.of(FileStatus.DOWNLOAD_COMPLETE, FileStatus.DOWNLOAD_COMPLETE));

    fileGroupManager
        .downloadFileGroup(testKey, DownloadConditions.getDefaultInstance(), noCustomValidation())
        .get();
  }

  @Test
  public void testRemoveFileGroup_withExperimentationConfig_removesExperimentIds()
      throws Exception {
    flags.enableDownloadStageExperimentIdPropagation = Optional.of(true);

    long buildId = 999L;
    int experimentId = 12345;
    DataFileGroupInternal dataFileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2).toBuilder()
            .setBuildId(buildId)
            .build();

    assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup).get()).isTrue();
    fileGroupManager.removeFileGroup(testKey, /* pendingOnly= */ false).get();
  }

  @Test
  public void testRemoveFileGroups_withExperimentationConfig_removesExperimentIds()
      throws Exception {
    flags.enableDownloadStageExperimentIdPropagation = Optional.of(true);

    long buildId = 999L;
    int experimentId = 12345;
    DataFileGroupInternal dataFileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2).toBuilder()
            .setBuildId(buildId)
            .build();

    assertThat(fileGroupManager.addGroupForDownload(testKey, dataFileGroup).get()).isTrue();
    fileGroupManager.removeFileGroups(ImmutableList.of(testKey)).get();
  }

  /**
   * Re-instantiates {@code fileGroupManager} with the injected parameters.
   *
   * <p>It can be used to work with the mocks for FileGroupsMetadata and/or SharedFileManager.
   */
  private void resetFileGroupManager(
      FileGroupsMetadata fileGroupsMetadata, SharedFileManager sharedFileManager) throws Exception {
    resetFileGroupManager(this.mockLogger, fileGroupsMetadata, sharedFileManager);
  }

  private void resetFileGroupManager(
      EventLogger eventLogger,
      FileGroupsMetadata fileGroupsMetadata,
      SharedFileManager sharedFileManager)
      throws Exception {
    fileGroupManager =
        new FileGroupManager(
            context,
            eventLogger,
            mockSilentFeedback,
            fileGroupsMetadata,
            sharedFileManager,
            testClock,
            Optional.of(mockAccountSource),
            SEQUENTIAL_CONTROL_EXECUTOR,
            Optional.absent(),
            fileStorage,
            downloadStageManager,
            flags);
  }

  private static DataDownloadFileGroupStats.Builder createFileGroupDetails(
      DataFileGroupInternal fileGroup) {
    return DataDownloadFileGroupStats.newBuilder()
        .setOwnerPackage(fileGroup.getOwnerPackage())
        .setFileGroupName(fileGroup.getGroupName())
        .setFileGroupVersionNumber(fileGroup.getFileGroupVersionNumber())
        .setBuildId(fileGroup.getBuildId())
        .setVariantId(fileGroup.getVariantId())
        .setFileCount(fileGroup.getFileCount());
  }

  private static Void createMddDownloadLatency(
      int downloadAttemptCount, long downloadLatencyMs, long totalLatencyMs) {
    return null;
  }

  private static DataFileGroupInternal createDataFileGroup(
      String groupName, int fileCount, int downloadAttemptCount, long newFilesReceivedTimestamp) {
    return MddTestUtil.createDataFileGroupInternal(groupName, fileCount).toBuilder()
        .setBookkeeping(
            DataFileGroupBookkeeping.newBuilder()
                .setDownloadStartedCount(downloadAttemptCount)
                .setGroupNewFilesReceivedTimestamp(newFilesReceivedTimestamp))
        .build();
  }

  /** The file download succeeds so the new file status is DOWNLOAD_COMPLETE. */
  private void fileDownloadSucceeds(NewFileKey key, Uri fileUri) {
    when(mockDownloader.startDownloading(
            any(String.class),
            any(GroupKey.class),
            anyInt(),
            anyLong(),
            any(String.class),
            eq(fileUri),
            any(String.class),
            anyInt(),
            any(DownloadConditions.class),
            isA(DownloaderCallbackImpl.class),
            anyInt(),
            anyList()))
        .then(
            new Answer<ListenableFuture<Void>>() {
              @Override
              public ListenableFuture<Void> answer(InvocationOnMock invocation) throws Throwable {
                SharedFile sharedFile =
                    sharedFileManager.getSharedFile(key).get().toBuilder()
                        .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
                        .build();
                sharedFilesMetadata.write(key, sharedFile).get();
                return Futures.immediateVoidFuture();
              }
            });
  }

  /**
   * The file download fails so the new file status is DOWNLOAD_FAILED. If failureCode is not null,
   * the downloader returns a immediateFailedFuture; otherwise it returns an immediateVoidFuture.
   */
  private void fileDownloadFails(NewFileKey key, Uri fileUri, DownloadResultCode failureCode) {
    when(mockDownloader.startDownloading(
            any(String.class),
            any(GroupKey.class),
            anyInt(),
            anyLong(),
            any(String.class),
            eq(fileUri),
            any(String.class),
            anyInt(),
            any(DownloadConditions.class),
            isA(DownloaderCallbackImpl.class),
            anyInt(),
            anyList()))
        .then(
            new Answer<ListenableFuture<Void>>() {
              @Override
              public ListenableFuture<Void> answer(InvocationOnMock invocation) throws Throwable {
                SharedFile sharedFile =
                    sharedFileManager.getSharedFile(key).get().toBuilder()
                        .setFileStatus(FileStatus.DOWNLOAD_FAILED)
                        .build();
                sharedFilesMetadata.write(key, sharedFile).get();
                if (failureCode == null) {
                  return Futures.immediateVoidFuture();
                }
                return Futures.immediateFailedFuture(
                    DownloadException.builder().setDownloadResultCode(failureCode).build());
              }
            });
  }

  private DataFileGroupInternal readPendingFileGroup(GroupKey key) throws Exception {
    GroupKey duplicateGroupKey = key.toBuilder().setDownloaded(false).build();
    return fileGroupsMetadata.read(duplicateGroupKey).get();
  }

  private DataFileGroupInternal readDownloadedFileGroup(GroupKey key) throws Exception {
    GroupKey duplicateGroupKey = key.toBuilder().setDownloaded(true).build();
    return fileGroupsMetadata.read(duplicateGroupKey).get();
  }

  private void writePendingFileGroup(GroupKey key, DataFileGroupInternal group) throws Exception {
    GroupKey duplicateGroupKey = key.toBuilder().setDownloaded(false).build();
    fileGroupsMetadata.write(duplicateGroupKey, group).get();
  }

  private void writeDownloadedFileGroup(GroupKey key, DataFileGroupInternal group)
      throws Exception {
    GroupKey duplicateGroupKey = key.toBuilder().setDownloaded(true).build();
    fileGroupsMetadata.write(duplicateGroupKey, group).get();
  }

  private void verifyAddGroupForDownloadWritesMetadata(
      GroupKey key, DataFileGroupInternal group, long expectedTimestamp) throws Exception {
    GroupKey duplicateGroupKey = key.toBuilder().setDownloaded(false).build();

    DataFileGroupInternal updatedFileGroup =
        setReceivedTimeStampWithFeatureOn(group, expectedTimestamp);
    assertThat(fileGroupsMetadata.read(duplicateGroupKey).get()).isEqualTo(updatedFileGroup);
  }

  private static GroupKey getPendingKey(GroupKey key) {
    return key.toBuilder().setDownloaded(false).build();
  }

  private static GroupKey getDownloadedKey(GroupKey key) {
    return key.toBuilder().setDownloaded(true).build();
  }

  private static DataFileGroupInternal setReceivedTimeStampWithFeatureOn(
      DataFileGroupInternal dataFileGroup, long elapsedTime) {
    DataFileGroupBookkeeping bookkeeping =
        dataFileGroup.getBookkeeping().toBuilder()
            .setGroupNewFilesReceivedTimestamp(elapsedTime)
            .build();
    return dataFileGroup.toBuilder().setBookkeeping(bookkeeping).build();
  }

  /**
   * Simulates the download of the file {@code dataFile} by writing a file with name {@code
   * fileName}.
   */
  private File simulateDownload(DataFile dataFile, String fileName) throws IOException {
    File onDeviceFile = new File(publicDirectory, fileName);
    byte[] bytes = new byte[dataFile.getByteSize()];
    try (FileOutputStream writer = new FileOutputStream(onDeviceFile)) {
      writer.write(bytes);
    }
    return onDeviceFile;
  }

  private List<Uri> getOnDeviceUrisForFileGroup(DataFileGroupInternal fileGroup) {
    ArrayList<Uri> uriList = new ArrayList<>(fileGroup.getFileCount());
    NewFileKey[] newFileKeys = MddTestUtil.createFileKeysForDataFileGroupInternal(fileGroup);

    for (int i = 0; i < newFileKeys.length; i++) {
      NewFileKey newFileKey = newFileKeys[i];
      DataFile dataFile = fileGroup.getFile(i);
      uriList.add(
          DirectoryUtil.getOnDeviceUri(
              context,
              newFileKey.getAllowedReaders(),
              dataFile.getFileId(),
              newFileKey.getChecksum(),
              mockSilentFeedback,
              /* instanceId= */ Optional.absent(),
              /* androidShared= */ false));
    }
    return uriList;
  }

  private AsyncFunction<DataFileGroupInternal, Boolean> noCustomValidation() {
    return unused -> Futures.immediateFuture(true);
  }
}
