package de.test.antennapod.service.playback;

import android.content.Context;

import androidx.test.annotation.UiThreadTest;
import androidx.test.filters.MediumTest;

import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting;
import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
import de.danoeh.antennapod.playback.base.PlayerStatus;
import de.danoeh.antennapod.playback.service.internal.LocalPSMP;
import de.danoeh.antennapod.storage.database.PodDBAdapter;
import de.test.antennapod.EspressoTestUtils;
import junit.framework.AssertionFailedError;

import org.apache.commons.io.IOUtils;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.model.feed.FeedPreferences;
import de.danoeh.antennapod.model.playback.Playable;
import de.test.antennapod.util.service.download.HTTPBin;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

/**
 * Test class for LocalPSMP
 */
@MediumTest
public class PlaybackServiceMediaPlayerTest {
    private static final String PLAYABLE_DEST_URL = "psmptestfile.mp3";
    private String PLAYABLE_LOCAL_URL = null;
    private static final int LATCH_TIMEOUT_SECONDS = 3;

    private HTTPBin httpServer;
    private String playableFileUrl;
    private volatile AssertionFailedError assertionError;

    @After
    @UiThreadTest
    public void tearDown() throws Exception {
        PodDBAdapter.deleteDatabase();
        httpServer.stop();
    }

    @Before
    @UiThreadTest
    public void setUp() throws Exception {
        assertionError = null;
        EspressoTestUtils.clearPreferences();
        EspressoTestUtils.clearDatabase();

        final Context context = getInstrumentation().getTargetContext();

        httpServer = new HTTPBin();
        httpServer.start();
        playableFileUrl = httpServer.getBaseUrl() + "/files/0";

        File cacheDir = context.getExternalFilesDir("testFiles");
        if (cacheDir == null)
            cacheDir = context.getExternalFilesDir("testFiles");
        File dest = new File(cacheDir, PLAYABLE_DEST_URL);

        assertNotNull(cacheDir);
        assertTrue(cacheDir.canWrite());
        assertTrue(cacheDir.canRead());
        if (!dest.exists()) {
            InputStream i = getInstrumentation().getContext().getAssets().open("3sec.mp3");
            OutputStream o = new FileOutputStream(new File(cacheDir, PLAYABLE_DEST_URL));
            IOUtils.copy(i, o);
            o.flush();
            o.close();
            i.close();
        }
        PLAYABLE_LOCAL_URL = dest.getAbsolutePath();
        assertEquals(0, httpServer.serveFile(dest));
    }

    private void checkPSMPInfo(LocalPSMP.PSMPInfo info) {
        try {
            switch (info.getPlayerStatus()) {
                case PLAYING:
                case PAUSED:
                case PREPARED:
                case PREPARING:
                case INITIALIZED:
                case INITIALIZING:
                case SEEKING:
                    assertNotNull(info.getPlayable());
                    break;
                case STOPPED:
                case ERROR:
                    assertNull(info.getPlayable());
                    break;
                default:
                    break;
            }
        } catch (AssertionFailedError e) {
            if (assertionError == null)
                assertionError = e;
        }
    }

    @Test
    @UiThreadTest
    public void testInit() {
        final Context c = getInstrumentation().getTargetContext();
        PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, new DefaultPSMPCallback());
        psmp.shutdown();
    }

    private Playable writeTestPlayable(String downloadUrl, String fileUrl) {
        Feed f = new Feed(0, null, "f", "l", "d", null, null, null, null,
                "i", null, null, "l", System.currentTimeMillis());
        FeedPreferences prefs = new FeedPreferences(f.getId(), false, FeedPreferences.AutoDeleteAction.NEVER,
                VolumeAdaptionSetting.OFF, FeedPreferences.NewEpisodesAction.NOTHING, null, null);
        f.setPreferences(prefs);
        f.setItems(new ArrayList<>());
        FeedItem i = new FeedItem(0, "t", "i", "l", new Date(), FeedItem.UNPLAYED, f);
        f.getItems().add(i);
        FeedMedia media = new FeedMedia(0, i, 0, 0, 0, "audio/wav", fileUrl, downloadUrl,
                fileUrl == null ? 0 : System.currentTimeMillis(), null, 0, 0);
        i.setMedia(media);
        PodDBAdapter adapter = PodDBAdapter.getInstance();
        adapter.open();
        adapter.setCompleteFeed(f);
        assertTrue(media.getId() != 0);
        adapter.close();
        return media;
    }

    @Test
    @UiThreadTest
    public void testPlayMediaObjectStreamNoStartNoPrepare() throws InterruptedException {
        final Context c = getInstrumentation().getTargetContext();
        final CountDownLatch countDownLatch = new CountDownLatch(2);
        CancelablePSMPCallback callback = new CancelablePSMPCallback(new DefaultPSMPCallback() {
            @Override
            public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
                try {
                    checkPSMPInfo(newInfo);
                    if (newInfo.getPlayerStatus() == PlayerStatus.ERROR) {
                        throw new IllegalStateException("MediaPlayer error");
                    }
                    if (countDownLatch.getCount() == 0) {
                        fail();
                    } else if (countDownLatch.getCount() == 2) {
                        assertEquals(PlayerStatus.INITIALIZING, newInfo.getPlayerStatus());
                        countDownLatch.countDown();
                    } else {
                        assertEquals(PlayerStatus.INITIALIZED, newInfo.getPlayerStatus());
                        countDownLatch.countDown();
                    }
                } catch (AssertionFailedError e) {
                    if (assertionError == null)
                        assertionError = e;
                }
            }
        });
        PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
        Playable p = writeTestPlayable(playableFileUrl, null);
        psmp.playMediaObject(p, true, false, false);
        boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS);
        if (assertionError != null)
            throw assertionError;
        assertTrue(res);

        assertSame(PlayerStatus.INITIALIZED, psmp.getPSMPInfo().getPlayerStatus());
        assertFalse(psmp.isStartWhenPrepared());
        callback.cancel();
        psmp.shutdown();
    }

    @Test
    @UiThreadTest
    public void testPlayMediaObjectStreamStartNoPrepare() throws InterruptedException {
        final Context c = getInstrumentation().getTargetContext();
        final CountDownLatch countDownLatch = new CountDownLatch(2);
        CancelablePSMPCallback callback = new CancelablePSMPCallback(new DefaultPSMPCallback() {
            @Override
            public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
                try {
                    checkPSMPInfo(newInfo);
                    if (newInfo.getPlayerStatus() == PlayerStatus.ERROR) {
                        throw new IllegalStateException("MediaPlayer error");
                    }
                    if (countDownLatch.getCount() == 0) {
                        fail();
                    } else if (countDownLatch.getCount() == 2) {
                        assertEquals(PlayerStatus.INITIALIZING, newInfo.getPlayerStatus());
                        countDownLatch.countDown();
                    } else {
                        assertEquals(PlayerStatus.INITIALIZED, newInfo.getPlayerStatus());
                        countDownLatch.countDown();
                    }
                } catch (AssertionFailedError e) {
                    if (assertionError == null)
                        assertionError = e;
                }
            }
        });
        PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
        Playable p = writeTestPlayable(playableFileUrl, null);
        psmp.playMediaObject(p, true, true, false);

        boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS);
        if (assertionError != null)
            throw assertionError;
        assertTrue(res);

        assertSame(PlayerStatus.INITIALIZED, psmp.getPSMPInfo().getPlayerStatus());
        assertTrue(psmp.isStartWhenPrepared());
        callback.cancel();
        psmp.shutdown();
    }

    @Test
    @UiThreadTest
    public void testPlayMediaObjectStreamNoStartPrepare() throws InterruptedException {
        final Context c = getInstrumentation().getTargetContext();
        final CountDownLatch countDownLatch = new CountDownLatch(4);
        CancelablePSMPCallback callback = new CancelablePSMPCallback(new DefaultPSMPCallback() {
            @Override
            public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
                try {
                    checkPSMPInfo(newInfo);
                    if (newInfo.getPlayerStatus() == PlayerStatus.ERROR) {
                        throw new IllegalStateException("MediaPlayer error");
                    }
                    if (countDownLatch.getCount() == 0) {
                        fail();
                    } else if (countDownLatch.getCount() == 4) {
                        assertEquals(PlayerStatus.INITIALIZING, newInfo.getPlayerStatus());
                    } else if (countDownLatch.getCount() == 3) {
                        assertEquals(PlayerStatus.INITIALIZED, newInfo.getPlayerStatus());
                    } else if (countDownLatch.getCount() == 2) {
                        assertEquals(PlayerStatus.PREPARING, newInfo.getPlayerStatus());
                    } else if (countDownLatch.getCount() == 1) {
                        assertEquals(PlayerStatus.PREPARED, newInfo.getPlayerStatus());
                    }
                    countDownLatch.countDown();
                } catch (AssertionFailedError e) {
                    if (assertionError == null)
                        assertionError = e;
                }
            }
        });
        PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
        Playable p = writeTestPlayable(playableFileUrl, null);
        psmp.playMediaObject(p, true, false, true);
        boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS);
        if (assertionError != null)
            throw assertionError;
        assertTrue(res);
        assertSame(PlayerStatus.PREPARED, psmp.getPSMPInfo().getPlayerStatus());
        callback.cancel();

        psmp.shutdown();
    }

    @Test
    @UiThreadTest
    public void testPlayMediaObjectStreamStartPrepare() throws InterruptedException {
        final Context c = getInstrumentation().getTargetContext();
        final CountDownLatch countDownLatch = new CountDownLatch(5);
        CancelablePSMPCallback callback = new CancelablePSMPCallback(new DefaultPSMPCallback() {
            @Override
            public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
                try {
                    checkPSMPInfo(newInfo);
                    if (newInfo.getPlayerStatus() == PlayerStatus.ERROR) {
                        throw new IllegalStateException("MediaPlayer error");
                    }
                    if (countDownLatch.getCount() == 0) {
                        fail();
                    } else if (countDownLatch.getCount() == 5) {
                        assertEquals(PlayerStatus.INITIALIZING, newInfo.getPlayerStatus());
                    } else if (countDownLatch.getCount() == 4) {
                        assertEquals(PlayerStatus.INITIALIZED, newInfo.getPlayerStatus());
                    } else if (countDownLatch.getCount() == 3) {
                        assertEquals(PlayerStatus.PREPARING, newInfo.getPlayerStatus());
                    } else if (countDownLatch.getCount() == 2) {
                        assertEquals(PlayerStatus.PREPARED, newInfo.getPlayerStatus());
                    } else if (countDownLatch.getCount() == 1) {
                        assertEquals(PlayerStatus.PLAYING, newInfo.getPlayerStatus());
                    }
                    countDownLatch.countDown();
                } catch (AssertionFailedError e) {
                    if (assertionError == null)
                        assertionError = e;
                }
            }
        });
        PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
        Playable p = writeTestPlayable(playableFileUrl, null);
        psmp.playMediaObject(p, true, true, true);
        boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS);
        if (assertionError != null)
            throw assertionError;
        assertTrue(res);
        assertSame(PlayerStatus.PLAYING, psmp.getPSMPInfo().getPlayerStatus());
        callback.cancel();
        psmp.shutdown();
    }

    @Test
    @UiThreadTest
    public void testPlayMediaObjectLocalNoStartNoPrepare() throws InterruptedException {
        final Context c = getInstrumentation().getTargetContext();
        final CountDownLatch countDownLatch = new CountDownLatch(2);
        CancelablePSMPCallback callback = new CancelablePSMPCallback(new DefaultPSMPCallback() {
            @Override
            public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
                try {
                    checkPSMPInfo(newInfo);
                    if (newInfo.getPlayerStatus() == PlayerStatus.ERROR) {
                        throw new IllegalStateException("MediaPlayer error");
                    }
                    if (countDownLatch.getCount() == 0) {
                        fail();
                    } else if (countDownLatch.getCount() == 2) {
                        assertEquals(PlayerStatus.INITIALIZING, newInfo.getPlayerStatus());
                        countDownLatch.countDown();
                    } else {
                        assertEquals(PlayerStatus.INITIALIZED, newInfo.getPlayerStatus());
                        countDownLatch.countDown();
                    }
                } catch (AssertionFailedError e) {
                    if (assertionError == null)
                        assertionError = e;
                }
            }
        });
        PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
        Playable p = writeTestPlayable(playableFileUrl, PLAYABLE_LOCAL_URL);
        psmp.playMediaObject(p, false, false, false);
        boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS);
        if (assertionError != null)
            throw assertionError;
        assertTrue(res);
        assertSame(PlayerStatus.INITIALIZED, psmp.getPSMPInfo().getPlayerStatus());
        assertFalse(psmp.isStartWhenPrepared());
        callback.cancel();
        psmp.shutdown();
    }

    @Test
    @UiThreadTest
    public void testPlayMediaObjectLocalStartNoPrepare() throws InterruptedException {
        final Context c = getInstrumentation().getTargetContext();
        final CountDownLatch countDownLatch = new CountDownLatch(2);
        CancelablePSMPCallback callback = new CancelablePSMPCallback(new DefaultPSMPCallback() {
            @Override
            public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
                try {
                    checkPSMPInfo(newInfo);
                    if (newInfo.getPlayerStatus() == PlayerStatus.ERROR) {
                        throw new IllegalStateException("MediaPlayer error");
                    }
                    if (countDownLatch.getCount() == 0) {
                        fail();
                    } else if (countDownLatch.getCount() == 2) {
                        assertEquals(PlayerStatus.INITIALIZING, newInfo.getPlayerStatus());
                        countDownLatch.countDown();
                    } else {
                        assertEquals(PlayerStatus.INITIALIZED, newInfo.getPlayerStatus());
                        countDownLatch.countDown();
                    }
                } catch (AssertionFailedError e) {
                    if (assertionError == null)
                        assertionError = e;
                }
            }
        });
        PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
        Playable p = writeTestPlayable(playableFileUrl, PLAYABLE_LOCAL_URL);
        psmp.playMediaObject(p, false, true, false);
        boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS);
        if (assertionError != null)
            throw assertionError;
        assertTrue(res);
        assertSame(PlayerStatus.INITIALIZED, psmp.getPSMPInfo().getPlayerStatus());
        assertTrue(psmp.isStartWhenPrepared());
        callback.cancel();
        psmp.shutdown();
    }

    @Test
    @UiThreadTest
    public void testPlayMediaObjectLocalNoStartPrepare() throws InterruptedException {
        final Context c = getInstrumentation().getTargetContext();
        final CountDownLatch countDownLatch = new CountDownLatch(4);
        CancelablePSMPCallback callback = new CancelablePSMPCallback(new DefaultPSMPCallback() {
            @Override
            public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
                try {
                    checkPSMPInfo(newInfo);
                    if (newInfo.getPlayerStatus() == PlayerStatus.ERROR) {
                        throw new IllegalStateException("MediaPlayer error");
                    }
                    if (countDownLatch.getCount() == 0) {
                        fail();
                    } else if (countDownLatch.getCount() == 4) {
                        assertEquals(PlayerStatus.INITIALIZING, newInfo.getPlayerStatus());
                    } else if (countDownLatch.getCount() == 3) {
                        assertEquals(PlayerStatus.INITIALIZED, newInfo.getPlayerStatus());
                    } else if (countDownLatch.getCount() == 2) {
                        assertEquals(PlayerStatus.PREPARING, newInfo.getPlayerStatus());
                    } else if (countDownLatch.getCount() == 1) {
                        assertEquals(PlayerStatus.PREPARED, newInfo.getPlayerStatus());
                    }
                    countDownLatch.countDown();
                } catch (AssertionFailedError e) {
                    if (assertionError == null)
                        assertionError = e;
                }
            }
        });
        PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
        Playable p = writeTestPlayable(playableFileUrl, PLAYABLE_LOCAL_URL);
        psmp.playMediaObject(p, false, false, true);
        boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS);
        if (assertionError != null)
            throw assertionError;
        assertTrue(res);
        assertSame(PlayerStatus.PREPARED, psmp.getPSMPInfo().getPlayerStatus());
        callback.cancel();
        psmp.shutdown();
    }

    @Test
    @UiThreadTest
    public void testPlayMediaObjectLocalStartPrepare() throws InterruptedException {
        final Context c = getInstrumentation().getTargetContext();
        final CountDownLatch countDownLatch = new CountDownLatch(5);
        CancelablePSMPCallback callback = new CancelablePSMPCallback(new DefaultPSMPCallback() {
            @Override
            public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
                try {
                    checkPSMPInfo(newInfo);
                    if (newInfo.getPlayerStatus() == PlayerStatus.ERROR) {
                        throw new IllegalStateException("MediaPlayer error");
                    }
                    if (countDownLatch.getCount() == 0) {
                        fail();
                    } else if (countDownLatch.getCount() == 5) {
                        assertEquals(PlayerStatus.INITIALIZING, newInfo.getPlayerStatus());
                    } else if (countDownLatch.getCount() == 4) {
                        assertEquals(PlayerStatus.INITIALIZED, newInfo.getPlayerStatus());
                    } else if (countDownLatch.getCount() == 3) {
                        assertEquals(PlayerStatus.PREPARING, newInfo.getPlayerStatus());
                    } else if (countDownLatch.getCount() == 2) {
                        assertEquals(PlayerStatus.PREPARED, newInfo.getPlayerStatus());
                    } else if (countDownLatch.getCount() == 1) {
                        assertEquals(PlayerStatus.PLAYING, newInfo.getPlayerStatus());
                    }

                } catch (AssertionFailedError e) {
                    if (assertionError == null)
                        assertionError = e;
                } finally {
                    countDownLatch.countDown();
                }
            }
        });
        PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
        Playable p = writeTestPlayable(playableFileUrl, PLAYABLE_LOCAL_URL);
        psmp.playMediaObject(p, false, true, true);
        boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS);
        if (assertionError != null)
            throw assertionError;
        assertTrue(res);
        assertSame(PlayerStatus.PLAYING, psmp.getPSMPInfo().getPlayerStatus());
        callback.cancel();
        psmp.shutdown();
    }

    private void pauseTestSkeleton(final PlayerStatus initialState, final boolean stream, final boolean abandonAudioFocus, final boolean reinit, long timeoutSeconds) throws InterruptedException {
        final Context c = getInstrumentation().getTargetContext();
        final int latchCount = (stream && reinit) ? 2 : 1;
        final CountDownLatch countDownLatch = new CountDownLatch(latchCount);

        CancelablePSMPCallback callback = new CancelablePSMPCallback(new DefaultPSMPCallback() {
            @Override
            public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
                checkPSMPInfo(newInfo);
                if (newInfo.getPlayerStatus() == PlayerStatus.ERROR) {
                    if (assertionError == null) {
                        assertionError = new UnexpectedStateChange(newInfo.getPlayerStatus());
                    }
                } else if (initialState != PlayerStatus.PLAYING) {
                    if (assertionError == null) {
                        assertionError = new UnexpectedStateChange(newInfo.getPlayerStatus());
                    }
                } else {
                    switch (newInfo.getPlayerStatus()) {
                        case PAUSED:
                            if (latchCount == countDownLatch.getCount())
                                countDownLatch.countDown();
                            else {
                                if (assertionError == null) {
                                    assertionError = new UnexpectedStateChange(newInfo.getPlayerStatus());
                                }
                            }
                            break;
                        case INITIALIZED:
                            if (stream && reinit && countDownLatch.getCount() < latchCount) {
                                countDownLatch.countDown();
                            } else if (countDownLatch.getCount() < latchCount) {
                                if (assertionError == null)
                                    assertionError = new UnexpectedStateChange(newInfo.getPlayerStatus());
                            }
                            break;
                        default:
                            break;
                    }
                }

            }

            @Override
            public void shouldStop() {
                if (assertionError == null)
                    assertionError = new AssertionFailedError("Unexpected call to shouldStop");
            }
        });
        PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
        Playable p = writeTestPlayable(playableFileUrl, PLAYABLE_LOCAL_URL);
        if (initialState == PlayerStatus.PLAYING) {
            psmp.playMediaObject(p, stream, true, true);
        }
        psmp.pause(abandonAudioFocus, reinit);
        boolean res = countDownLatch.await(timeoutSeconds, TimeUnit.SECONDS);
        if (assertionError != null)
            throw assertionError;
        assertTrue(res || initialState != PlayerStatus.PLAYING);
        callback.cancel();
        psmp.shutdown();
    }

    @Test
    @UiThreadTest
    public void testPauseDefaultState() throws InterruptedException {
        pauseTestSkeleton(PlayerStatus.STOPPED, false, false, false, 1);
    }

    @Test
    @UiThreadTest
    public void testPausePlayingStateNoAbandonNoReinitNoStream() throws InterruptedException {
        pauseTestSkeleton(PlayerStatus.PLAYING, false, false, false, LATCH_TIMEOUT_SECONDS);
    }

    @Test
    @UiThreadTest
    public void testPausePlayingStateNoAbandonNoReinitStream() throws InterruptedException {
        pauseTestSkeleton(PlayerStatus.PLAYING, true, false, false, LATCH_TIMEOUT_SECONDS);
    }

    @Test
    @UiThreadTest
    public void testPausePlayingStateAbandonNoReinitNoStream() throws InterruptedException {
        pauseTestSkeleton(PlayerStatus.PLAYING, false, true, false, LATCH_TIMEOUT_SECONDS);
    }

    @Test
    @UiThreadTest
    public void testPausePlayingStateAbandonNoReinitStream() throws InterruptedException {
        pauseTestSkeleton(PlayerStatus.PLAYING, true, true, false, LATCH_TIMEOUT_SECONDS);
    }

    @Test
    @UiThreadTest
    public void testPausePlayingStateNoAbandonReinitNoStream() throws InterruptedException {
        pauseTestSkeleton(PlayerStatus.PLAYING, false, false, true, LATCH_TIMEOUT_SECONDS);
    }

    @Test
    @UiThreadTest
    public void testPausePlayingStateNoAbandonReinitStream() throws InterruptedException {
        pauseTestSkeleton(PlayerStatus.PLAYING, true, false, true, LATCH_TIMEOUT_SECONDS);
    }

    @Test
    @UiThreadTest
    public void testPausePlayingStateAbandonReinitNoStream() throws InterruptedException {
        pauseTestSkeleton(PlayerStatus.PLAYING, false, true, true, LATCH_TIMEOUT_SECONDS);
    }

    @Test
    @UiThreadTest
    public void testPausePlayingStateAbandonReinitStream() throws InterruptedException {
        pauseTestSkeleton(PlayerStatus.PLAYING, true, true, true, LATCH_TIMEOUT_SECONDS);
    }

    private void resumeTestSkeleton(final PlayerStatus initialState, long timeoutSeconds) throws InterruptedException {
        final Context c = getInstrumentation().getTargetContext();
        final int latchCount = (initialState == PlayerStatus.PAUSED || initialState == PlayerStatus.PLAYING) ? 2 :
                (initialState == PlayerStatus.PREPARED) ? 1 : 0;
        final CountDownLatch countDownLatch = new CountDownLatch(latchCount);

        CancelablePSMPCallback callback = new CancelablePSMPCallback(new DefaultPSMPCallback() {
            @Override
            public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
                checkPSMPInfo(newInfo);
                if (newInfo.getPlayerStatus() == PlayerStatus.ERROR) {
                    if (assertionError == null) {
                        assertionError = new UnexpectedStateChange(newInfo.getPlayerStatus());
                    }
                } else if (newInfo.getPlayerStatus() == PlayerStatus.PLAYING) {
                    if (countDownLatch.getCount() == 0) {
                        if (assertionError == null) {
                            assertionError = new UnexpectedStateChange(newInfo.getPlayerStatus());
                        }
                    } else {
                        countDownLatch.countDown();
                    }
                }

            }
        });
        PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
        if (initialState == PlayerStatus.PREPARED || initialState == PlayerStatus.PLAYING || initialState == PlayerStatus.PAUSED) {
            boolean startWhenPrepared = (initialState != PlayerStatus.PREPARED);
            psmp.playMediaObject(writeTestPlayable(playableFileUrl, PLAYABLE_LOCAL_URL), false, startWhenPrepared, true);
        }
        if (initialState == PlayerStatus.PAUSED) {
            psmp.pause(false, false);
        }
        psmp.resume();
        boolean res = countDownLatch.await(timeoutSeconds, TimeUnit.SECONDS);
        if (assertionError != null)
            throw assertionError;
        assertTrue(res || (initialState != PlayerStatus.PAUSED && initialState != PlayerStatus.PREPARED));
        callback.cancel();
        psmp.shutdown();
    }

    @Test
    @UiThreadTest
    public void testResumePausedState() throws InterruptedException {
        resumeTestSkeleton(PlayerStatus.PAUSED, LATCH_TIMEOUT_SECONDS);
    }

    @Test
    @UiThreadTest
    public void testResumePreparedState() throws InterruptedException {
        resumeTestSkeleton(PlayerStatus.PREPARED, LATCH_TIMEOUT_SECONDS);
    }

    @Test
    @UiThreadTest
    public void testResumePlayingState() throws InterruptedException {
        resumeTestSkeleton(PlayerStatus.PLAYING, 1);
    }

    private void prepareTestSkeleton(final PlayerStatus initialState, long timeoutSeconds) throws InterruptedException {
        final Context c = getInstrumentation().getTargetContext();
        final int latchCount = 1;
        final CountDownLatch countDownLatch = new CountDownLatch(latchCount);
        CancelablePSMPCallback callback = new CancelablePSMPCallback(new DefaultPSMPCallback() {
            @Override
            public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
                checkPSMPInfo(newInfo);
                if (newInfo.getPlayerStatus() == PlayerStatus.ERROR) {
                    if (assertionError == null) {
                        assertionError = new UnexpectedStateChange(newInfo.getPlayerStatus());
                    }
                } else {
                    if (initialState == PlayerStatus.INITIALIZED && newInfo.getPlayerStatus()
                            == PlayerStatus.PREPARED) {
                        countDownLatch.countDown();
                    } else if (initialState != PlayerStatus.INITIALIZED && initialState == newInfo.getPlayerStatus()) {
                        countDownLatch.countDown();
                    }
                }
            }
        });
        PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
        Playable p = writeTestPlayable(playableFileUrl, PLAYABLE_LOCAL_URL);
        if (initialState == PlayerStatus.INITIALIZED
                || initialState == PlayerStatus.PLAYING
                || initialState == PlayerStatus.PREPARED
                || initialState == PlayerStatus.PAUSED) {
            boolean prepareImmediately = (initialState != PlayerStatus.INITIALIZED);
            boolean startWhenPrepared = (initialState != PlayerStatus.PREPARED);
            psmp.playMediaObject(p, false, startWhenPrepared, prepareImmediately);
            if (initialState == PlayerStatus.PAUSED) {
                psmp.pause(false, false);
            }
            psmp.prepare();
        }

        boolean res = countDownLatch.await(timeoutSeconds, TimeUnit.SECONDS);
        if (initialState != PlayerStatus.INITIALIZED) {
            assertEquals(initialState, psmp.getPSMPInfo().getPlayerStatus());
        }

        if (assertionError != null)
            throw assertionError;
        assertTrue(res);
        callback.cancel();
        psmp.shutdown();
    }

    @Test
    @UiThreadTest
    public void testPrepareInitializedState() throws InterruptedException {
        prepareTestSkeleton(PlayerStatus.INITIALIZED, LATCH_TIMEOUT_SECONDS);
    }

    @Test
    @UiThreadTest
    public void testPreparePlayingState() throws InterruptedException {
        prepareTestSkeleton(PlayerStatus.PLAYING, 1);
    }

    @Test
    @UiThreadTest
    public void testPreparePausedState() throws InterruptedException {
        prepareTestSkeleton(PlayerStatus.PAUSED, 1);
    }

    @Test
    @UiThreadTest
    public void testPreparePreparedState() throws InterruptedException {
        prepareTestSkeleton(PlayerStatus.PREPARED, 1);
    }

    private void reinitTestSkeleton(final PlayerStatus initialState, final long timeoutSeconds) throws InterruptedException {
        final Context c = getInstrumentation().getTargetContext();
        final int latchCount = 2;
        final CountDownLatch countDownLatch = new CountDownLatch(latchCount);
        CancelablePSMPCallback callback = new CancelablePSMPCallback(new DefaultPSMPCallback() {
            @Override
            public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
                checkPSMPInfo(newInfo);
                if (newInfo.getPlayerStatus() == PlayerStatus.ERROR) {
                    if (assertionError == null) {
                        assertionError = new UnexpectedStateChange(newInfo.getPlayerStatus());
                    }
                } else {
                    if (newInfo.getPlayerStatus() == initialState) {
                        countDownLatch.countDown();
                    } else if (countDownLatch.getCount() < latchCount && newInfo.getPlayerStatus()
                            == PlayerStatus.INITIALIZED) {
                        countDownLatch.countDown();
                    }
                }
            }
        });
        PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
        Playable p = writeTestPlayable(playableFileUrl, PLAYABLE_LOCAL_URL);
        boolean prepareImmediately = initialState != PlayerStatus.INITIALIZED;
        boolean startImmediately = initialState != PlayerStatus.PREPARED;
        psmp.playMediaObject(p, false, startImmediately, prepareImmediately);
        if (initialState == PlayerStatus.PAUSED) {
            psmp.pause(false, false);
        }
        psmp.reinit();
        boolean res = countDownLatch.await(timeoutSeconds, TimeUnit.SECONDS);
        if (assertionError != null)
            throw assertionError;
        assertTrue(res);
        callback.cancel();
        psmp.shutdown();
    }

    @Test
    @UiThreadTest
    public void testReinitPlayingState() throws InterruptedException {
        reinitTestSkeleton(PlayerStatus.PLAYING, LATCH_TIMEOUT_SECONDS);
    }

    @Test
    @UiThreadTest
    public void testReinitPausedState() throws InterruptedException {
        reinitTestSkeleton(PlayerStatus.PAUSED, LATCH_TIMEOUT_SECONDS);
    }

    @Test
    @UiThreadTest
    public void testPreparedPlayingState() throws InterruptedException {
        reinitTestSkeleton(PlayerStatus.PREPARED, LATCH_TIMEOUT_SECONDS);
    }

    @Test
    @UiThreadTest
    public void testReinitInitializedState() throws InterruptedException {
        reinitTestSkeleton(PlayerStatus.INITIALIZED, LATCH_TIMEOUT_SECONDS);
    }

    private static class UnexpectedStateChange extends AssertionFailedError {
        public UnexpectedStateChange(PlayerStatus status) {
            super("Unexpected state change: " + status);
        }
    }
}
