package org.airsonic.player.service;

import org.airsonic.player.domain.InternetRadio;
import org.airsonic.player.domain.InternetRadioSource;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.*;
import org.mockito.junit.MockitoJUnitRunner;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Date;
import java.util.List;

import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class InternetRadioServiceTest {

    String TEST_RADIO_NAME = "Test Radio";
    String TEST_RADIO_HOMEPAGE = "http://example.com";
    String TEST_PLAYLIST_URL_MOVE = "http://example.com/stream_move.m3u";
    String TEST_PLAYLIST_URL_MOVE_LOOP = "http://example.com/stream_infinity_move.m3u";
    String TEST_PLAYLIST_URL_LARGE = "http://example.com/stream_infinity_repeat.m3u";
    String TEST_PLAYLIST_URL_LARGE_2 = "http://example.com/stream_infinity_big.m3u";
    String TEST_PLAYLIST_URL_1 = "http://example.com/stream1.m3u";
    String TEST_PLAYLIST_URL_2 = "http://example.com/stream2.m3u";
    String TEST_STREAM_URL_1 = "http://example.com/stream1";
    String TEST_STREAM_URL_2 = "http://example.com/stream2";
    String TEST_STREAM_URL_3 = "http://example.com/stream3";
    String TEST_STREAM_URL_4 = "http://example.com/stream4";
    String TEST_STREAM_PLAYLIST_CONTENTS_1 = (
        "http://example.com/stream1\n" +
        "http://example.com/stream2\n"
    );
    String TEST_STREAM_PLAYLIST_CONTENTS_2 = (
        "#EXTM3U\n" +
        "#EXTINF:123, Sample artist - Sample title\n" +
        "http://example.com/stream3\n" +
        "#EXTINF:321,Example Artist - Example title\n" +
        "http://example.com/stream4\n"
    );

    InternetRadio radio1;
    InternetRadio radio2;
    InternetRadio radioMove;
    InternetRadio radioMoveLoop;
    InternetRadio radioLarge;
    InternetRadio radioLarge2;

    @Spy
    InternetRadioService internetRadioService;

    @Before
    public void setup() throws Exception {

        // Prepare a mock InternetRadio object
        radio1 = new InternetRadio(1, TEST_RADIO_NAME, TEST_PLAYLIST_URL_1, TEST_RADIO_HOMEPAGE, true, new Date());
        radio2 = new InternetRadio(2, TEST_RADIO_NAME, TEST_PLAYLIST_URL_2, TEST_RADIO_HOMEPAGE, true, new Date());
        radioMove = new InternetRadio(3, TEST_RADIO_NAME, TEST_PLAYLIST_URL_MOVE, TEST_RADIO_HOMEPAGE, true, new Date());
        radioMoveLoop = new InternetRadio(3, TEST_RADIO_NAME, TEST_PLAYLIST_URL_MOVE_LOOP, TEST_RADIO_HOMEPAGE, true, new Date());
        radioLarge = new InternetRadio(4, TEST_RADIO_NAME, TEST_PLAYLIST_URL_LARGE, TEST_RADIO_HOMEPAGE, true, new Date());
        radioLarge2 = new InternetRadio(5, TEST_RADIO_NAME, TEST_PLAYLIST_URL_LARGE_2, TEST_RADIO_HOMEPAGE, true, new Date());

        // Prepare the mocked URL connection for the simple playlist
        HttpURLConnection mockURLConnection1 = Mockito.mock(HttpURLConnection.class);
        InputStream mockURLInputStream1 = new ByteArrayInputStream(TEST_STREAM_PLAYLIST_CONTENTS_1.getBytes());
        doReturn(mockURLInputStream1).when(mockURLConnection1).getInputStream();
        doReturn(HttpURLConnection.HTTP_OK).when(mockURLConnection1).getResponseCode();

        // Prepare the mocked URL connection for the second simple playlist
        HttpURLConnection mockURLConnection2 = Mockito.mock(HttpURLConnection.class);
        InputStream mockURLInputStream2 = new ByteArrayInputStream(TEST_STREAM_PLAYLIST_CONTENTS_2.getBytes());
        doReturn(mockURLInputStream2).when(mockURLConnection2).getInputStream();
        doReturn(HttpURLConnection.HTTP_OK).when(mockURLConnection2).getResponseCode();

        // Prepare the mocked URL connection for the redirection to simple playlist
        HttpURLConnection mockURLConnectionMove = Mockito.mock(HttpURLConnection.class);
        InputStream mockURLInputStreamMove = new ByteArrayInputStream("".getBytes());
        doReturn(HttpURLConnection.HTTP_MOVED_PERM).when(mockURLConnectionMove).getResponseCode();
        doReturn(TEST_PLAYLIST_URL_2).when(mockURLConnectionMove).getHeaderField(eq("Location"));

        // Prepare the mocked URL connection for the redirection loop
        HttpURLConnection mockURLConnectionMoveLoop = Mockito.mock(HttpURLConnection.class);
        InputStream mockURLInputStreamMoveLoop = new ByteArrayInputStream("".getBytes());
        doReturn(HttpURLConnection.HTTP_MOVED_PERM).when(mockURLConnectionMoveLoop).getResponseCode();
        doReturn(TEST_PLAYLIST_URL_MOVE_LOOP).when(mockURLConnectionMoveLoop).getHeaderField(eq("Location"));

        // Prepare the mocked URL connection for the 'content too large' test
        HttpURLConnection mockURLConnectionLarge = Mockito.mock(HttpURLConnection.class);
        InputStream mockURLInputStreamLarge = new InputStream() {
            private long pos = 0;
            @Override
            public int read() {
                return TEST_STREAM_PLAYLIST_CONTENTS_2.charAt((int)(pos++ % TEST_STREAM_PLAYLIST_CONTENTS_2.length()));
            }
        };
        doReturn(mockURLInputStreamLarge).when(mockURLConnectionLarge).getInputStream();
        doReturn(HttpURLConnection.HTTP_OK).when(mockURLConnectionLarge).getResponseCode();

        // Prepare the mocked URL connection for the 'content too large' test
        // (return a single entry with 'aaaa...' running infinitely long).
        HttpURLConnection mockURLConnectionLarge2 = Mockito.mock(HttpURLConnection.class);
        InputStream mockURLInputStreamLarge2 = new InputStream() {
            private long pos = 0;
            @Override
            public int read() {
                return 0x41;
            }
        };
        doReturn(mockURLInputStreamLarge2).when(mockURLConnectionLarge2).getInputStream();
        doReturn(HttpURLConnection.HTTP_OK).when(mockURLConnectionLarge2).getResponseCode();

        // Prepare the mock 'connectToURL' method
        doReturn(mockURLConnection1).when(internetRadioService).connectToURL(eq(new URL(TEST_PLAYLIST_URL_1)));
        doReturn(mockURLConnection2).when(internetRadioService).connectToURL(eq(new URL(TEST_PLAYLIST_URL_2)));
        doReturn(mockURLConnectionMove).when(internetRadioService).connectToURL(eq(new URL(TEST_PLAYLIST_URL_MOVE)));
        doReturn(mockURLConnectionMoveLoop).when(internetRadioService).connectToURL(eq(new URL(TEST_PLAYLIST_URL_MOVE_LOOP)));
        doReturn(mockURLConnectionLarge).when(internetRadioService).connectToURL(eq(new URL(TEST_PLAYLIST_URL_LARGE)));
        doReturn(mockURLConnectionLarge2).when(internetRadioService).connectToURL(eq(new URL(TEST_PLAYLIST_URL_LARGE_2)));
    }

    @Test
    public void testParseSimplePlaylist() {
        List<InternetRadioSource> radioSources = internetRadioService.getInternetRadioSources(radio1);

        Assert.assertEquals(2, radioSources.size());
        Assert.assertEquals(TEST_STREAM_URL_1, radioSources.get(0).getStreamUrl());
        Assert.assertEquals(TEST_STREAM_URL_2, radioSources.get(1).getStreamUrl());
    }

    @Test
    public void testRedirects() {
        List<InternetRadioSource> radioSources = internetRadioService.getInternetRadioSources(radioMove);

        Assert.assertEquals(2, radioSources.size());
        Assert.assertEquals(TEST_STREAM_URL_3, radioSources.get(0).getStreamUrl());
        Assert.assertEquals(TEST_STREAM_URL_4, radioSources.get(1).getStreamUrl());
    }

    @Test
    public void testLargeInput() {
        List<InternetRadioSource> radioSources = internetRadioService.getInternetRadioSources(radioLarge);

        // A PlaylistTooLarge exception is thrown internally, and the
        // `getInternetRadioSources` method logs it and returns a
        // limited number of sources.
        Assert.assertEquals(250, radioSources.size());
    }

    @Test
    public void testLargeInputURL() {
        List<InternetRadioSource> radioSources = internetRadioService.getInternetRadioSources(radioLarge2);

        // A PlaylistTooLarge exception is thrown internally, and the
        // `getInternetRadioSources` method logs it and returns a
        // limited number of bytes from the input.
        Assert.assertEquals(1, radioSources.size());
    }

    @Test
    public void testRedirectLoop() {
        List<InternetRadioSource> radioSources = internetRadioService.getInternetRadioSources(radioMoveLoop);

        // A PlaylistHasTooManyRedirects exception is thrown internally,
        // and the `getInternetRadioSources` method logs it and returns 0 sources.
        Assert.assertEquals(0, radioSources.size());
    }
}
