package de.danoeh.antennapod.storage.database;

import android.content.Context;
import de.danoeh.antennapod.model.download.DownloadError;
import de.danoeh.antennapod.model.download.DownloadResult;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedItemFilter;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.model.feed.SortOrder;
import de.danoeh.antennapod.net.sync.serviceinterface.SynchronizationQueue;
import de.danoeh.antennapod.net.sync.serviceinterface.SynchronizationQueueStub;
import de.danoeh.antennapod.storage.preferences.UserPreferences;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ExecutionException;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;

@RunWith(RobolectricTestRunner.class)
public class FeedDatabaseWriterTest {
    private Context context;

    @Before
    public void setUp() {
        context = RuntimeEnvironment.getApplication();
        UserPreferences.init(context);
        PodDBAdapter.init(context);
        PodDBAdapter.deleteDatabase();
        PodDBAdapter adapter = PodDBAdapter.getInstance();
        adapter.open();
        adapter.close();
        SynchronizationQueue.setInstance(new SynchronizationQueueStub());
    }

    @Test
    public void testStoreNewFeed() {
        Feed feed = createFeed();
        for (int i = 0; i < 3; i++) {
            feed.getItems().add(createItem("item-" + i, "Item " + i, feed));
        }
        Feed updatedFeed = FeedDatabaseWriter.updateFeed(context, feed, false);
        List<FeedItem> storedItems = DBReader.getFeedItemList(updatedFeed, FeedItemFilter.unfiltered(),
                SortOrder.EPISODE_TITLE_A_Z, 0, Integer.MAX_VALUE);
        assertEquals(3, storedItems.size());
        for (int i = 0; i < 3; i++) {
            assertEquals("item-" + i, storedItems.get(i).getItemIdentifier());
        }
    }

    @Test
    public void testAddItemsToExistingFeed() {
        Feed feed = createFeed();
        for (int i = 0; i < 3; i++) {
            feed.getItems().add(createItem("item-" + i, "Item " + i, feed));
        }
        feed = FeedDatabaseWriter.updateFeed(context, feed, false);

        Feed updatedFeed = createFeed();
        updatedFeed.setId(feed.getId());
        for (int i = 3; i < 6; i++) {
            updatedFeed.getItems().add(createItem("item-" + i, "Item " + i, feed));
        }
        FeedDatabaseWriter.updateFeed(context, updatedFeed, false);

        List<FeedItem> dbItems = DBReader.getFeedItemList(feed, FeedItemFilter.unfiltered(),
                SortOrder.EPISODE_TITLE_A_Z, 0, Integer.MAX_VALUE);
        assertEquals(6, dbItems.size());
        for (int i = 0; i < 6; i++) {
            assertEquals("item-" + i, dbItems.get(i).getItemIdentifier());
        }
    }

    @Test
    public void testAddOrUpdateItems() throws ExecutionException, InterruptedException {
        Feed feed = createFeed();
        for (int i = 0; i < 3; i++) {
            feed.getItems().add(createItem("item-" + i, "Item " + i, feed));
        }
        feed = FeedDatabaseWriter.updateFeed(context, feed, false);
        DBReader.getFeedItemList(feed, FeedItemFilter.unfiltered(),
                SortOrder.EPISODE_TITLE_A_Z, 0, Integer.MAX_VALUE);
        DBWriter.markItemPlayed(feed.getItems().get(2), FeedItem.PLAYED, false).get();

        Feed updatedFeed = createFeed();
        updatedFeed.setId(feed.getId());
        for (int i = 2; i < 5; i++) {
            updatedFeed.getItems().add(createItem("item-" + i, "Item " + i, feed));
        }
        FeedDatabaseWriter.updateFeed(context, updatedFeed, false);

        List<FeedItem> dbItems = DBReader.getFeedItemList(feed, FeedItemFilter.unfiltered(),
                SortOrder.EPISODE_TITLE_A_Z, 0, Integer.MAX_VALUE);
        assertEquals(5, dbItems.size());
        for (int i = 0; i < 5; i++) {
            assertEquals("item-" + i, dbItems.get(i).getItemIdentifier());
        }
        assertEquals(FeedItem.PLAYED, dbItems.get(2).getPlayState());
    }

    @Test
    public void testDuplicateItemsInFeed() {
        Feed feed = createFeed();
        feed.getItems().add(createItem("id1", "Duplicate Title", feed));
        feed.getItems().add(createItem("id2", "Duplicate Title", feed));
        FeedDatabaseWriter.updateFeed(context, feed, false); // First update just takes the feed without complaining
        FeedDatabaseWriter.updateFeed(context, feed, false);

        List<DownloadResult> downloadLog = DBReader.getDownloadLog();
        assertEquals(1, downloadLog.size());
        assertEquals(DownloadError.ERROR_PARSER_EXCEPTION_DUPLICATE, downloadLog.get(0).getReason());
    }

    @Test
    public void testGuidUpdated() {
        Feed feed = createFeed();
        feed.getItems().add(createItem("old-id", "Unique Title", feed));
        FeedDatabaseWriter.updateFeed(context, feed, false);

        Feed newFeed = createFeed();
        newFeed.getItems().add(createItem("new-id", "Unique Title", newFeed));
        Feed stored = FeedDatabaseWriter.updateFeed(context, newFeed, false);

        assertEquals(1, stored.getItems().size());
        assertEquals("new-id", stored.getItems().get(0).getItemIdentifier());
    }

    @Test
    public void testUpdateFeedNewFeed() {
        final int numItems = 10;

        Feed feed = createFeed();
        for (int i = 0; i < numItems; i++) {
            feed.getItems().add(new FeedItem(0, "item " + i, "id " + i, "link " + i,
                    new Date(), FeedItem.UNPLAYED, feed));
        }
        Feed newFeed = FeedDatabaseWriter.updateFeed(context, feed, false);

        assertEquals(feed.getId(), newFeed.getId());
        assertTrue(feed.getId() != 0);
        for (FeedItem item : feed.getItems()) {
            assertFalse(item.isPlayed());
            assertTrue(item.getId() != 0);
        }
    }

    /** Two feeds with the same title, but different download URLs should be treated as different feeds. */
    @Test
    public void testUpdateFeedSameTitle() {
        Feed feed1 = createFeed();
        Feed feed2 = createFeed();
        feed2.setDownloadUrl("different url");

        Feed savedFeed1 = FeedDatabaseWriter.updateFeed(context, feed1, false);
        Feed savedFeed2 = FeedDatabaseWriter.updateFeed(context, feed2, false);

        assertTrue(savedFeed1.getId() != savedFeed2.getId());
    }

    @Test
    public void testUpdateFeedUpdatedFeed() {
        final int numItemsOld = 10;
        final int numItemsNew = 10;

        final Feed feed = createFeed();
        for (int i = 0; i < numItemsOld; i++) {
            feed.getItems().add(new FeedItem(0, "item " + i, "id " + i, "link " + i,
                    new Date(i), FeedItem.PLAYED, feed));
        }
        PodDBAdapter adapter = PodDBAdapter.getInstance();
        adapter.open();
        adapter.setCompleteFeed(feed);
        adapter.close();

        // ensure that objects have been saved in db, then reset
        assertTrue(feed.getId() != 0);
        final long feedID = feed.getId();
        feed.setId(0);
        List<Long> itemIDs = new ArrayList<>();
        for (FeedItem item : feed.getItems()) {
            assertTrue(item.getId() != 0);
            itemIDs.add(item.getId());
            item.setId(0);
        }

        for (int i = numItemsOld; i < numItemsNew + numItemsOld; i++) {
            feed.getItems().add(0, new FeedItem(0, "item " + i, "id " + i, "link " + i,
                    new Date(i), FeedItem.UNPLAYED, feed));
        }

        final Feed newFeed = FeedDatabaseWriter.updateFeed(context, feed, false);
        assertNotSame(newFeed, feed);

        updatedFeedTest(newFeed, feedID, itemIDs, numItemsOld, numItemsNew);

        final Feed feedFromDB = DBReader.getFeed(newFeed.getId(), false, 0, Integer.MAX_VALUE);
        assertNotNull(feedFromDB);
        assertEquals(newFeed.getId(), feedFromDB.getId());
        updatedFeedTest(feedFromDB, feedID, itemIDs, numItemsOld, numItemsNew);
    }

    @Test
    public void testUpdateFeedMediaUrlResetState() {
        final Feed feed = createFeed();
        FeedItem item = new FeedItem(0, "item", "id", "link", new Date(), FeedItem.PLAYED, feed);
        feed.setItems(Collections.singletonList(item));

        PodDBAdapter adapter = PodDBAdapter.getInstance();
        adapter.open();
        adapter.setCompleteFeed(feed);
        adapter.close();

        // ensure that objects have been saved in db, then reset
        assertTrue(feed.getId() != 0);
        assertTrue(item.getId() != 0);

        FeedMedia media = new FeedMedia(item, "url", 1024, "mime/type");
        item.setMedia(media);
        List<FeedItem> list = new ArrayList<>();
        list.add(item);
        feed.setItems(list);

        final Feed newFeed = FeedDatabaseWriter.updateFeed(context, feed, false);
        assertNotSame(newFeed, feed);

        final Feed feedFromDB = DBReader.getFeed(newFeed.getId(), false, 0, Integer.MAX_VALUE);
        final FeedItem feedItemFromDB = feedFromDB.getItems().get(0);
        assertTrue(feedItemFromDB.isNew());
    }

    @Test
    public void testUpdateFeedRemoveUnlistedItems() {
        final Feed feed = createFeed();
        for (int i = 0; i < 10; i++) {
            feed.getItems().add(
                    new FeedItem(0, "item " + i, "id " + i, "link " + i, new Date(i), FeedItem.PLAYED, feed));
        }
        PodDBAdapter adapter = PodDBAdapter.getInstance();
        adapter.open();
        adapter.setCompleteFeed(feed);
        adapter.close();

        // delete some items
        feed.getItems().subList(0, 2).clear();
        Feed newFeed = FeedDatabaseWriter.updateFeed(context, feed, true);
        assertEquals(8, newFeed.getItems().size()); // 10 - 2 = 8 items

        Feed feedFromDB = DBReader.getFeed(newFeed.getId(), false, 0, Integer.MAX_VALUE);
        assertEquals(8, feedFromDB.getItems().size()); // 10 - 2 = 8 items
    }

    @Test
    public void testUpdateFeedSetDuplicate() {
        final Feed feed = createFeed();
        for (int i = 0; i < 10; i++) {
            FeedItem item =
                    new FeedItem(0, "item " + i, "id " + i, "link " + i, new Date(i), FeedItem.PLAYED, feed);
            FeedMedia media = new FeedMedia(item, "download url " + i, 123, "media/mp3");
            item.setMedia(media);
            feed.getItems().add(item);
        }
        PodDBAdapter adapter = PodDBAdapter.getInstance();
        adapter.open();
        adapter.setCompleteFeed(feed);
        adapter.close();

        // change the guid of the first item, but leave the download url the same
        FeedItem item = feed.getItemAtIndex(0);
        item.setItemIdentifier("id 0-duplicate");
        item.setTitle("item 0 duplicate");
        Feed newFeed = FeedDatabaseWriter.updateFeed(context, feed, false);
        assertEquals(10, newFeed.getItems().size()); // id 1-duplicate replaces because the stream url is the same

        Feed feedFromDB = DBReader.getFeed(newFeed.getId(), false, 0, Integer.MAX_VALUE);
        assertEquals(10, feedFromDB.getItems().size()); // id1-duplicate should override id 1

        FeedItem updatedItem = feedFromDB.getItemAtIndex(9);
        assertEquals("item 0 duplicate", updatedItem.getTitle());
        assertEquals("id 0-duplicate", updatedItem.getItemIdentifier()); // Should use the new ID for sync etc
    }


    @SuppressWarnings("SameParameterValue")
    private void updatedFeedTest(final Feed newFeed, long feedID, List<Long> itemIDs,
                                 int numItemsOld, int numItemsNew) {
        assertEquals(feedID, newFeed.getId());
        assertEquals(numItemsNew + numItemsOld, newFeed.getItems().size());
        Collections.reverse(newFeed.getItems());
        Date lastDate = new Date(0);
        for (int i = 0; i < numItemsOld; i++) {
            FeedItem item = newFeed.getItems().get(i);
            assertSame(newFeed, item.getFeed());
            assertEquals((long) itemIDs.get(i), item.getId());
            assertTrue(item.isPlayed());
            assertTrue(item.getPubDate().getTime() >= lastDate.getTime());
            lastDate = item.getPubDate();
        }
        for (int i = numItemsOld; i < numItemsNew + numItemsOld; i++) {
            FeedItem item = newFeed.getItems().get(i);
            assertSame(newFeed, item.getFeed());
            assertTrue(item.getId() != 0);
            assertFalse(item.isPlayed());
            assertTrue(item.getPubDate().getTime() >= lastDate.getTime());
            lastDate = item.getPubDate();
        }
    }

    private Feed createFeed() {
        Feed feed = new Feed("url", null, null);
        feed.setItems(new ArrayList<>());
        return feed;
    }

    private FeedItem createItem(String identifier, String title, Feed feed) {
        FeedItem item = new FeedItem();
        item.setItemIdentifier(identifier);
        item.setTitle(title);
        item.setMedia(new FeedMedia(item, "url-" + title, 2, "mime"));
        item.setFeed(feed);
        return item;
    }
}
