package org.airsonic.player.api.jukebox;

import org.airsonic.player.TestCaseUtils;
import org.airsonic.player.controller.SubsonicRESTController;
import org.airsonic.player.dao.*;
import org.airsonic.player.domain.*;
import org.airsonic.player.service.MediaScannerService;
import org.airsonic.player.service.PlayerService;
import org.airsonic.player.service.SettingsService;
import org.airsonic.player.util.HomeRule;
import org.airsonic.player.util.MusicFolderTestData;
import org.airsonic.player.util.StringUtil;
import org.junit.*;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.ResultMatcher;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = AbstractAirsonicRestApiJukeboxIntTest.Config.class)
@AutoConfigureMockMvc
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
public abstract class AbstractAirsonicRestApiJukeboxIntTest {

    @ClassRule
    public static final HomeRule classRule = new HomeRule(); // sets airsonic.home to a temporary dir

    @TestConfiguration
    static class Config {
        @Bean
        public BeanPostProcessor convertToSpy() {
            return new BeanPostProcessor() {
                @Override
                public Object postProcessAfterInitialization(Object bean, String beanName) {
                    if (bean instanceof PlayerDaoPlayQueueFactory) {
                        PlayerDaoPlayQueueFactory temp = (PlayerDaoPlayQueueFactory) spy(bean);
                        doReturn(spy(temp.createPlayQueue())).when(temp).createPlayQueue();
                        bean = temp;
                    }
                    return bean;
                }
            };
        }
    }

    static final String CLIENT_NAME = "airsonic";
    static final String JUKEBOX_PLAYER_NAME = CLIENT_NAME + "-jukebox";
    private static final String EXPECTED_FORMAT = "json";
    private static String AIRSONIC_API_VERSION;

    private static boolean dataBasePopulated;

    @Autowired
    protected PlayerService playerService;
    @Autowired
    private MockMvc mvc;
    @Autowired
    private MusicFolderDao musicFolderDao;
    @Autowired
    private SettingsService settingsService;
    @Autowired
    private MediaScannerService mediaScannerService;
    @Autowired
    private PlayerDao playerDao;
    @Autowired
    private MediaFileDao mediaFileDao;
    @Autowired
    private DaoHelper daoHelper;
    @Autowired
    private AlbumDao albumDao;
    @Autowired
    private ArtistDao artistDao;

    private Player testJukeboxPlayer;

    @BeforeClass
    public static void setupClass() {
        AIRSONIC_API_VERSION = TestCaseUtils.restApiVersion();
        dataBasePopulated = false;
    }

    /**
     * Populate test datas in the database only once.
     *
     * <ul>
     *     <li>Creates 2 music folder</li>
     *     <li>Scans the music folders</li>
     *     <li>Creates a test jukebox player</li>
     * </ul>
     */
    private void populateDatabase() {
        if (!dataBasePopulated) {

            assertThat(musicFolderDao.getAllMusicFolders().size()).isEqualTo(1);
            MusicFolderTestData.getTestMusicFolders().forEach(musicFolderDao::createMusicFolder);
            settingsService.clearMusicFolderCache();

            TestCaseUtils.execScan(mediaScannerService);

            assertThat(playerDao.getAllPlayers().size()).isEqualTo(0);
            createTestPlayer();
            assertThat(playerDao.getAllPlayers().size()).isEqualTo(1);

            dataBasePopulated = true;
        }
    }

    @Before
    public void setup() throws Exception {
        populateDatabase();

        testJukeboxPlayer = findTestJukeboxPlayer();
        assertThat(testJukeboxPlayer).isNotNull();
        reset(testJukeboxPlayer.getPlayQueue());
        testJukeboxPlayer.getPlayQueue().clear();
        assertThat(testJukeboxPlayer.getPlayQueue().size()).isEqualTo(0);
        testJukeboxPlayer.getPlayQueue().addFiles(true,
                mediaFileDao.getSongsForAlbum("_DIR_ Ravel", "Complete Piano Works"));
        assertThat(testJukeboxPlayer.getPlayQueue().size()).isEqualTo(2);
    }

    @After
    public void cleanDataBase() {
        daoHelper.getJdbcTemplate().execute("DROP SCHEMA PUBLIC CASCADE");
        dataBasePopulated = false;
    }

    protected abstract void createTestPlayer();

    private Player findTestJukeboxPlayer() {
        return playerDao.getAllPlayers().stream().filter(player -> player.getName().equals(JUKEBOX_PLAYER_NAME))
                .findFirst().orElseThrow(() -> new RuntimeException("No player found in database"));
    }

    private String convertDateToString(Date date) {
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
        formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
        return formatter.format(date);
    }

    private ResultMatcher playListItem1isCorrect() {
        MediaFile mediaFile = testJukeboxPlayer.getPlayQueue().getFile(0);
        MediaFile parent = mediaFileDao.getMediaFile(mediaFile.getParentPath());
        Album album = albumDao.getAlbum(mediaFile.getArtist(), mediaFile.getAlbumName());
        Artist artist = artistDao.getArtist(mediaFile.getArtist());
        assertThat(album).isNotNull();
        return result -> {
            jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].id").value(mediaFile.getId()).match(result);
            jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].parent").value(parent.getId()).match(result);
            jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].isDir").value(mediaFile.isDirectory()).match(result);
            jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].title").value(mediaFile.getTitle()).match(result);
            jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].album").value(mediaFile.getAlbumName()).match(result);
            jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].artist").value(mediaFile.getArtist()).match(result);
            jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].coverArt").value(parent.getId()).match(result);
            jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].size").value(mediaFile.getFileSize()).match(result);
            jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].contentType").value(StringUtil.getMimeType(mediaFile.getFormat())).match(result);
            jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].suffix").value(mediaFile.getFormat()).match(result);
            jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].duration").value(mediaFile.getDurationSeconds()).match(result);
            jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].bitRate").value(mediaFile.getBitRate()).match(result);
            jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].path").value(SubsonicRESTController.getRelativePath(mediaFile, settingsService)).match(result);
            jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].isVideo").value(mediaFile.isVideo()).match(result);
            jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].playCount").isNumber().match(result);
            jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].created").value(convertDateToString(mediaFile.getCreated())).match(result);
            jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].albumId").value(album.getId()).match(result);
            jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].artistId").value(artist.getId()).match(result);
            jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].type").value(mediaFile.getMediaType().name().toLowerCase()).match(result);
        };
    }

    @Test
    @WithMockUser(username = "admin")
    public void jukeboxStartActionTest() throws Exception {
        // Given

        // When and Then
        performStartAction();
        performStatusAction("true");
        performGetAction()
                .andExpect(jsonPath("$.subsonic-response.jukeboxPlaylist.currentIndex").value("0"))
                .andExpect(jsonPath("$.subsonic-response.jukeboxPlaylist.playing").value("true"))
                .andExpect(jsonPath("$.subsonic-response.jukeboxPlaylist.gain").value("0.75"))
                .andExpect(jsonPath("$.subsonic-response.jukeboxPlaylist.position").value("0"))
                .andExpect(jsonPath("$.subsonic-response.jukeboxPlaylist.entry").isArray())
                .andExpect(jsonPath("$.subsonic-response.jukeboxPlaylist.entry.length()").value(2))
                .andExpect(playListItem1isCorrect())
                .andDo(print());

        verify(testJukeboxPlayer.getPlayQueue(), times(2)).setStatus(PlayQueue.Status.PLAYING);
        assertThat(testJukeboxPlayer.getPlayQueue().getIndex()).isEqualTo(0);
        assertThat(testJukeboxPlayer.getPlayQueue().getStatus()).isEqualTo(PlayQueue.Status.PLAYING);
    }

    @Test
    @WithMockUser(username = "admin")
    public void jukeboxStopActionTest() throws Exception {
        // Given

        // When and Then
        performStartAction();
        performStatusAction("true");
        performStopAction();
        performStatusAction("false");

        verify(testJukeboxPlayer.getPlayQueue(), times(2)).setStatus(PlayQueue.Status.PLAYING);
        verify(testJukeboxPlayer.getPlayQueue(), times(1)).setStatus(PlayQueue.Status.STOPPED);
        assertThat(testJukeboxPlayer.getPlayQueue().getIndex()).isEqualTo(0);
        assertThat(testJukeboxPlayer.getPlayQueue().getStatus()).isEqualTo(PlayQueue.Status.STOPPED);
    }

    private void performStatusAction(String expectedPlayingValue) throws Exception {
        mvc.perform(get("/rest/jukeboxControl.view")
                .param("v", AIRSONIC_API_VERSION)
                .param("c", CLIENT_NAME)
                .param("f", EXPECTED_FORMAT)
                .param("action", "status")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.subsonic-response.status").value("ok"))
                .andExpect(jsonPath("$.subsonic-response.jukeboxStatus.currentIndex").value("0"))
                .andExpect(jsonPath("$.subsonic-response.jukeboxStatus.playing").value(expectedPlayingValue))
                .andExpect(jsonPath("$.subsonic-response.jukeboxStatus.position").value("0"));
    }

    private ResultActions performGetAction() throws Exception {
        return mvc.perform(get("/rest/jukeboxControl.view")
                .param("v", AIRSONIC_API_VERSION)
                .param("c", CLIENT_NAME)
                .param("f", EXPECTED_FORMAT)
                .param("action", "get")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.subsonic-response.status").value("ok"));
    }

    private void performStopAction() throws Exception {
        mvc.perform(get("/rest/jukeboxControl.view")
                .param("v", AIRSONIC_API_VERSION)
                .param("c", CLIENT_NAME)
                .param("f", EXPECTED_FORMAT)
                .param("action", "stop")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.subsonic-response.status").value("ok"))
                .andExpect(jsonPath("$.subsonic-response.jukeboxStatus.currentIndex").value("0"))
                .andExpect(jsonPath("$.subsonic-response.jukeboxStatus.playing").value("false"))
                .andExpect(jsonPath("$.subsonic-response.jukeboxStatus.position").value("0"));
    }

    private void performStartAction() throws Exception {
        mvc.perform(get("/rest/jukeboxControl.view")
                .param("v", AIRSONIC_API_VERSION)
                .param("c", CLIENT_NAME)
                .param("f", EXPECTED_FORMAT)
                .param("action", "start")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.subsonic-response.status").value("ok"))
                .andExpect(jsonPath("$.subsonic-response.jukeboxStatus.currentIndex").value("0"))
                .andExpect(jsonPath("$.subsonic-response.jukeboxStatus.playing").value("true"))
                .andExpect(jsonPath("$.subsonic-response.jukeboxStatus.position").value("0"));
    }
}
