/*
 * 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.truth.Truth.assertWithMessage;

import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import androidx.test.core.app.ApplicationProvider;
import com.google.mobiledatadownload.internal.MetadataProto.DataFileGroupInternal;
import com.google.mobiledatadownload.internal.MetadataProto.GroupKey;
import com.google.mobiledatadownload.internal.MetadataProto.GroupKeyProperties;
import com.google.android.libraries.mobiledatadownload.SilentFeedback;
import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage;
import com.google.android.libraries.mobiledatadownload.file.backends.AndroidFileBackend;
import com.google.android.libraries.mobiledatadownload.file.backends.AndroidUri;
import com.google.android.libraries.mobiledatadownload.internal.collect.GroupKeyAndGroup;
import com.google.android.libraries.mobiledatadownload.internal.logging.EventLogger;
import com.google.android.libraries.mobiledatadownload.internal.util.DirectoryUtil;
import com.google.android.libraries.mobiledatadownload.internal.util.FileGroupUtil;
import com.google.android.libraries.mobiledatadownload.internal.util.FileGroupsMetadataUtil;
import com.google.android.libraries.mobiledatadownload.internal.util.ProtoConversionUtil;
import com.google.android.libraries.mobiledatadownload.internal.util.SharedPreferencesUtil;
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.ImmutableList;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.mobiledatadownload.DownloadConfigProto.DataFileGroup;
import java.io.File;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
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.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.ParameterizedRobolectricTestRunner;
import org.robolectric.ParameterizedRobolectricTestRunner.Parameter;
import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;

@RunWith(ParameterizedRobolectricTestRunner.class)
public class FileGroupsMetadataTest {

  // TODO(b/26110951): use Parameterized runner once android_test supports it
  private enum MetadataStoreImpl {
    SP_IMPL,
  }

  // Whether to use PDS metadata store or SharedPreferences metadata store.
  @Parameter(value = 0)
  public MetadataStoreImpl metadataStoreImpl;

  @Parameter(value = 1)
  public Optional<String> instanceId;

  @Parameters(name = "metadataStoreImpl = {0} instanceId = {1}")
  public static Collection<Object[]> parameters() {
    return Arrays.asList(
        new Object[][] {
          {MetadataStoreImpl.SP_IMPL, Optional.absent()},
          {MetadataStoreImpl.SP_IMPL, Optional.of("id")},
        });
  }

  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 Executor CONTROL_EXECUTOR =
      MoreExecutors.newSequentialExecutor(Executors.newCachedThreadPool());

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

  private SynchronousFileStorage fileStorage;
  private Context context;
  private FakeTimeSource testClock;
  private FileGroupsMetadata fileGroupsMetadata;
  private Uri destinationUri;
  private Uri diagnosticUri;
  private final TestFlags flags = new TestFlags();

  @Mock EventLogger mockLogger;
  @Mock SilentFeedback mockSilentFeedback;

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

  @Before
  public void setUp() throws Exception {

    context = ApplicationProvider.getApplicationContext();

    testKey =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP)
            .setOwnerPackage(context.getPackageName())
            .setDownloaded(false)
            .build();

    testKey2 =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP_2)
            .setOwnerPackage(context.getPackageName())
            .setDownloaded(false)
            .build();

    testKey3 =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP_3)
            .setOwnerPackage(context.getPackageName())
            .setDownloaded(false)
            .build();

    fileStorage =
        new SynchronousFileStorage(Arrays.asList(AndroidFileBackend.builder(context).build()));

    testClock = new FakeTimeSource();
    destinationUri =
        AndroidUri.builder(context)
            .setPackage(context.getPackageName())
            .setRelativePath("dest.pb")
            .build();
    diagnosticUri =
        AndroidUri.builder(context)
            .setPackage(context.getPackageName())
            .setRelativePath("diag.pb")
            .build();
    SharedPreferencesFileGroupsMetadata sharedPreferencesImpl =
        new SharedPreferencesFileGroupsMetadata(
            context, testClock, mockSilentFeedback, instanceId, CONTROL_EXECUTOR);
    switch (metadataStoreImpl) {
      case SP_IMPL:
        fileGroupsMetadata = sharedPreferencesImpl;
        break;
    }
  }

  @After
  public void tearDown() throws Exception {
    if (fileStorage.exists(diagnosticUri)) {
      fileStorage.deleteFile(diagnosticUri);
    }
    if (fileStorage.exists(destinationUri)) {
      fileStorage.deleteFile(destinationUri);
    }
    fileGroupsMetadata.clear().get();
  }

  @Test
  public void serializeAndDeserializeFileGroupKey() throws Exception {
    String serializedGroupKey = FileGroupsMetadataUtil.getSerializedGroupKey(testKey);
    GroupKey deserializedGroupKey = FileGroupsMetadataUtil.deserializeGroupKey(serializedGroupKey);

    assertThat(deserializedGroupKey.getGroupName()).isEqualTo(TEST_GROUP);
    assertThat(deserializedGroupKey.getOwnerPackage()).isEqualTo(context.getPackageName());
    assertThat(deserializedGroupKey.getDownloaded()).isFalse();
  }

  @Test
  public void readAndWriteFileGroup() throws Exception {
    DataFileGroupInternal writeFileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    DataFileGroupInternal writeFileGroup2 =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP_2, 1);
    DataFileGroupInternal writeFileGroup3 =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP_3, 5);

    assertThat(fileGroupsMetadata.read(testKey).get()).isNull();
    assertThat(fileGroupsMetadata.write(testKey, writeFileGroup).get()).isTrue();

    assertThat(fileGroupsMetadata.read(testKey2).get()).isNull();
    assertThat(fileGroupsMetadata.write(testKey2, writeFileGroup2).get()).isTrue();

    assertThat(fileGroupsMetadata.read(testKey3).get()).isNull();
    assertThat(fileGroupsMetadata.write(testKey3, writeFileGroup3).get()).isTrue();

    DataFileGroupInternal readFileGroup = fileGroupsMetadata.read(testKey).get();
    MddTestUtil.assertMessageEquals(readFileGroup, writeFileGroup);

    DataFileGroupInternal readFileGroup2 = fileGroupsMetadata.read(testKey2).get();
    MddTestUtil.assertMessageEquals(readFileGroup2, writeFileGroup2);

    DataFileGroupInternal readFileGroup3 = fileGroupsMetadata.read(testKey3).get();
    MddTestUtil.assertMessageEquals(readFileGroup3, writeFileGroup3);

    verifyNoErrorInPdsMigration();
  }

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

    assertThat(fileGroupsMetadata.read(testKey).get()).isNull();
    assertThat(fileGroupsMetadata.write(testKey, writeFileGroup).get()).isTrue();

    DataFileGroupInternal readFileGroup = fileGroupsMetadata.read(testKey).get();
    MddTestUtil.assertMessageEquals(readFileGroup, writeFileGroup);

    writeFileGroup = FileGroupUtil.setStaleExpirationDate(writeFileGroup, 1000);
    assertThat(fileGroupsMetadata.write(testKey, writeFileGroup).get()).isTrue();

    readFileGroup = fileGroupsMetadata.read(testKey).get();
    MddTestUtil.assertMessageEquals(readFileGroup, writeFileGroup);

    verifyNoErrorInPdsMigration();
  }

  @Test
  public void removeFileGroup() throws Exception {
    DataFileGroupInternal fileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    DataFileGroupInternal fileGroup2 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP_2, 1);
    DataFileGroupInternal fileGroup3 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP_3, 5);

    assertThat(fileGroupsMetadata.write(testKey, fileGroup).get()).isTrue();
    assertThat(fileGroupsMetadata.remove(testKey).get()).isTrue();
    assertThat(fileGroupsMetadata.read(testKey).get()).isNull();

    assertThat(fileGroupsMetadata.write(testKey2, fileGroup2).get()).isTrue();
    assertThat(fileGroupsMetadata.remove(testKey2).get()).isTrue();
    assertThat(fileGroupsMetadata.read(testKey2).get()).isNull();

    assertThat(fileGroupsMetadata.write(testKey3, fileGroup3).get()).isTrue();
    assertThat(fileGroupsMetadata.remove(testKey3).get()).isTrue();
    assertThat(fileGroupsMetadata.read(testKey3).get()).isNull();

    verifyNoErrorInPdsMigration();
  }

  @Test
  public void readAndWriteFileGroupKeyProperties() throws Exception {
    GroupKeyProperties writeGroupKeyProperties =
        GroupKeyProperties.newBuilder().setActivatedOnDevice(true).build();
    GroupKeyProperties writeGroupKeyProperties2 =
        GroupKeyProperties.newBuilder().setActivatedOnDevice(false).build();

    assertThat(fileGroupsMetadata.readGroupKeyProperties(testKey).get()).isNull();
    assertThat(fileGroupsMetadata.writeGroupKeyProperties(testKey, writeGroupKeyProperties).get())
        .isTrue();

    assertThat(fileGroupsMetadata.readGroupKeyProperties(testKey2).get()).isNull();
    assertThat(fileGroupsMetadata.writeGroupKeyProperties(testKey2, writeGroupKeyProperties2).get())
        .isTrue();

    GroupKeyProperties readGroupKeyProperties =
        fileGroupsMetadata.readGroupKeyProperties(testKey).get();
    MddTestUtil.assertMessageEquals(writeGroupKeyProperties, readGroupKeyProperties);

    GroupKeyProperties readGroupKeyProperties2 =
        fileGroupsMetadata.readGroupKeyProperties(testKey2).get();
    MddTestUtil.assertMessageEquals(writeGroupKeyProperties2, readGroupKeyProperties2);

    verifyNoErrorInPdsMigration();
  }

  @Test
  public void clear_removesAllMetadata() throws Exception {
    DataFileGroupInternal fileGroup = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    DataFileGroupInternal fileGroup2 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP_2, 1);
    DataFileGroupInternal fileGroup3 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP_3, 5);

    File parentDir =
        new File(context.getFilesDir(), DirectoryUtil.MDD_STORAGE_MODULE + "/" + "shared");
    assertThat(parentDir.mkdirs()).isTrue();
    File garbageFile = FileGroupsMetadataUtil.getGarbageCollectorFile(context, instanceId);

    DataFileGroupInternal staleFileGroup =
        MddTestUtil.createDataFileGroupInternal("stale-group", 2).toBuilder()
            .setStaleLifetimeSecs(Duration.ofDays(1).getSeconds())
            .build();

    assertThat(fileGroupsMetadata.write(testKey, fileGroup).get()).isTrue();
    assertThat(fileGroupsMetadata.write(testKey2, fileGroup2).get()).isTrue();
    assertThat(fileGroupsMetadata.write(testKey3, fileGroup3).get()).isTrue();
    assertThat(fileGroupsMetadata.addStaleGroup(staleFileGroup).get()).isTrue();

    fileGroupsMetadata.clear().get();

    assertThat(fileGroupsMetadata.read(testKey).get()).isNull();
    assertThat(fileGroupsMetadata.read(testKey2).get()).isNull();
    assertThat(fileGroupsMetadata.read(testKey3).get()).isNull();
    assertThat(garbageFile.exists()).isFalse();

    for (File file : parentDir.listFiles()) {
      boolean unused = file.delete();
    }

    verifyNoErrorInPdsMigration();
  }

  @Test
  public void retrieveAllGroups() throws Exception {
    GroupKey notSetDownloadedGroupKey =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP)
            .setOwnerPackage(context.getPackageName())
            .build();

    DataFileGroupInternal fileGroup1 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    assertThat(fileGroupsMetadata.write(notSetDownloadedGroupKey, fileGroup1).get()).isTrue();

    GroupKey setTrueDownloadedGroupKey =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP_2)
            .setOwnerPackage(context.getPackageName())
            .setDownloaded(true)
            .build();
    DataFileGroupInternal fileGroup2 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP_2, 2);
    assertThat(fileGroupsMetadata.write(setTrueDownloadedGroupKey, fileGroup2).get()).isTrue();

    GroupKey setFalseDownloadedGroupKey =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP_3)
            .setOwnerPackage(context.getPackageName())
            .setDownloaded(false)
            .build();
    DataFileGroupInternal fileGroup3 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP_3, 2);
    assertThat(fileGroupsMetadata.write(setFalseDownloadedGroupKey, fileGroup3).get()).isTrue();

    if (metadataStoreImpl == MetadataStoreImpl.SP_IMPL) {
      // Garbage entry that will create null GroupKey
      SharedPreferences prefs =
          SharedPreferencesUtil.getSharedPreferences(
              context, FileGroupsMetadataUtil.MDD_FILE_GROUPS, instanceId);
      prefs.edit().putString("garbage-key", "garbage-value").commit();
    }

    List<GroupKeyAndGroup> allGroups = fileGroupsMetadata.getAllFreshGroups().get();
    assertThat(allGroups).hasSize(3);

    verifyNoErrorInPdsMigration();
  }

  @Test
  public void removeGroups_noGroups() throws Exception {
    // Newer pending version of this group.
    DataFileGroupInternal fileGroup1 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    GroupKey key1 =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP)
            .setOwnerPackage(context.getPackageName())
            .setDownloaded(false)
            .build();
    writePendingFileGroupToSharedPrefs(key1, fileGroup1);

    // Older downloaded version of the same group
    DataFileGroupInternal fileGroup2 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1);
    GroupKey key2 =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP)
            .setOwnerPackage(context.getPackageName())
            .setDownloaded(true)
            .build();
    writeDownloadedFileGroupToSharedPrefs(key2, fileGroup2);

    assertThat(fileGroupsMetadata.removeAllGroupsWithKeys(ImmutableList.of()).get()).isTrue();

    assertThat(readPendingFileGroupFromSharedPrefs(key1, true /*shouldExist*/))
        .isEqualTo(fileGroup1);
    assertThat(readDownloadedFileGroupFromSharedPrefs(key2, true /*shouldExist*/))
        .isEqualTo(fileGroup2);

    verifyNoErrorInPdsMigration();
  }

  @Test
  public void removeGroups_removePendingGroup() throws Exception {
    // Newer pending version of this group.
    DataFileGroupInternal fileGroup1 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    GroupKey key1 =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP)
            .setOwnerPackage(context.getPackageName())
            .setDownloaded(false)
            .build();
    writePendingFileGroupToSharedPrefs(key1, fileGroup1);

    // Older downloaded version of the same group
    DataFileGroupInternal fileGroup2 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1);
    GroupKey key2 =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP)
            .setOwnerPackage(context.getPackageName())
            .setDownloaded(true)
            .build();
    writeDownloadedFileGroupToSharedPrefs(key2, fileGroup2);

    assertThat(fileGroupsMetadata.removeAllGroupsWithKeys(Arrays.asList(key1)).get()).isTrue();

    readPendingFileGroupFromSharedPrefs(key1, false /*shouldExist*/);
    assertThat(readDownloadedFileGroupFromSharedPrefs(key2, true /*shouldExist*/))
        .isEqualTo(fileGroup2);

    verifyNoErrorInPdsMigration();
  }

  @Test
  public void removeGroups_removeDownloadedGroup() throws Exception {
    // Newer pending version of this group.
    DataFileGroupInternal fileGroup1 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 2);
    GroupKey key1 =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP)
            .setOwnerPackage(context.getPackageName())
            .setDownloaded(false)
            .build();
    writePendingFileGroupToSharedPrefs(key1, fileGroup1);

    // Older downloaded version of the same group
    DataFileGroupInternal fileGroup2 = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1);
    GroupKey key2 =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP)
            .setOwnerPackage(context.getPackageName())
            .setDownloaded(true)
            .build();
    writeDownloadedFileGroupToSharedPrefs(key2, fileGroup2);

    assertThat(fileGroupsMetadata.removeAllGroupsWithKeys(Arrays.asList(key2)).get()).isTrue();

    assertThat(readPendingFileGroupFromSharedPrefs(key1, true /*shouldExist*/))
        .isEqualTo(fileGroup1);
    readDownloadedFileGroupFromSharedPrefs(key2, false /*shouldExist*/);

    verifyNoErrorInPdsMigration();
  }

  @Test
  public void addStaleGroup_multipleGroups() throws Exception {
    long staleExpirationLifetimeSecs = 1000;

    DataFileGroupInternal fileGroup1 =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1).toBuilder()
            .setStaleLifetimeSecs(staleExpirationLifetimeSecs)
            .build();
    DataFileGroupInternal fileGroup2 =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 3).toBuilder()
            .setStaleLifetimeSecs(staleExpirationLifetimeSecs)
            .build();

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

    testClock.set(15000 /* 15 seconds */);
    assertThat(fileGroupsMetadata.addStaleGroup(fileGroup1).get()).isTrue();
    assertThat(fileGroupsMetadata.addStaleGroup(fileGroup2).get()).isTrue();

    List<DataFileGroupInternal> staleGroups = fileGroupsMetadata.getAllStaleGroups().get();
    assertThat(staleGroups).hasSize(2);

    fileGroup1 = FileGroupUtil.setStaleExpirationDate(fileGroup1, staleExpirationLifetimeSecs + 15);
    fileGroup2 = FileGroupUtil.setStaleExpirationDate(fileGroup2, staleExpirationLifetimeSecs + 15);

    assertThat(staleGroups.get(0)).isEqualTo(fileGroup1);
    assertThat(staleGroups.get(1)).isEqualTo(fileGroup2);

    verifyNoErrorInPdsMigration();
  }

  @Test
  public void removeAllStaleGroups_multipleGroups() throws Exception {
    long staleExpirationLifetimeSecs = 1000;

    List<DataFileGroupInternal> fileGroups = new ArrayList<>();
    DataFileGroupInternal fileGroup1 =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1).toBuilder()
            .setStaleLifetimeSecs(staleExpirationLifetimeSecs)
            .build();
    fileGroups.add(fileGroup1);

    DataFileGroupInternal fileGroup2 =
        MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 3).toBuilder()
            .setStaleLifetimeSecs(staleExpirationLifetimeSecs)
            .build();
    fileGroups.add(fileGroup2);

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

    assertThat(fileGroupsMetadata.writeStaleGroups(fileGroups).get()).isTrue();
    assertThat(fileGroupsMetadata.getAllStaleGroups().get()).hasSize(2);

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

    verifyNoErrorInPdsMigration();
  }

  @Test
  public void writeStaleGroups_noGroup() throws Exception {
    List<DataFileGroupInternal> fileGroups = new ArrayList<>();
    assertThat(fileGroupsMetadata.writeStaleGroups(fileGroups).get()).isTrue();
    assertThat(fileGroupsMetadata.getAllStaleGroups().get()).isEmpty();
    verifyNoErrorInPdsMigration();
  }

  /**
   * This test mainly exists to ensure that the garbage collector handles IO operations correctly
   * for large inputs.
   */
  @Test
  public void writeAndReadStaleGroups_onLotsOfFileGroups() throws Exception {
    long staleExpirationDate = 1000;

    // Create files on device so that the garbage collector can delete them
    List<DataFileGroupInternal> fileGroups = new ArrayList<>();
    for (int i = 0; i < 5; ++i) {
      DataFileGroupInternal dataFileGroup = MddTestUtil.createDataFileGroupInternal("group" + i, 1);
      dataFileGroup = FileGroupUtil.setStaleExpirationDate(dataFileGroup, staleExpirationDate);
      fileGroups.add(dataFileGroup);
    }

    assertThat(fileGroupsMetadata.writeStaleGroups(fileGroups).get()).isTrue();
    assertThat(
            fileGroupsMetadata
                .getAllStaleGroups()
                .get()
                .get(0)
                .getBookkeeping()
                .getStaleExpirationDate())
        .isEqualTo(1000);
    assertThat(fileGroupsMetadata.getAllStaleGroups().get()).containsExactlyElementsIn(fileGroups);

    verifyNoErrorInPdsMigration();
  }

  /**
   * This test mainly exists to ensure that after migrating the group metadata storage proto from
   * {@link DataFileGroup} to {@link DataFileGroupInternal}, MDD is still able to parse the group
   * metadata which was previously written to disk before the migration.
   */
  @Test
  public void writeAndReadGroups_migration_fromDataFileGroup_toDataFileGroupInternal()
      throws Exception {
    DataFileGroup fileGroup1 = MddTestUtil.createDataFileGroup(TEST_GROUP, 2);
    GroupKey key1 =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP)
            .setOwnerPackage(context.getPackageName())
            .setDownloaded(false)
            .build();
    assertThat(writeDataFileGroup(key1, fileGroup1, instanceId)).isTrue();

    // Older downloaded version of the same group
    DataFileGroup fileGroup2 = MddTestUtil.createDataFileGroup(TEST_GROUP, 1);
    GroupKey key2 =
        GroupKey.newBuilder()
            .setGroupName(TEST_GROUP)
            .setOwnerPackage(context.getPackageName())
            .setDownloaded(true)
            .build();
    assertThat(writeDataFileGroup(key2, fileGroup2, instanceId)).isTrue();

    // Make sure that parsing DataFileGroup to DataFileGroupInternal produces identical result as
    // calling proto convert.
    assertThat(fileGroupsMetadata.read(key1).get())
        .isEqualTo(ProtoConversionUtil.convert(fileGroup1));
    assertThat(fileGroupsMetadata.read(key2).get())
        .isEqualTo(ProtoConversionUtil.convert(fileGroup2));

    verifyNoErrorInPdsMigration();
  }

  @Test
  public void garbageCollectorFileSeparation() throws Exception {
    SharedPreferencesFileGroupsMetadata fileGroupsMetadataAbsent =
        new SharedPreferencesFileGroupsMetadata(
            context, testClock, mockSilentFeedback, Optional.absent(), CONTROL_EXECUTOR);

    SharedPreferencesFileGroupsMetadata fileGroupsMetadata2 =
        new SharedPreferencesFileGroupsMetadata(
            context, testClock, mockSilentFeedback, Optional.of("instance2"), CONTROL_EXECUTOR);

    SharedPreferencesFileGroupsMetadata fileGroupsMetadata3 =
        new SharedPreferencesFileGroupsMetadata(
            context, testClock, mockSilentFeedback, Optional.of("instance3"), CONTROL_EXECUTOR);

    assertThat(fileGroupsMetadataAbsent.getGarbageCollectorFile().getAbsolutePath())
        .isNotEqualTo(fileGroupsMetadata2.getGarbageCollectorFile().getAbsolutePath());

    assertThat(fileGroupsMetadata2.getGarbageCollectorFile().getAbsolutePath())
        .isNotEqualTo(fileGroupsMetadata3.getGarbageCollectorFile().getAbsolutePath());
  }

  /**
   * Writes {@link DataFileGroup} into disk. The main purpose of this method is for the convenience
   * of migration tests. Previously, the file group metadata is stored in DataFileGroup with
   * extensions. We wanted to make sure that after migrating to {@link DataFileGroupInternal}, the
   * previous metadata can still be parsed.
   */
  boolean writeDataFileGroup(
      GroupKey groupKey, DataFileGroup fileGroup, Optional<String> instanceId) {
    String serializedGroupKey = FileGroupsMetadataUtil.getSerializedGroupKey(groupKey);
    SharedPreferences prefs =
        SharedPreferencesUtil.getSharedPreferences(
            context, FileGroupsMetadataUtil.MDD_FILE_GROUPS, instanceId);
    return SharedPreferencesUtil.writeProto(prefs, serializedGroupKey, fileGroup);
  }

  private DataFileGroupInternal readPendingFileGroupFromSharedPrefs(
      GroupKey key, boolean shouldExist) throws Exception {
    GroupKey duplicateGroupKey = key.toBuilder().setDownloaded(false).build();
    return readFileGroupFromSharedPrefs(duplicateGroupKey, shouldExist);
  }

  private void writePendingFileGroupToSharedPrefs(GroupKey key, DataFileGroupInternal group)
      throws Exception {
    GroupKey duplicateGroupKey = key.toBuilder().setDownloaded(false).build();
    assertThat(fileGroupsMetadata.write(duplicateGroupKey, group).get()).isTrue();
  }

  private DataFileGroupInternal readDownloadedFileGroupFromSharedPrefs(
      GroupKey key, boolean shouldExist) throws Exception {
    GroupKey duplicateGroupKey = key.toBuilder().setDownloaded(true).build();
    return readFileGroupFromSharedPrefs(duplicateGroupKey, shouldExist);
  }

  private void writeDownloadedFileGroupToSharedPrefs(GroupKey key, DataFileGroupInternal group)
      throws Exception {
    GroupKey duplicateGroupKey = key.toBuilder().setDownloaded(true).build();
    assertThat(fileGroupsMetadata.write(duplicateGroupKey, group).get()).isTrue();
  }

  private DataFileGroupInternal readFileGroupFromSharedPrefs(GroupKey key, boolean shouldExist)
      throws Exception {
    DataFileGroupInternal group = fileGroupsMetadata.read(key).get();
    if (shouldExist) {
      assertWithMessage(String.format("Expected that key %s should exist.", key))
          .that(group)
          .isNotNull();
    } else {
      assertWithMessage(String.format("Expected that key %s should not exist.", key))
          .that(group)
          .isNull();
    }
    return group;
  }

  private void verifyNoErrorInPdsMigration() {}
}
