content: package de.danoeh.antennapod.net.download.service.feed.local;
  
  import android.content.Context;
  import android.media.MediaMetadataRetriever;
  import android.net.Uri;
  import android.webkit.MimeTypeMap;
  
  import androidx.annotation.NonNull;
  import androidx.test.platform.app.InstrumentationRegistry;
  
  import de.danoeh.antennapod.model.feed.FeedItemFilter;
  import de.danoeh.antennapod.model.feed.SortOrder;
  import de.danoeh.antennapod.storage.preferences.PlaybackPreferences;
  import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface;
  import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterfaceStub;
  import de.danoeh.antennapod.model.feed.Feed;
  import de.danoeh.antennapod.model.feed.FeedItem;
  import de.danoeh.antennapod.storage.database.PodDBAdapter;
  import de.danoeh.antennapod.storage.preferences.SynchronizationSettings;
  import org.junit.After;
  import org.junit.Before;
  import org.junit.Test;
  import org.junit.runner.RunWith;
  import org.mockito.MockedStatic;
  import org.mockito.Mockito;
  import org.robolectric.RobolectricTestRunner;
  import org.robolectric.shadows.ShadowMediaMetadataRetriever;
  
  import java.io.File;
  import java.io.IOException;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.List;
  import java.util.Objects;
  
  import de.danoeh.antennapod.storage.preferences.UserPreferences;
  import de.danoeh.antennapod.storage.database.DBReader;
  import de.danoeh.antennapod.storage.database.DBWriter;
  
  import static org.hamcrest.CoreMatchers.is;
  import static org.hamcrest.CoreMatchers.endsWith;
  import static org.hamcrest.CoreMatchers.startsWith;
  import static org.hamcrest.MatcherAssert.assertThat;
  import static org.hamcrest.Matchers.empty;
  import static org.junit.Assert.assertEquals;
  import static org.mockito.ArgumentMatchers.any;
  import static org.robolectric.Shadows.shadowOf;
  
  /**
   * Test local feeds handling in class LocalFeedUpdater.
   */
  @RunWith(RobolectricTestRunner.class)
  public class LocalFeedUpdaterTest {
  
      /**
       * URL to locate the local feed media files on the external storage (SD card).
       * The exact URL doesn't matter here as access to external storage is mocked
       * (seems not to be supported by Robolectric).
       */
      private static final String FEED_URL =
              "content://com.android.externalstorage.documents/tree/primary%3ADownload%2Flocal-feed";
      private static final String LOCAL_FEED_DIR1 = "src/test/assets/local-feed1";
      private static final String LOCAL_FEED_DIR2 = "src/test/assets/local-feed2";
  
      private Context context;
  
      @Before
      public void setUp() throws Exception {
          // Initialize environment
          context = InstrumentationRegistry.getInstrumentation().getContext();
          UserPreferences.init(context);
          PlaybackPreferences.init(context);
          SynchronizationSettings.init(context);
          DownloadServiceInterface.setImpl(new DownloadServiceInterfaceStub());
  
          // Initialize database
          PodDBAdapter.init(context);
          PodDBAdapter.deleteDatabase();
          PodDBAdapter adapter = PodDBAdapter.getInstance();
          adapter.open();
          adapter.close();
  
          mapDummyMetadata(LOCAL_FEED_DIR1);
          mapDummyMetadata(LOCAL_FEED_DIR2);
          shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("mp3", "audio/mp3");
      }
  
      @After
      public void tearDown() {
          DBWriter.tearDownTests();
          PodDBAdapter.tearDownTests();
      }
  
      /**
       * Test adding a new local feed.
       */
      @Test
      public void testUpdateFeed_AddNewFeed() {
          // check for empty database
          List<Feed> feedListBefore = DBReader.getFeedList();
          assertThat(feedListBefore, is(empty()));
  
          callUpdateFeed(LOCAL_FEED_DIR2);
  
          // verify new feed in database
          verifySingleFeedInDatabaseAndItemCount(2);
          Feed feedAfter = verifySingleFeedInDatabase();
          assertEquals(FEED_URL, feedAfter.getDownloadUrl());
      }
  
      /**
       * Test adding further items to an existing local feed.
       */
      @Test
      public void testUpdateFeed_AddMoreItems() {
          // add local feed with 1 item (localFeedDir1)
          callUpdateFeed(LOCAL_FEED_DIR1);
  
          // now add another item (by changing to local feed folder localFeedDir2)
          callUpdateFeed(LOCAL_FEED_DIR2);
  
          verifySingleFeedInDatabaseAndItemCount(2);
      }
  
      /**
       * Test removing items from an existing local feed without a corresponding media file.
       */
      @Test
      public void testUpdateFeed_RemoveItems() {
          // add local feed with 2 items (localFeedDir1)
          callUpdateFeed(LOCAL_FEED_DIR2);
  
          // now remove an item (by changing to local feed folder localFeedDir1)
          callUpdateFeed(LOCAL_FEED_DIR1);
  
          verifySingleFeedInDatabaseAndItemCount(1);
      }
  
      /**
       * Test feed icon defined in the local feed media folder.
       */
      @Test
      public void testUpdateFeed_FeedIconFromFolder() {
          callUpdateFeed(LOCAL_FEED_DIR2);
  
          Feed feedAfter = verifySingleFeedInDatabase();
          assertThat(feedAfter.getImageUrl(), endsWith("local-feed2/folder.png"));
      }
  
      /**
       * Test default feed icon if there is no matching file in the local feed media folder.
       */
      @Test
      public void testUpdateFeed_FeedIconDefault() {
          callUpdateFeed(LOCAL_FEED_DIR1);
  
          Feed feedAfter = verifySingleFeedInDatabase();
          assertThat(feedAfter.getImageUrl(), startsWith(Feed.PREFIX_GENERATIVE_COVER));
      }
  
      /**
       * Test default feed metadata.
       *
       * @see #mapDummyMetadata Title and PubDate are dummy values.
       */
      @Test
      public void testUpdateFeed_FeedMetadata() {
          callUpdateFeed(LOCAL_FEED_DIR1);
  
          Feed feed = verifySingleFeedInDatabase();
          List<FeedItem> feedItems = DBReader.getFeedItemList(feed, FeedItemFilter.unfiltered(),
                  SortOrder.DATE_NEW_OLD, 0, Integer.MAX_VALUE);
          assertEquals("track1", feedItems.get(0).getTitle());
      }
  
      @Test
      public void testGetImageUrl_EmptyFolder() {
          String imageUrl = LocalFeedUpdater.getImageUrl(Collections.emptyList(), Uri.EMPTY);
          assertThat(imageUrl, startsWith(Feed.PREFIX_GENERATIVE_COVER));
      }
  
      @Test
      public void testGetImageUrl_NoImageButAudioFiles() {
          List<FastDocumentFile> folder = Collections.singletonList(mockDocumentFile("audio.mp3", "audio/mp3"));
          String imageUrl = LocalFeedUpdater.getImageUrl(folder, Uri.EMPTY);
          assertThat(imageUrl, startsWith(Feed.PREFIX_GENERATIVE_COVER));
      }
  
      @Test
      public void testGetImageUrl_PreferredImagesFilenames() {
          for (String filename : LocalFeedUpdater.PREFERRED_FEED_IMAGE_FILENAMES) {
              List<FastDocumentFile> folder = Arrays.asList(mockDocumentFile("audio.mp3", "audio/mp3"),
                      mockDocumentFile(filename, "image/jpeg")); // image MIME type doesn't matter
              String imageUrl = LocalFeedUpdater.getImageUrl(folder, Uri.EMPTY);
              assertThat(imageUrl, endsWith(filename));
          }
      }
  
      @Test
      public void testGetImageUrl_OtherImageFilenameJpg() {
          List<FastDocumentFile> folder = Arrays.asList(mockDocumentFile("audio.mp3", "audio/mp3"),
                  mockDocumentFile("my-image.jpg", "image/jpeg"));
          String imageUrl = LocalFeedUpdater.getImageUrl(folder, Uri.EMPTY);
          assertThat(imageUrl, endsWith("my-image.jpg"));
      }
  
      @Test
      public void testGetImageUrl_OtherImageFilenameJpeg() {
          List<FastDocumentFile> folder = Arrays.asList(mockDocumentFile("audio.mp3", "audio/mp3"),
                  mockDocumentFile("my-image.jpeg", "image/jpeg"));
          String imageUrl = LocalFeedUpdater.getImageUrl(folder, Uri.EMPTY);
          assertThat(imageUrl, endsWith("my-image.jpeg"));
      }
  
      @Test
      public void testGetImageUrl_OtherImageFilenamePng() {
          List<FastDocumentFile> folder = Arrays.asList(mockDocumentFile("audio.mp3", "audio/mp3"),
                  mockDocumentFile("my-image.png", "image/png"));
          String imageUrl = LocalFeedUpdater.getImageUrl(folder, Uri.EMPTY);
          assertThat(imageUrl, endsWith("my-image.png"));
      }
  
      @Test
      public void testGetImageUrl_OtherImageFilenameUnsupportedMimeType() {
          List<FastDocumentFile> folder = Arrays.asList(mockDocumentFile("audio.mp3", "audio/mp3"),
                  mockDocumentFile("my-image.svg", "image/svg+xml"));
          String imageUrl = LocalFeedUpdater.getImageUrl(folder, Uri.EMPTY);
          assertThat(imageUrl, startsWith(Feed.PREFIX_GENERATIVE_COVER));
      }
  
      /**
       * Fill ShadowMediaMetadataRetriever with dummy duration and title.
       *
       * @param localFeedDir assets local feed folder with media files
       */
      private void mapDummyMetadata(@NonNull String localFeedDir) {
          for (String fileName : Objects.requireNonNull(new File(localFeedDir).list())) {
              String path = localFeedDir + '/' + fileName;
              ShadowMediaMetadataRetriever.addMetadata(path,
                      MediaMetadataRetriever.METADATA_KEY_DURATION, "10");
              ShadowMediaMetadataRetriever.addMetadata(path,
                      MediaMetadataRetriever.METADATA_KEY_TITLE, fileName);
              ShadowMediaMetadataRetriever.addMetadata(path,
                      MediaMetadataRetriever.METADATA_KEY_DATE, "20200601T222324");
          }
      }
  
      /**
       * Calls the method LocalFeedUpdater#tryUpdateFeed with the given local feed folder.
       *
       * @param localFeedDir assets local feed folder with media files
       */
      private void callUpdateFeed(@NonNull String localFeedDir) {
          try (MockedStatic<FastDocumentFile> dfMock = Mockito.mockStatic(FastDocumentFile.class)) {
              // mock external storage
              dfMock.when(() -> FastDocumentFile.list(any(), any())).thenReturn(mockLocalFolder(localFeedDir));
  
              // call method to test
              Feed feed = new Feed(FEED_URL, null);
              try {
                  LocalFeedUpdater.tryUpdateFeed(feed, context, null, null);
              } catch (IOException e) {
                  throw new RuntimeException(e);
              }
          }
      }
  
      /**
       * Verify that the database contains exactly one feed and return that feed.
       */
      @NonNull
      private static Feed verifySingleFeedInDatabase() {
          List<Feed> feedListAfter = DBReader.getFeedList();
          assertEquals(1, feedListAfter.size());
          return feedListAfter.get(0);
      }
  
      /**
       * Verify that the database contains exactly one feed and the number of
       * items in the feed.
       *
       * @param expectedItemCount expected number of items in the feed
       */
      private static void verifySingleFeedInDatabaseAndItemCount(int expectedItemCount) {
          Feed feed = verifySingleFeedInDatabase();
          List<FeedItem> feedItems = DBReader.getFeedItemList(feed, FeedItemFilter.unfiltered(),
                  SortOrder.DATE_NEW_OLD, 0, Integer.MAX_VALUE);
          assertEquals(expectedItemCount, feedItems.size());
      }
  
      /**
       * Create a DocumentFile mock object.
       */
      @NonNull
      private static FastDocumentFile mockDocumentFile(@NonNull String fileName, @NonNull String mimeType) {
          return new FastDocumentFile(fileName, mimeType, Uri.parse("file:///path/" + fileName), 0, 0);
      }
  
      private static List<FastDocumentFile> mockLocalFolder(String folderName) {
          List<FastDocumentFile> files = new ArrayList<>();
          for (File f : Objects.requireNonNull(new File(folderName).listFiles())) {
              String extension = MimeTypeMap.getFileExtensionFromUrl(f.getPath());
              String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
              files.add(new FastDocumentFile(f.getName(), mimeType,
                      Uri.parse(f.toURI().toString()), f.length(), f.lastModified()));
          }
          return files;
      }
  }
  