package io.delphiplatform.api.v3.rdb.service;

import com.google.common.collect.Sets;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;

import io.delphiplatform.api.util.StubSpecification;
import io.delphiplatform.api.v3.constant.ApplicationConstants;
import io.delphiplatform.api.v3.model.service.ImageService;
import io.delphiplatform.api.v3.model.video.ExpandTo;
import io.delphiplatform.api.v3.model.video.Video;
import io.delphiplatform.api.v3.model.video.VideoDspSlug;
import io.delphiplatform.api.v3.model.video.YoutubeContentType;
import io.delphiplatform.api.v3.rdb.entity.video.VideoEntity;
import io.delphiplatform.api.v3.rdb.repository.VideoRepository;
import io.delphiplatform.api.v3.rdb.service.specification.SpecificationProvider;
import io.delphiplatform.api.v3.view.util.Params;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

class VideoServiceTest {

    private static final String ISRC_1 = "ISRC_1";
    private static final String ISRC_2 = "ISRC_2";
    private static final String ISRC_3 = "ISRC_3";
    private static final String ARTIST_1 = "artist1";
    private static final String ARTIST_2 = "artist2";
    private static final String TRACK_1 = "track1";
    private static final String VIDEO_ID_1 = "video1";
    private static final String VIDEO_ID_2 = "video2";
    private static final String CHANNEL_1 = "channel1";
    private static final Long MIN_UGC_VIEWS = 1000L;
    private static final Set<YoutubeContentType> DEF_CONTENT_TYPES = Set
        .of(YoutubeContentType.PARTNER_UPLOADED, YoutubeContentType.PREMIUM_UGC);
    private final Set<String> DEF_FETCHES = Set.of("channel");
    private VideoService service;
    private VideoRepository repository;
    private SpecificationProvider<VideoEntity> specificationProvider;
    private ImageService imageService;
    private TrackService trackService;
    private ArtistService artistService;

    @BeforeEach
    public void setUp() {

        repository = mock(VideoRepository.class);
        specificationProvider = mock(SpecificationProvider.class);
        imageService = spy(new ImageService());
        trackService = mock(TrackService.class);
        artistService = mock(ArtistService.class);

        when(specificationProvider.singleValueSpec(anyString(), any())).thenReturn(new StubSpecification<>());
        when(specificationProvider.singleValueSpec(anyString(), any(), anyString()))
            .thenReturn(new StubSpecification<>());
        when(specificationProvider.singleValueSpec(any(List.class), any(), any(List.class)))
            .thenReturn(new StubSpecification<>());

        service = new VideoService(repository, specificationProvider, imageService, trackService, artistService);
    }

    @Test
    void getVideoIds_byVideoIds() {
        Set<String> requestVideoIds = Set.of("v1", "v2");
        List<String> videoIds = service.getVideoIds(Params.builder()
            .videoIds(requestVideoIds)
            .build());

        assertEquals(new ArrayList<>(requestVideoIds), videoIds,
            "When query by video_ids other params can be ignored.");
    }

    @Test
    void getIsrcsForVideos_listOfIsrc() {
        Set<String> isrcsForVideos = service.getIsrcsForVideos(Params.builder()
            .isrc(List.of(ISRC_1))
            .build());

        assertEquals(Set.of(ISRC_1), isrcsForVideos);
    }

    @Test
    void getIsrcsForVideos_listOfIsrcAndRelated() {
        when(trackService.findRelatedIsrc(List.of(ISRC_1)))
            .thenReturn(Set.of(ISRC_1, ISRC_2));

        Set<String> isrcsForVideos = service.getIsrcsForVideos(Params.builder()
            .isrc(List.of(ISRC_1))
            .expandTo(Set.of(ExpandTo.RELATED_ISRCS))
            .build());

        assertEquals(Set.of(ISRC_1, ISRC_2), isrcsForVideos);
    }

    @Test
    void getIsrcsForVideos_byArtistId() {
        Set<String> participants = Set.of(ARTIST_1, ARTIST_2);
        when(artistService.findParticipantIdsByArtistId(ARTIST_1)).thenReturn(participants);
        when(trackService.findIsrcsByArtist(eq(participants), any())).thenReturn(Set.of(ISRC_1, ISRC_2));
        Set<String> isrcsForVideos = service.getIsrcsForVideos(Params.builder()
            .artistId(ARTIST_1)
            .build());

        assertEquals(Set.of(ISRC_1, ISRC_2), isrcsForVideos);
    }

    @Test
    void getIsrcsForVideos_byArtistIdAndTrackId() {
        Set<String> participants = Set.of(ARTIST_1, ARTIST_2);
        when(artistService.findParticipantIdsByArtistId(ARTIST_1)).thenReturn(participants);
        when(trackService.findIsrcsForVideo(List.of(TRACK_1), participants)).thenReturn(Set.of(ISRC_1, ISRC_2));
        Set<String> isrcsForVideos = service.getIsrcsForVideos(Params.builder()
            .artistId(ARTIST_1)
            .trackId(List.of(TRACK_1))
            .build());

        assertEquals(Set.of(ISRC_1, ISRC_2), isrcsForVideos);
    }


    @Test
    void getIsrcsForVideos_byIsrcAndArtistId() {
        Set<String> participants = Set.of(ARTIST_1, ARTIST_2);

        when(artistService.findParticipantIdsByArtistId(ARTIST_1)).thenReturn(participants);
        when(trackService.findIsrcsByArtist(eq(participants), any())).thenReturn(Set.of(ISRC_2, ISRC_3));
        when(trackService.findRelatedIsrc(List.of(ISRC_1))).thenReturn(Sets.newHashSet(ISRC_1, ISRC_2));

        Set<String> isrcsForVideos = service.getIsrcsForVideos(Params.builder()
            .artistId(ARTIST_1)
            .isrc(List.of(ISRC_1))
            .expandTo(Set.of(ExpandTo.RELATED_ISRCS))
            .build());

        assertEquals(Set.of(ISRC_2), isrcsForVideos, "The resulting set should contain interception");
    }

    @Test
    void getIsrcsForVideos_byVideId() {
        VideoEntity video = new VideoEntity();
        video.setIsrcs(Set.of(ISRC_1, ISRC_2));
        when(repository.findByVideoIdIn(Set.of(VIDEO_ID_1))).thenReturn(List.of(video));

        Set<String> isrcsForVideos = service.getIsrcsForVideos(Params.builder()
            .videoIds(Set.of(VIDEO_ID_1))
            .build());

        assertEquals(Set.of(ISRC_1, ISRC_2), isrcsForVideos);
    }

    @Test
    void find_byChannelAndIsSonyAndDsp() {
        VideoEntity testEntity = new VideoEntity();
        testEntity.setVideoId(VIDEO_ID_1);
        when(repository.findAll(any(StubSpecification.class), any(Pageable.class)))
            .thenReturn(new PageImpl<>(List.of(testEntity)));

        List<Video> videos = service.find(Params.builder()
            .channelId(CHANNEL_1)
            .isSonyVideo(true)
            .videoDsp(VideoDspSlug.YOUTUBE)
            .minPremiumUgcViews(MIN_UGC_VIEWS)
            .youTubeContentTypes(DEF_CONTENT_TYPES)
            .build());

        assertEquals(1, videos.size());
        assertEquals(VIDEO_ID_1, videos.get(0).getVideoId());

        verify(specificationProvider).singleValueSpec("channelId", CHANNEL_1, "channel");
        verify(specificationProvider).singleValueSpec("dsp", VideoDspSlug.YOUTUBE);
        verify(specificationProvider)
            .videosByContentTypeSpec(DEF_CONTENT_TYPES, MIN_UGC_VIEWS);
        verify(specificationProvider).multiValueSpec("videoId", null);
        verify(specificationProvider).fetchesSpec(DEF_FETCHES);

        verify(repository).findAll(any(StubSpecification.class), any(Pageable.class));

        verifyNoMoreInteractions(specificationProvider, repository);
    }

    @Test
    void find_byArtistId() {
        VideoEntity testEntity = new VideoEntity();
        testEntity.setVideoId(VIDEO_ID_1);
        when(repository.findAll(any(StubSpecification.class), any(Pageable.class)))
            .thenReturn(new PageImpl<>(List.of(testEntity)));
        Set<String> participants = Set.of(ARTIST_1, ARTIST_2);
        when(artistService.findParticipantIdsByArtistId(ARTIST_1)).thenReturn(participants);
        when(repository.findVideoIdsWithArtistIdAndContentType(participants, YoutubeContentType.PARTNER_UPLOADED
            .getValue().toUpperCase(), 0L)).thenReturn(Set.of(VIDEO_ID_1));
        when(repository.findVideoIdsWithArtistIdAndContentType(participants, YoutubeContentType.PREMIUM_UGC
            .getValue().toUpperCase(), MIN_UGC_VIEWS)).thenReturn(Set.of(VIDEO_ID_2));

        List<Video> videos = service.find(Params.builder()
            .artistId(ARTIST_1)
            .minPremiumUgcViews(MIN_UGC_VIEWS)
            .youTubeContentTypes(DEF_CONTENT_TYPES)
            .build());

        assertEquals(1, videos.size());
        assertEquals(VIDEO_ID_1, videos.get(0).getVideoId());

        verify(specificationProvider).singleValueSpec("channelId", null, "channel");
        verify(specificationProvider).singleValueSpec("dsp", (Object) null);
        verify(specificationProvider)
            .videosByContentTypeSpec(DEF_CONTENT_TYPES, MIN_UGC_VIEWS);
        verify(specificationProvider).fetchesSpec(DEF_FETCHES);

        verify(specificationProvider).multiValueSpec("videoId", Set.of(VIDEO_ID_1, VIDEO_ID_2));
        verify(specificationProvider).multiValueSpec("videoId", null);

        verify(repository).findAll(any(StubSpecification.class), any(Pageable.class));
        verify(repository).findVideoIdsWithArtistIdAndContentType(participants,
            YoutubeContentType.PARTNER_UPLOADED.getValue().toUpperCase(), 0L);
        verify(repository).findVideoIdsWithArtistIdAndContentType(participants,
            YoutubeContentType.PREMIUM_UGC.getValue().toUpperCase(), MIN_UGC_VIEWS);

        verifyNoMoreInteractions(specificationProvider, repository);
    }


    @Test
    void find_byArtistId_noVideos() {
        Set<String> participants = Set.of(ARTIST_1, ARTIST_2);
        when(artistService.findParticipantIdsByArtistId(ARTIST_1)).thenReturn(participants);
        when(repository.findVideoIdsWithArtistIdAndContentType(participants, YoutubeContentType.PARTNER_UPLOADED
            .getValue().toUpperCase(), 0L)).thenReturn(Collections.emptySet());
        when(repository.findVideoIdsWithArtistIdAndContentType(participants, YoutubeContentType.PREMIUM_UGC
            .getValue().toUpperCase(), MIN_UGC_VIEWS)).thenReturn(Collections.emptySet());

        List<Video> videos = service.find(Params.builder()
            .artistId(ARTIST_1)
            .minPremiumUgcViews(MIN_UGC_VIEWS)
            .youTubeContentTypes(DEF_CONTENT_TYPES)
            .build());

        assertEquals(0, videos.size());

        verify(specificationProvider).singleValueSpec("channelId", null, "channel");
        verify(specificationProvider).singleValueSpec("dsp", (Object) null);
        verify(specificationProvider)
            .videosByContentTypeSpec(DEF_CONTENT_TYPES, MIN_UGC_VIEWS);
        verify(specificationProvider).multiValueSpec("videoId", null);
        verify(specificationProvider).fetchesSpec(DEF_FETCHES);

        verify(repository).findVideoIdsWithArtistIdAndContentType(participants,
            YoutubeContentType.PARTNER_UPLOADED.getValue().toUpperCase(), 0L);
        verify(repository).findVideoIdsWithArtistIdAndContentType(participants,
            YoutubeContentType.PREMIUM_UGC.getValue().toUpperCase(), MIN_UGC_VIEWS);

        verifyNoMoreInteractions(specificationProvider, repository);
    }


    @Test
    void find_byIsrc() {
        VideoEntity testEntity = new VideoEntity();
        testEntity.setVideoId(VIDEO_ID_1);
        when(repository.findAll(any(StubSpecification.class), any(Pageable.class)))
            .thenReturn(new PageImpl<>(List.of(testEntity)));

        List<Video> videos = service.find(Params.builder()
            .isrc(List.of(ISRC_1, ISRC_2))
            .minPremiumUgcViews(MIN_UGC_VIEWS)
            .youTubeContentTypes(DEF_CONTENT_TYPES)
            .build());

        assertEquals(1, videos.size());
        assertEquals(VIDEO_ID_1, videos.get(0).getVideoId());

        verify(specificationProvider).singleValueSpec("channelId", null, "channel");
        verify(specificationProvider).singleValueSpec("dsp", (Object) null);
        verify(specificationProvider)
            .videosByContentTypeSpec(DEF_CONTENT_TYPES, MIN_UGC_VIEWS);
        verify(specificationProvider).fetchesSpec(DEF_FETCHES);
        verify(specificationProvider).videosByIsrcsSpec(Set.of(ISRC_1, ISRC_2), DEF_CONTENT_TYPES,
            MIN_UGC_VIEWS);
        verify(specificationProvider).multiValueSpec("videoId", null);

        verify(repository).findAll(any(StubSpecification.class), any(Pageable.class));
        verifyNoMoreInteractions(specificationProvider, repository);
    }

    @Test
    void getAllVideosForFurtherProcessing_byArtistId() {
        VideoEntity testEntity = new VideoEntity();
        testEntity.setVideoId(VIDEO_ID_1);
        when(repository.findAll(any(StubSpecification.class), any(Pageable.class)))
            .thenReturn(new PageImpl<>(List.of(testEntity)));
        Set<String> participants = Set.of(ARTIST_1, ARTIST_2);
        when(artistService.findParticipantIdsByArtistId(ARTIST_1)).thenReturn(participants);
        when(repository.findVideoIdsWithArtistIdAndContentType(participants, YoutubeContentType.PARTNER_UPLOADED
            .getValue().toUpperCase(), 0L)).thenReturn(Set.of(VIDEO_ID_1));
        when(repository.findVideoIdsWithArtistIdAndContentType(participants, YoutubeContentType.PREMIUM_UGC
            .getValue().toUpperCase(), MIN_UGC_VIEWS)).thenReturn(Set.of(VIDEO_ID_2));

        List<Video> videos = service.getAllVideosForFurtherProcessing(Params.builder()
            .youTubeContentTypes(DEF_CONTENT_TYPES)
            .minPremiumUgcViews(MIN_UGC_VIEWS)
            .artistId(ARTIST_1)
            .videoDsp(VideoDspSlug.YOUTUBE)
            .isSonyVideo(true)
            .build());

        assertEquals(1, videos.size());
        assertEquals(VIDEO_ID_1, videos.get(0).getVideoId());

        verify(specificationProvider).singleValueSpec("channelId", null, "channel");
        verify(specificationProvider).singleValueSpec("dsp", VideoDspSlug.YOUTUBE);
        verify(specificationProvider)
            .videosByContentTypeSpec(DEF_CONTENT_TYPES, MIN_UGC_VIEWS);
        verify(specificationProvider).multiValueSpec("videoId", null);
        verify(specificationProvider).fetchesSpec(DEF_FETCHES);

        verify(specificationProvider).multiValueSpec("videoId", Set.of(VIDEO_ID_1, VIDEO_ID_2));

        ArgumentCaptor<Pageable> pageableArgumentCaptor = ArgumentCaptor.forClass(Pageable.class);

        verify(repository).findAll(any(StubSpecification.class), pageableArgumentCaptor.capture());
        Pageable pageable = pageableArgumentCaptor.getValue();
        assertFalse(pageable.getSort().isSorted());
        assertEquals(ApplicationConstants.DEFAULT_PAGE_SIZE, pageable.getPageSize());

        verify(repository).findVideoIdsWithArtistIdAndContentType(participants,
            YoutubeContentType.PARTNER_UPLOADED.getValue().toUpperCase(), 0L);
        verify(repository).findVideoIdsWithArtistIdAndContentType(participants,
            YoutubeContentType.PREMIUM_UGC.getValue().toUpperCase(), MIN_UGC_VIEWS);

        verifyNoMoreInteractions(specificationProvider, repository);
    }

}
