/*
 * 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.common.truth.Truth.assertThat;
import static com.google.common.util.concurrent.Futures.immediateFuture;
import static com.google.common.util.concurrent.Futures.immediateVoidFuture;
import static java.util.concurrent.TimeUnit.DAYS;
import static org.junit.Assert.assertThrows;
import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
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.ArgumentMatchers.isNull;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
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.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import androidx.test.core.app.ApplicationProvider;
import com.google.mobiledatadownload.internal.MetadataProto.DataFile;
import com.google.mobiledatadownload.internal.MetadataProto.DataFile.ChecksumType;
import com.google.mobiledatadownload.internal.MetadataProto.DataFileGroupInternal;
import com.google.mobiledatadownload.internal.MetadataProto.DownloadConditions;
import com.google.mobiledatadownload.internal.MetadataProto.DownloadConditions.DeviceNetworkPolicy;
import com.google.mobiledatadownload.internal.MetadataProto.DownloadConditions.DeviceStoragePolicy;
import com.google.mobiledatadownload.internal.MetadataProto.GroupKey;
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.file.common.testing.TemporaryUri;
import com.google.android.libraries.mobiledatadownload.internal.FileGroupManager.GroupDownloadStatus;
import com.google.android.libraries.mobiledatadownload.internal.Migrations.FileKeyVersion;
import com.google.android.libraries.mobiledatadownload.internal.collect.GroupKeyAndGroup;
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.EventLogger;
import com.google.android.libraries.mobiledatadownload.internal.logging.FileGroupStatsLogger;
import com.google.android.libraries.mobiledatadownload.internal.logging.LoggingStateStore;
import com.google.android.libraries.mobiledatadownload.internal.logging.NetworkLogger;
import com.google.android.libraries.mobiledatadownload.internal.logging.StorageLogger;
import com.google.android.libraries.mobiledatadownload.internal.util.FileGroupUtil;
import com.google.android.libraries.mobiledatadownload.internal.util.SharedPreferencesUtil;
import com.google.android.libraries.mobiledatadownload.testing.FakeTimeSource;
import com.google.android.libraries.mobiledatadownload.testing.MddTestDependencies;
import com.google.android.libraries.mobiledatadownload.testing.TestFlags;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.labs.concurrent.LabsFutures;
import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.mobiledatadownload.LogEnumsProto.MddClientEvent;
import com.google.mobiledatadownload.TransformProto.CompressTransform;
import com.google.mobiledatadownload.TransformProto.Transform;
import com.google.mobiledatadownload.TransformProto.Transforms;
import com.google.mobiledatadownload.TransformProto.ZipTransform;
import com.google.protobuf.ByteString;
import java.io.IOException;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.LooperMode;

// The LooperMode Mode.PAUSED fixes buggy behavior in the legacy looper implementation that can lead
// to deadlock in some cases. See documentation at:
// http://robolectric.org/javadoc/4.3/org/robolectric/annotation/LooperMode.Mode.html for more
// information.
@RunWith(RobolectricTestRunner.class)
@LooperMode(LooperMode.Mode.PAUSED)
public class MobileDataDownloadManagerTest {

  private static final String TEST_GROUP = "test-group";
  private static final GroupKey TEST_KEY =
      FileGroupUtil.createGroupKey(TEST_GROUP, "com.google.android.gms");
  private static final Executor CONTROL_EXECUTOR = Executors.newCachedThreadPool();

  private static final int DEFAULT_DAYS_SINCE_LAST_LOG = 1;

  // Note: We can't make those android uris static variable since the Uri.parse will fail
  // with initialization.
  private final Uri fileUri1 = Uri.parse(MddTestUtil.FILE_URI + "1");
  private final Uri fileUri2 = Uri.parse(MddTestUtil.FILE_URI + "2");

  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 Context context;
  private MobileDataDownloadManager mddManager;
  private final TestFlags flags = new TestFlags();

  @Rule(order = 2)
  public final TemporaryUri tmpUri = new TemporaryUri();

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

  @Mock EventLogger mockLogger;
  @Mock SharedFileManager mockSharedFileManager;
  @Mock SharedFilesMetadata mockSharedFilesMetadata;
  @Mock FileGroupManager mockFileGroupManager;
  @Mock FileGroupsMetadata mockFileGroupsMetadata;
  @Mock ExpirationHandler mockExpirationHandler;
  @Mock SilentFeedback mockSilentFeedback;
  @Mock StorageLogger mockStorageLogger;
  @Mock FileGroupStatsLogger mockFileGroupStatsLogger;
  @Mock NetworkLogger mockNetworkLogger;

  private LoggingStateStore loggingStateStore;
  private DownloadStageManager downloadStageManager;
  private FakeTimeSource testClock;

  @Captor ArgumentCaptor<List<GroupKey>> groupKeyListCaptor;

  @Before
  public void setUp() throws Exception {
    context = ApplicationProvider.getApplicationContext();
    this.testClock = new FakeTimeSource();
    testClock.advance(1, DAYS);

    loggingStateStore =
        MddTestDependencies.LoggingStateStoreImpl.SHARED_PREFERENCES.loggingStateStore(
            context, Optional.absent(), testClock, CONTROL_EXECUTOR, new Random());

    loggingStateStore.getAndResetDaysSinceLastMaintenance().get();
    testClock.advance(1, DAYS); // The next call into logging state store will return 1

    downloadStageManager = new NoOpDownloadStageManager();

    mddManager =
        new MobileDataDownloadManager(
            context,
            mockLogger,
            mockSharedFileManager,
            mockSharedFilesMetadata,
            mockFileGroupManager,
            mockFileGroupsMetadata,
            mockExpirationHandler,
            mockSilentFeedback,
            mockStorageLogger,
            mockFileGroupStatsLogger,
            mockNetworkLogger,
            Optional.absent(),
            CONTROL_EXECUTOR,
            flags,
            loggingStateStore,
            downloadStageManager);

    // Enable migrations so that init doesn't run all migrations before each test.
    setMigrationState(MobileDataDownloadManager.MDD_MIGRATED_TO_OFFROAD, true);

    when(mockSharedFileManager.init()).thenReturn(Futures.immediateFuture(true));
    when(mockSharedFileManager.clear()).thenReturn(Futures.immediateFuture(null));
    when(mockSharedFileManager.cancelDownload(any())).thenReturn(Futures.immediateFuture(null));
    when(mockSharedFileManager.cancelDownloadAndClear()).thenReturn(Futures.immediateFuture(null));

    when(mockSharedFilesMetadata.init()).thenReturn(Futures.immediateFuture(true));
    when(mockSharedFilesMetadata.clear()).thenReturn(immediateVoidFuture());

    when(mockFileGroupsMetadata.init()).thenReturn(Futures.immediateFuture(null));
    when(mockFileGroupsMetadata.clear()).thenReturn(Futures.immediateFuture(null));
    when(mockFileGroupsMetadata.getAllStaleGroups())
        .thenReturn(Futures.immediateFuture(ImmutableList.of()));
    when(mockFileGroupsMetadata.getAllFreshGroups())
        .thenReturn(Futures.immediateFuture(ImmutableList.of()));
  }

  @After
  public void tearDown() throws Exception {
    mddManager.clear().get();
  }

  @Test
  public void init_offroadDownloaderMigration() throws Exception {
    setMigrationState(MobileDataDownloadManager.MDD_MIGRATED_TO_OFFROAD, false);

    mddManager.init().get();

    verify(mockSharedFileManager).clear();
  }

  @Test
  public void init_offroadDownloaderMigration_onlyOnce() throws Exception {
    setMigrationState(MobileDataDownloadManager.MDD_MIGRATED_TO_OFFROAD, false);

    mddManager.init().get();
    mddManager.init().get();

    verify(mockSharedFileManager, times(1)).clear();
  }

  @Test
  public void initDoesNotClearsIfInternalInitSucceeds() throws Exception {
    when(mockSharedFileManager.init()).thenReturn(Futures.immediateFuture(true));

    mddManager.init().get();

    verify(mockSharedFileManager, times(0)).clear();
  }

  @Test
  public void initClearsIfInternalInitFails() throws Exception {
    when(mockSharedFileManager.init()).thenReturn(Futures.immediateFuture(false));

    mddManager.init().get();

    verify(mockSharedFileManager).clear();
  }

  @Test
  public void testAddGroupForDownload() throws Exception {
    // This tests that the default value of {allowed_readers, allowed_readers_enum} is to allow
    // access to all 1p google apps.
    DataFileGroupInternal dataFileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1);
    when(mockFileGroupManager.addGroupForDownload(eq(TEST_KEY), eq(dataFileGroup)))
        .thenReturn(Futures.immediateFuture(true));
    when(mockFileGroupManager.getFileGroup(eq(TEST_KEY), anyBoolean()))
        .thenReturn(immediateFuture(dataFileGroup));
    when(mockFileGroupManager.verifyGroupDownloaded(
            eq(TEST_KEY), eq(dataFileGroup), anyBoolean(), any(), any()))
        .thenReturn(Futures.immediateFuture(GroupDownloadStatus.PENDING));
    when(mockFileGroupsMetadata.getAllFreshGroups())
        .thenReturn(
            immediateFuture(ImmutableList.of(GroupKeyAndGroup.create(TEST_KEY, dataFileGroup))));

    assertThat(mddManager.addGroupForDownload(TEST_KEY, dataFileGroup).get()).isTrue();
    verify(mockFileGroupManager).addGroupForDownload(TEST_KEY, dataFileGroup);
    verify(mockFileGroupManager)
        .verifyGroupDownloaded(eq(TEST_KEY), eq(dataFileGroup), anyBoolean(), any(), any());
    verifyNoInteractions(mockLogger);

    assertThat(mddManager.addGroupForDownload(TEST_KEY, dataFileGroup).get()).isTrue();
  }

  @Test
  public void testAddGroupForDownload_compressedFile() throws Exception {
    Migrations.setCurrentVersion(context, FileKeyVersion.USE_CHECKSUM_ONLY);
    DataFileGroupInternal.Builder fileGroupBuilder =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1).toBuilder();
    DataFileGroupInternal dataFileGroup =
        fileGroupBuilder
            .setFile(
                0,
                fileGroupBuilder.getFile(0).toBuilder()
                    .setDownloadedFileChecksum("downloadchecksum")
                    .setDownloadTransforms(
                        Transforms.newBuilder()
                            .addTransform(
                                Transform.newBuilder()
                                    .setCompress(CompressTransform.getDefaultInstance()))))
            .build();
    when(mockFileGroupManager.addGroupForDownload(TEST_KEY, dataFileGroup))
        .thenReturn(Futures.immediateFuture(true));
    when(mockFileGroupManager.getFileGroup(eq(TEST_KEY), anyBoolean()))
        .thenReturn(immediateFuture(dataFileGroup));
    when(mockFileGroupManager.verifyGroupDownloaded(
            eq(TEST_KEY), eq(dataFileGroup), anyBoolean(), any(), any()))
        .thenReturn(Futures.immediateFuture(GroupDownloadStatus.PENDING));
    when(mockFileGroupsMetadata.getAllFreshGroups())
        .thenReturn(
            immediateFuture(ImmutableList.of(GroupKeyAndGroup.create(TEST_KEY, dataFileGroup))));

    assertThat(mddManager.addGroupForDownload(TEST_KEY, dataFileGroup).get()).isTrue();
    verify(mockFileGroupManager).addGroupForDownload(TEST_KEY, dataFileGroup);
    verify(mockFileGroupManager)
        .verifyGroupDownloaded(eq(TEST_KEY), eq(dataFileGroup), anyBoolean(), any(), any());
    verifyNoInteractions(mockLogger);

    assertThat(mddManager.addGroupForDownload(TEST_KEY, dataFileGroup).get()).isTrue();
  }

  @Test
  public void testAddGroupForDownload_deltaFile() throws Exception {
    Migrations.setCurrentVersion(context, FileKeyVersion.USE_CHECKSUM_ONLY);
    DataFileGroupInternal dataFileGroup =
        MddTestUtil.createFileGroupInternalWithDeltaFile(TEST_GROUP);
    when(mockFileGroupManager.addGroupForDownload(TEST_KEY, dataFileGroup))
        .thenReturn(Futures.immediateFuture(true));
    when(mockFileGroupManager.getFileGroup(eq(TEST_KEY), anyBoolean()))
        .thenReturn(immediateFuture(dataFileGroup));
    when(mockFileGroupManager.verifyGroupDownloaded(
            eq(TEST_KEY), eq(dataFileGroup), anyBoolean(), any(), any()))
        .thenReturn(Futures.immediateFuture(GroupDownloadStatus.PENDING));
    when(mockFileGroupsMetadata.getAllFreshGroups())
        .thenReturn(
            immediateFuture(ImmutableList.of(GroupKeyAndGroup.create(TEST_KEY, dataFileGroup))));

    assertThat(mddManager.addGroupForDownload(TEST_KEY, dataFileGroup).get()).isTrue();
    verify(mockFileGroupManager).addGroupForDownload(TEST_KEY, dataFileGroup);
    verify(mockFileGroupManager)
        .verifyGroupDownloaded(eq(TEST_KEY), eq(dataFileGroup), anyBoolean(), any(), any());
    verifyNoInteractions(mockLogger);

    assertThat(mddManager.addGroupForDownload(TEST_KEY, dataFileGroup).get()).isTrue();
  }

  @Test
  public void testAddGroupForDownload_downloadImmediate() throws Exception {
    // This tests that the default value of {allowed_readers, allowed_readers_enum} is to allow
    // access to all 1p google apps.
    DataFileGroupInternal dataFileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1).toBuilder()
            .setVariantId("testVariant")
            .setBuildId(10)
            .build();
    when(mockFileGroupManager.addGroupForDownload(TEST_KEY, dataFileGroup))
        .thenReturn(Futures.immediateFuture(true));
    when(mockFileGroupManager.getFileGroup(eq(TEST_KEY), anyBoolean()))
        .thenReturn(immediateFuture(dataFileGroup));
    when(mockFileGroupManager.verifyGroupDownloaded(
            eq(TEST_KEY), eq(dataFileGroup), anyBoolean(), any(), any()))
        .thenReturn(Futures.immediateFuture(GroupDownloadStatus.DOWNLOADED));
    when(mockFileGroupsMetadata.getAllFreshGroups())
        .thenReturn(
            immediateFuture(ImmutableList.of(GroupKeyAndGroup.create(TEST_KEY, dataFileGroup))));

    assertThat(mddManager.addGroupForDownload(TEST_KEY, dataFileGroup).get()).isTrue();
    verify(mockFileGroupManager).addGroupForDownload(TEST_KEY, dataFileGroup);
    verify(mockFileGroupManager)
        .verifyGroupDownloaded(eq(TEST_KEY), eq(dataFileGroup), anyBoolean(), any(), any());
    verify(mockLogger)
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            TEST_GROUP,
            /* fileGroupVersionNumber= */ 0,
            /* buildId= */ dataFileGroup.getBuildId(),
            /* variantId= */ dataFileGroup.getVariantId());

    assertThat(mddManager.addGroupForDownload(TEST_KEY, dataFileGroup).get()).isTrue();
  }

  @Test
  public void testAddGroupForDownload_throwsIOException() throws Exception {
    DataFileGroupInternal dataFileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1);
    when(mockFileGroupManager.addGroupForDownload(TEST_KEY, dataFileGroup))
        .thenThrow(new IOException());

    // assertThat(mddManager.addGroupForDownload(TEST_KEY, dataFileGroup).get()).isFalse();
    ExecutionException exception =
        assertThrows(
            ExecutionException.class,
            () -> mddManager.addGroupForDownload(TEST_KEY, dataFileGroup).get());
    assertThat(exception).hasCauseThat().isInstanceOf(IOException.class);
    verify(mockFileGroupManager).addGroupForDownload(TEST_KEY, dataFileGroup);
    verify(mockSilentFeedback).send(isA(IOException.class), isA(String.class));
    verifyNoInteractions(mockLogger);
  }

  @Test
  public void testAddGroupForDownload_throwsUninstalledAppException() throws Exception {
    DataFileGroupInternal dataFileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1);
    when(mockFileGroupManager.addGroupForDownload(TEST_KEY, dataFileGroup))
        .thenThrow(new UninstalledAppException());

    // assertThat(mddManager.addGroupForDownload(TEST_KEY, dataFileGroup).get()).isFalse();
    ExecutionException exception =
        assertThrows(
            ExecutionException.class,
            () -> mddManager.addGroupForDownload(TEST_KEY, dataFileGroup).get());
    assertThat(exception).hasCauseThat().isInstanceOf(UninstalledAppException.class);
    verify(mockFileGroupManager).addGroupForDownload(TEST_KEY, dataFileGroup);
    verifyNoInteractions(mockLogger);
  }

  @Test
  public void testAddGroupForDownload_throwsExpiredFileGroupException() throws Exception {
    DataFileGroupInternal dataFileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1);
    when(mockFileGroupManager.addGroupForDownload(TEST_KEY, dataFileGroup))
        .thenThrow(new ExpiredFileGroupException());

    // assertThat(mddManager.addGroupForDownload(TEST_KEY, dataFileGroup).get()).isFalse();
    ExecutionException exception =
        assertThrows(
            ExecutionException.class,
            () -> mddManager.addGroupForDownload(TEST_KEY, dataFileGroup).get());
    assertThat(exception).hasCauseThat().isInstanceOf(ExpiredFileGroupException.class);
    verify(mockFileGroupManager).addGroupForDownload(TEST_KEY, dataFileGroup);
    verifyNoInteractions(mockLogger);
  }

  @Test
  public void testAddGroupForDownload_multipleCallsSameGroup() throws Exception {
    DataFileGroupInternal dataFileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1);
    when(mockFileGroupManager.addGroupForDownload(TEST_KEY, dataFileGroup))
        .thenReturn(Futures.immediateFuture(true), Futures.immediateFuture(false));
    when(mockFileGroupManager.getFileGroup(eq(TEST_KEY), anyBoolean()))
        .thenReturn(immediateFuture(dataFileGroup));
    when(mockFileGroupManager.verifyGroupDownloaded(
            eq(TEST_KEY), eq(dataFileGroup), anyBoolean(), any(), any()))
        .thenReturn(Futures.immediateFuture(GroupDownloadStatus.PENDING));
    when(mockFileGroupsMetadata.getAllFreshGroups())
        .thenReturn(
            immediateFuture(ImmutableList.of(GroupKeyAndGroup.create(TEST_KEY, dataFileGroup))));

    assertThat(mddManager.addGroupForDownload(TEST_KEY, dataFileGroup).get()).isTrue();
    assertThat(mddManager.addGroupForDownload(TEST_KEY, dataFileGroup).get()).isTrue();
    verify(mockFileGroupManager, times(2)).addGroupForDownload(TEST_KEY, dataFileGroup);
    verify(mockFileGroupManager, times(1))
        .verifyGroupDownloaded(eq(TEST_KEY), eq(dataFileGroup), anyBoolean(), any(), any());
    verifyNoInteractions(mockExpirationHandler);
    verifyNoInteractions(mockLogger);
  }

  @Test
  public void testAddGroupForDownload_isValidGroup() throws Exception {
    DataFileGroupInternal dataFileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1).toBuilder()
            .setGroupName("")
            .setVariantId("testVariant")
            .setBuildId(10)
            .build();
    assertThat(mddManager.addGroupForDownload(TEST_KEY, dataFileGroup).get()).isFalse();
    verifyNoInteractions(mockFileGroupManager);

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

  @Test
  public void testAddGroupForDownload_noChecksum() throws Exception {
    DataFileGroupInternal.Builder fileGroupBuilder =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1).toBuilder();
    DataFileGroupInternal dataFileGroup =
        fileGroupBuilder
            .setFile(
                0,
                fileGroupBuilder.getFile(0).toBuilder()
                    .setChecksumType(ChecksumType.NONE)
                    .setChecksum(""))
            .build();

    ArgumentCaptor<DataFileGroupInternal> dataFileGroupCaptor =
        ArgumentCaptor.forClass(DataFileGroupInternal.class);

    when(mockFileGroupManager.addGroupForDownload(eq(TEST_KEY), dataFileGroupCaptor.capture()))
        .thenReturn(Futures.immediateFuture(true));
    when(mockFileGroupManager.getFileGroup(eq(TEST_KEY), anyBoolean()))
        .thenReturn(Futures.immediateFuture(dataFileGroup));
    when(mockFileGroupManager.verifyGroupDownloaded(
            eq(TEST_KEY), eq(dataFileGroup), anyBoolean(), any(), any()))
        .thenReturn(Futures.immediateFuture(GroupDownloadStatus.PENDING));
    when(mockFileGroupsMetadata.getAllFreshGroups())
        .thenReturn(
            immediateFuture(ImmutableList.of(GroupKeyAndGroup.create(TEST_KEY, dataFileGroup))));

    assertThat(mddManager.addGroupForDownload(TEST_KEY, dataFileGroup).get()).isTrue();
    verifyNoInteractions(mockLogger);

    DataFileGroupInternal capturedDataFileGroup = dataFileGroupCaptor.getValue();

    assertThat(capturedDataFileGroup.getFileCount()).isEqualTo(1);
    DataFile dataFile = capturedDataFileGroup.getFile(0);
    // Checksum of the Url.
    assertThat(dataFile.getChecksum()).isEqualTo("0d79849a839d83fbc53e3bfe794ec38a305b7220");
  }

  @Test
  public void testAddGroupForDownload_noChecksumWithZipTransform() throws Exception {
    DataFileGroupInternal.Builder fileGroupBuilder =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1).toBuilder();
    DataFileGroupInternal dataFileGroup =
        fileGroupBuilder
            .setFile(
                0,
                fileGroupBuilder.getFile(0).toBuilder()
                    .setChecksumType(ChecksumType.NONE)
                    .setChecksum("")
                    .setDownloadedFileChecksum("")
                    .setDownloadTransforms(
                        Transforms.newBuilder()
                            .addTransform(
                                Transform.newBuilder()
                                    .setZip(ZipTransform.newBuilder().setTarget("*")))))
            .build();

    ArgumentCaptor<DataFileGroupInternal> dataFileGroupCaptor =
        ArgumentCaptor.forClass(DataFileGroupInternal.class);

    when(mockFileGroupManager.addGroupForDownload(eq(TEST_KEY), dataFileGroupCaptor.capture()))
        .thenReturn(Futures.immediateFuture(true));
    when(mockFileGroupManager.getFileGroup(eq(TEST_KEY), anyBoolean()))
        .thenReturn(immediateFuture(dataFileGroup));
    when(mockFileGroupManager.verifyGroupDownloaded(
            eq(TEST_KEY), eq(dataFileGroup), anyBoolean(), any(), any()))
        .thenReturn(Futures.immediateFuture(GroupDownloadStatus.PENDING));
    when(mockFileGroupsMetadata.getAllFreshGroups())
        .thenReturn(
            immediateFuture(ImmutableList.of(GroupKeyAndGroup.create(TEST_KEY, dataFileGroup))));

    assertThat(mddManager.addGroupForDownload(TEST_KEY, dataFileGroup).get()).isTrue();
    verifyNoInteractions(mockLogger);

    DataFileGroupInternal capturedDataFileGroup = dataFileGroupCaptor.getValue();

    assertThat(capturedDataFileGroup.getFileCount()).isEqualTo(1);
    DataFile dataFile = capturedDataFileGroup.getFile(0);
    // Checksum of url is propagated to downloaded file checksum if data file has zip transform.
    assertThat(dataFile.getChecksum()).isEmpty();
    assertThat(dataFile.getDownloadedFileChecksum())
        .isEqualTo("0d79849a839d83fbc53e3bfe794ec38a305b7220");
  }

  @Test
  public void testAddGroupForDownload_noChecksumAndNotSetChecksumType() throws Exception {
    DataFileGroupInternal.Builder fileGroupBuilder =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1).toBuilder();
    // Not setting ChecksumType.NONE
    DataFileGroupInternal dataFileGroup =
        fileGroupBuilder
            .setFile(0, fileGroupBuilder.getFile(0).toBuilder().setChecksum(""))
            .build();

    assertThat(mddManager.addGroupForDownload(TEST_KEY, dataFileGroup).get()).isFalse();
    verify(mockLogger)
        .logEventSampled(
            MddClientEvent.Code.EVENT_CODE_UNSPECIFIED,
            TEST_GROUP,
            /* fileGroupVersionNumber= */ 0,
            /* buildId= */ 0,
            /* variantId= */ "");
    verifyNoInteractions(mockFileGroupManager);
  }

  @Test
  public void testAddGroupForDownload_sideloadedFile_onlyWhenSideloadingIsEnabled()
      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();

    when(mockFileGroupManager.addGroupForDownload(eq(TEST_KEY), any()))
        .thenReturn(Futures.immediateFuture(true));
    when(mockFileGroupManager.getFileGroup(eq(TEST_KEY), anyBoolean()))
        .thenReturn(immediateFuture(sideloadedGroup));
    when(mockFileGroupManager.verifyGroupDownloaded(
            eq(TEST_KEY), eq(sideloadedGroup), anyBoolean(), any(), any()))
        .thenReturn(Futures.immediateFuture(GroupDownloadStatus.PENDING));
    when(mockFileGroupsMetadata.getAllFreshGroups())
        .thenReturn(
            immediateFuture(ImmutableList.of(GroupKeyAndGroup.create(TEST_KEY, sideloadedGroup))));

    {
      // Force sideloading off
      flags.enableSideloading = Optional.of(false);

      assertThat(mddManager.addGroupForDownload(TEST_KEY, sideloadedGroup).get()).isFalse();
    }

    {
      // Force sideloading on
      flags.enableSideloading = Optional.of(true);

      assertThat(mddManager.addGroupForDownload(TEST_KEY, sideloadedGroup).get()).isTrue();
    }
  }

  @Test
  public void testRemoveFileGroup() throws Exception {
    GroupKey groupKey =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP)
            .setOwnerPackage(context.getPackageName())
            .build();
    when(mockFileGroupManager.removeFileGroup(eq(groupKey), eq(false)))
        .thenReturn(Futures.immediateFuture(null /* Void */));

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

    verify(mockFileGroupManager).removeFileGroup(groupKey, /* pendingOnly= */ false);
    verifyNoMoreInteractions(mockFileGroupManager);
    verifyNoInteractions(mockLogger);
  }

  @Test
  public void testRemoveFileGroup_onFailure() throws Exception {
    GroupKey groupKey =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP)
            .setOwnerPackage(context.getPackageName())
            .build();
    doThrow(new IOException())
        .when(mockFileGroupManager)
        .removeFileGroup(groupKey, /* pendingOnly= */ false);

    ExecutionException ex =
        assertThrows(
            ExecutionException.class,
            mddManager.removeFileGroup(groupKey, /* pendingOnly= */ false)::get);
    assertThat(ex).hasCauseThat().isInstanceOf(IOException.class);

    verify(mockFileGroupManager).removeFileGroup(groupKey, /* pendingOnly= */ false);
    verifyNoMoreInteractions(mockFileGroupManager);
    verifyNoInteractions(mockLogger);
  }

  @Test
  public void testRemoveFileGroups() 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();

    when(mockFileGroupManager.removeFileGroups(groupKeyListCaptor.capture()))
        .thenReturn(Futures.immediateVoidFuture());

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

    verify(mockFileGroupManager).removeFileGroups(anyList());
    List<GroupKey> groupKeyListCapture = groupKeyListCaptor.getValue();
    assertThat(groupKeyListCapture).hasSize(2);
    assertThat(groupKeyListCapture).contains(groupKey1);
    assertThat(groupKeyListCapture).contains(groupKey2);
  }

  @Test
  public void testRemoveFileGroups_onFailure() 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();

    when(mockFileGroupManager.removeFileGroups(groupKeyListCaptor.capture()))
        .thenReturn(Futures.immediateFailedFuture(new Exception("Test failure")));

    ExecutionException ex =
        assertThrows(
            ExecutionException.class,
            () -> mddManager.removeFileGroups(ImmutableList.of(groupKey1, groupKey2)).get());
    assertThat(ex).hasMessageThat().contains("Test failure");

    verify(mockFileGroupManager).removeFileGroups(anyList());
    List<GroupKey> groupKeyListCapture = groupKeyListCaptor.getValue();
    assertThat(groupKeyListCapture).hasSize(2);
    assertThat(groupKeyListCapture).contains(groupKey1);
    assertThat(groupKeyListCapture).contains(groupKey2);
  }

  @Test
  public void testGetDownloadedGroup() throws Exception {
    DataFileGroupInternal dataFileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1);
    when(mockFileGroupManager.getFileGroup(TEST_KEY, true))
        .thenReturn(Futures.immediateFuture(dataFileGroup));

    DataFileGroupInternal completedDataFileGroup = mddManager.getFileGroup(TEST_KEY, true).get();
    MddTestUtil.assertMessageEquals(dataFileGroup, completedDataFileGroup);
    verifyNoInteractions(mockLogger);
  }

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

    when(mockFileGroupManager.getOnDeviceUris(dataFileGroup))
        .thenReturn(
            Futures.immediateFuture(
                ImmutableMap.of(
                    dataFileGroup.getFile(0), fileUri1, dataFileGroup.getFile(1), fileUri2)));

    assertThat(
            mddManager
                .getDataFileUri(
                    dataFileGroup.getFile(0), dataFileGroup, /* verifyIsolatedStructure= */ true)
                .get())
        .isEqualTo(fileUri1);
    assertThat(
            mddManager
                .getDataFileUri(
                    dataFileGroup.getFile(1), dataFileGroup, /* verifyIsolatedStructure= */ true)
                .get())
        .isEqualTo(fileUri2);
  }

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

    Transforms compressTransform =
        Transforms.newBuilder()
            .addTransform(
                Transform.newBuilder().setCompress(CompressTransform.getDefaultInstance()))
            .build();
    dataFileGroup =
        dataFileGroup.toBuilder()
            .setFile(0, dataFileGroup.getFile(0).toBuilder().setReadTransforms(compressTransform))
            .build();

    when(mockFileGroupManager.getOnDeviceUris(dataFileGroup))
        .thenReturn(
            Futures.immediateFuture(
                ImmutableMap.of(
                    dataFileGroup.getFile(0), fileUri1, dataFileGroup.getFile(1), fileUri2)));

    assertThat(
            mddManager
                .getDataFileUri(
                    dataFileGroup.getFile(0), dataFileGroup, /* verifyIsolatedStructure= */ true)
                .get())
        .isEqualTo(fileUri1.buildUpon().encodedFragment("transform=compress").build());
    assertThat(
            mddManager
                .getDataFileUri(
                    dataFileGroup.getFile(1), dataFileGroup, /* verifyIsolatedStructure= */ true)
                .get())
        .isEqualTo(fileUri2);
  }

  @Test
  public void testGetDataFileUri_relativeFilePaths() throws Exception {
    DataFile relativePathFile = MddTestUtil.createRelativePathDataFile("file", 1, "test");
    DataFileGroupInternal testFileGroup =
        DataFileGroupInternal.newBuilder()
            .setGroupName(TEST_GROUP)
            .setPreserveFilenamesAndIsolateFiles(true)
            .addFile(relativePathFile)
            .build();

    Uri symlinkedUri =
        FileGroupUtil.getIsolatedFileUri(
            context, Optional.absent(), relativePathFile, testFileGroup);

    when(mockFileGroupManager.getOnDeviceUris(testFileGroup))
        .thenReturn(Futures.immediateFuture(ImmutableMap.of(testFileGroup.getFile(0), fileUri1)));
    when(mockFileGroupManager.getIsolatedFileUris(testFileGroup))
        .thenReturn(ImmutableMap.of(testFileGroup.getFile(0), symlinkedUri));
    when(mockFileGroupManager.verifyIsolatedFileUris(any(), any()))
        .thenReturn(ImmutableMap.of(testFileGroup.getFile(0), symlinkedUri));

    assertThat(
            mddManager
                .getDataFileUri(
                    relativePathFile, testFileGroup, /* verifyIsolatedStructure= */ true)
                .get())
        .isEqualTo(symlinkedUri);
  }

  @Test
  public void testGetDataFileUri_whenSymlinkRequiredButNotPresent_returnsNull() throws Exception {
    DataFile relativePathFile = MddTestUtil.createRelativePathDataFile("file", 1, "test");
    DataFileGroupInternal testFileGroup =
        DataFileGroupInternal.newBuilder()
            .setGroupName(TEST_GROUP)
            .setPreserveFilenamesAndIsolateFiles(true)
            .addFile(relativePathFile)
            .build();

    when(mockFileGroupManager.getOnDeviceUris(testFileGroup))
        .thenReturn(Futures.immediateFuture(ImmutableMap.of(testFileGroup.getFile(0), fileUri1)));
    when(mockFileGroupManager.getIsolatedFileUris(testFileGroup)).thenReturn(ImmutableMap.of());
    when(mockFileGroupManager.verifyIsolatedFileUris(any(), any())).thenReturn(ImmutableMap.of());

    assertThat(
            mddManager
                .getDataFileUri(
                    relativePathFile, testFileGroup, /* verifyIsolatedStructure= */ true)
                .get())
        .isNull();
  }

  @Test
  public void testImportFiles_failed() throws Exception {
    ImmutableList<DataFile> updatedDataFileList =
        ImmutableList.of(
            MddTestUtil.createDataFile("inline-file", 0).toBuilder()
                .setUrlToDownload("inlinefile:sha1:abcdef")
                .build());
    ImmutableMap<String, FileSource> inlineFileMap =
        ImmutableMap.of(
            "inline-file", FileSource.ofByteString(ByteString.copyFromUtf8("TEST_CONTENT")));
    when(mockFileGroupManager.importFilesIntoFileGroup(
            eq(TEST_KEY), anyLong(), any(), any(), any(), any(), any()))
        .thenReturn(Futures.immediateFailedFuture(new Exception("Test failure")));

    ExecutionException ex =
        assertThrows(
            ExecutionException.class,
            () ->
                mddManager
                    .importFiles(
                        TEST_KEY,
                        1,
                        "testvariant",
                        updatedDataFileList,
                        inlineFileMap,
                        Optional.absent(),
                        noCustomValidation())
                    .get());

    assertThat(ex).hasMessageThat().contains("Test failure");
    verify(mockFileGroupManager)
        .importFilesIntoFileGroup(
            eq(TEST_KEY),
            anyLong(),
            any(),
            eq(updatedDataFileList),
            eq(inlineFileMap),
            any(),
            any());
  }

  @Test
  public void testImportFiles_succeeds() throws Exception {
    ImmutableList<DataFile> updatedDataFileList =
        ImmutableList.of(
            MddTestUtil.createDataFile("inline-file", 0).toBuilder()
                .setUrlToDownload("inlinefile:sha1:abcdef")
                .build());
    ImmutableMap<String, FileSource> inlineFileMap =
        ImmutableMap.of(
            "inline-file", FileSource.ofByteString(ByteString.copyFromUtf8("TEST_CONTENT")));

    when(mockFileGroupManager.importFilesIntoFileGroup(
            eq(TEST_KEY), anyLong(), any(), any(), any(), any(), any()))
        .thenReturn(immediateVoidFuture());

    mddManager
        .importFiles(
            TEST_KEY,
            1,
            "testvariant",
            updatedDataFileList,
            inlineFileMap,
            Optional.absent(),
            noCustomValidation())
        .get();

    verify(mockFileGroupManager)
        .importFilesIntoFileGroup(
            eq(TEST_KEY),
            anyLong(),
            any(),
            eq(updatedDataFileList),
            eq(inlineFileMap),
            any(),
            any());
  }

  @Test
  public void testDownloadPendingGroup_failed() {
    when(mockFileGroupManager.downloadFileGroup(eq(TEST_KEY), isNull(), any()))
        .thenReturn(
            Futures.immediateFailedFuture(
                DownloadException.builder()
                    .setDownloadResultCode(DownloadResultCode.UNKNOWN_ERROR)
                    .setMessage("Fail")
                    .build()));

    ListenableFuture<DataFileGroupInternal> downloadFuture =
        mddManager.downloadFileGroup(TEST_KEY, Optional.absent(), noCustomValidation());
    assertThrows(ExecutionException.class, downloadFuture::get);
    DownloadException unused =
        LabsFutures.getFailureCauseAs(downloadFuture, DownloadException.class);

    verify(mockFileGroupManager).downloadFileGroup(eq(TEST_KEY), isNull(), any());
  }

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

    when(mockFileGroupManager.downloadFileGroup(eq(TEST_KEY), isNull(), any()))
        .thenReturn(Futures.immediateFuture(pendingGroup));

    assertThat(
            mddManager.downloadFileGroup(TEST_KEY, Optional.absent(), noCustomValidation()).get())
        .isEqualTo(pendingGroup);

    verify(mockFileGroupManager).downloadFileGroup(eq(TEST_KEY), isNull(), any());
  }

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

    Optional<DownloadConditions> downloadConditionsOptional =
        Optional.of(
            DownloadConditions.newBuilder()
                .setDeviceNetworkPolicy(DeviceNetworkPolicy.DOWNLOAD_ON_ANY_NETWORK)
                .setDeviceStoragePolicy(DeviceStoragePolicy.BLOCK_DOWNLOAD_IN_LOW_STORAGE)
                .build());

    when(mockFileGroupManager.downloadFileGroup(
            eq(TEST_KEY), eq(downloadConditionsOptional.get()), any()))
        .thenReturn(Futures.immediateFuture(pendingGroup));

    assertThat(
            mddManager
                .downloadFileGroup(TEST_KEY, downloadConditionsOptional, noCustomValidation())
                .get())
        .isEqualTo(pendingGroup);

    verify(mockFileGroupManager)
        .downloadFileGroup(eq(TEST_KEY), eq(downloadConditionsOptional.get()), any());
  }

  @Test
  public void testDownloadAllPendingGroups() throws Exception {
    when(mockFileGroupManager.scheduleAllPendingGroupsForDownload(eq(true), any()))
        .thenReturn(Futures.immediateFuture(null));

    mddManager.downloadAllPendingGroups(true, noCustomValidation()).get();

    verify(mockLogger).logEventSampled(MddClientEvent.Code.EVENT_CODE_UNSPECIFIED);
    verify(mockFileGroupManager).scheduleAllPendingGroupsForDownload(eq(true), any());
    verifyNoMoreInteractions(mockLogger);
  }

  @Test
  public void testVerifyPendingGroups() throws Exception {
    when(mockFileGroupManager.verifyAllPendingGroupsDownloaded(any()))
        .thenReturn(Futures.immediateFuture(null));

    mddManager.verifyAllPendingGroups(noCustomValidation()).get();

    verify(mockFileGroupManager).verifyAllPendingGroupsDownloaded(any());
    verify(mockLogger).logEventSampled(MddClientEvent.Code.EVENT_CODE_UNSPECIFIED);
    verifyNoMoreInteractions(mockLogger);
  }

  @Test
  public void testMaintenance_mddFileExpiration() throws Exception {
    assumeTrue(flags.mddEnableGarbageCollection());

    setupMaintenanceTasks();

    mddManager.maintenance().get();

    verify(mockFileGroupManager).deleteUninstalledAppGroups();

    verify(mockExpirationHandler).updateExpiration();

    verify(mockFileGroupStatsLogger).log(DEFAULT_DAYS_SINCE_LAST_LOG);
    verify(mockLogger).logEventSampled(MddClientEvent.Code.EVENT_CODE_UNSPECIFIED);
  }

  @Test
  public void testMaintenance_gcFlagControlsGcDuringMaintenance() throws Exception {
    setupMaintenanceTasks();
    flags.mddEnableGarbageCollection = Optional.of(false);

    mddManager.maintenance().get();

    verify(mockExpirationHandler, never()).updateExpiration();
  }

  @Test
  public void testMaintenance_logStorage() throws Exception {
    setupMaintenanceTasks();

    mddManager.maintenance().get();

    verify(mockStorageLogger).logStorageStats(DEFAULT_DAYS_SINCE_LAST_LOG);
  }

  @Test
  public void testMaintenance_logNetwork() throws Exception {
    setupMaintenanceTasks();

    mddManager.maintenance().get();
    verify(mockNetworkLogger).log();
  }

  @Test
  public void maintenance_triggerSync_absentSpe() throws Exception {
    mddManager =
        new MobileDataDownloadManager(
            context,
            mockLogger,
            mockSharedFileManager,
            mockSharedFilesMetadata,
            mockFileGroupManager,
            mockFileGroupsMetadata,
            mockExpirationHandler,
            mockSilentFeedback,
            mockStorageLogger,
            mockFileGroupStatsLogger,
            mockNetworkLogger,
            Optional.absent(),
            CONTROL_EXECUTOR,
            flags,
            loggingStateStore,
            downloadStageManager);

    setupMaintenanceTasks();

    mddManager.maintenance().get();

    // With absent SPE, no triggerSync was called.
    verify(mockFileGroupManager, never()).triggerSyncAllPendingGroups();
  }

  @Test
  public void testMaintenance_deleteRemovedAccountGroups() throws Exception {
    setupMaintenanceTasks();

    flags.mddDeleteGroupsRemovedAccounts = Optional.of(true);

    mddManager.maintenance().get();
    verify(mockFileGroupManager).deleteRemovedAccountGroups();
  }

  void setupMaintenanceTasks() {

    flags.enableDaysSinceLastMaintenanceTracking = Optional.of(true);

    when(mockStorageLogger.logStorageStats(DEFAULT_DAYS_SINCE_LAST_LOG))
        .thenReturn(Futures.immediateVoidFuture());
    when(mockExpirationHandler.updateExpiration()).thenReturn(Futures.immediateVoidFuture());
    when(mockFileGroupStatsLogger.log(DEFAULT_DAYS_SINCE_LAST_LOG))
        .thenReturn(Futures.immediateVoidFuture());
    when(mockNetworkLogger.log()).thenReturn(Futures.immediateVoidFuture());
    when(mockFileGroupManager.logAndDeleteForMissingSharedFiles())
        .thenReturn(Futures.immediateVoidFuture());
    when(mockFileGroupManager.deleteUninstalledAppGroups())
        .thenReturn(Futures.immediateVoidFuture());
    when(mockFileGroupManager.deleteRemovedAccountGroups())
        .thenReturn(Futures.immediateVoidFuture());
    when(mockFileGroupManager.triggerSyncAllPendingGroups()).thenReturn(immediateVoidFuture());

    when(mockFileGroupManager.verifyAndAttemptToRepairIsolatedFiles())
        .thenReturn(immediateVoidFuture());
  }

  @Test
  public void testRemoveExpiredGroupsAndFiles() throws Exception {
    setupMaintenanceTasks();

    mddManager.removeExpiredGroupsAndFiles().get();

    verify(mockExpirationHandler).updateExpiration();
  }

  @Test
  public void testClear() throws Exception {
    mddManager.clear().get();

    verify(mockSharedFileManager).cancelDownloadAndClear();
    verifyNoInteractions(mockLogger);
  }

  @Test
  public void testCheckResetTrigger_resetTrigger_noIncrement() throws Exception {
    setSavedResetValue(1);
    flags.mddResetTrigger = Optional.of(1);

    mddManager.checkResetTrigger().get();
    verify(mockSharedFileManager, never()).clear();
    verifyNoInteractions(mockLogger);
    // saved reset value should not have changed
    checkSavedResetValue(1);
    verifyNoInteractions(mockLogger);
  }

  @Test
  public void testCheckResetTrigger_resetTrigger_singleIncrement() throws Exception {
    setSavedResetValue(1);
    flags.mddResetTrigger = Optional.of(2);

    mddManager.checkResetTrigger().get();
    verify(mockSharedFileManager).cancelDownloadAndClear();
    verify(mockLogger).logEventSampled(MddClientEvent.Code.EVENT_CODE_UNSPECIFIED);
    // saved reset value should be set to 2
    checkSavedResetValue(2);
    verifyNoMoreInteractions(mockLogger);
  }

  @Test
  public void testCheckResetTrigger_resetTrigger_singleIncrementMultipleChecks() throws Exception {
    setSavedResetValue(1);
    flags.mddResetTrigger = Optional.of(2);

    mddManager.checkResetTrigger().get();
    // The second check should have no effect - clear should only be called once.
    mddManager.checkResetTrigger().get();
    verify(mockSharedFileManager).cancelDownloadAndClear();
    verify(mockLogger).logEventSampled(MddClientEvent.Code.EVENT_CODE_UNSPECIFIED);
    // saved reset value should be set to 2
    checkSavedResetValue(2);
    verifyNoMoreInteractions(mockLogger);
  }

  @Test
  public void testCheckResetTrigger_resetTrigger_multipleIncrementMultipleChecks()
      throws Exception {
    setSavedResetValue(1);
    flags.mddResetTrigger = Optional.of(2);

    mddManager.checkResetTrigger().get();

    flags.mddResetTrigger = Optional.of(3);

    mddManager.checkResetTrigger().get();

    verify(mockSharedFileManager, times(2)).cancelDownloadAndClear();
    verify(mockLogger, times(2)).logEventSampled(MddClientEvent.Code.EVENT_CODE_UNSPECIFIED);
    // saved reset value should be set to 2
    checkSavedResetValue(3);
    verifyNoMoreInteractions(mockLogger);
  }

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

    long buildId = 999L;
    int experimentId = 12345;
    DataFileGroupInternal dataFileGroup =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2).toBuilder()
            .setBuildId(buildId)
            .build();

    when(mockFileGroupsMetadata.getAllFreshGroups())
        .thenReturn(
            immediateFuture(
                ImmutableList.of(
                    GroupKeyAndGroup.create(
                        GroupKey.newBuilder().setGroupName(TEST_GROUP).build(), dataFileGroup))));

    when(mockFileGroupsMetadata.getAllStaleGroups())
        .thenReturn(immediateFuture(ImmutableList.of()));

    mddManager.clear().get();

    InOrder inOrder = inOrder(mockFileGroupsMetadata);

    inOrder.verify(mockFileGroupsMetadata).getAllFreshGroups();
    inOrder.verify(mockFileGroupsMetadata).clear();
  }

  private void setMigrationState(String key, boolean value) {
    SharedPreferences sharedPreferences =
        SharedPreferencesUtil.getSharedPreferences(
            context, MobileDataDownloadManager.MDD_MANAGER_METADATA, Optional.absent());
    sharedPreferences.edit().putBoolean(key, value).commit();
  }

  private void setSavedResetValue(int value) {
    SharedPreferences prefs =
        SharedPreferencesUtil.getSharedPreferences(
            context, MobileDataDownloadManager.MDD_MANAGER_METADATA, Optional.absent());
    SharedPreferences.Editor editor = prefs.edit();
    editor.putInt(MobileDataDownloadManager.RESET_TRIGGER, value);
    editor.commit();
  }

  private void checkSavedResetValue(int expected) {
    SharedPreferences prefs =
        SharedPreferencesUtil.getSharedPreferences(
            context, MobileDataDownloadManager.MDD_MANAGER_METADATA, Optional.absent());
    assertThat(prefs.getInt(MobileDataDownloadManager.RESET_TRIGGER, expected - 1))
        .isEqualTo(expected);
  }

  private AsyncFunction<DataFileGroupInternal, Boolean> noCustomValidation() {
    return unused -> Futures.immediateFuture(true);
  }
}
