package io.delphiplatform.api.extapi;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.springframework.web.server.ResponseStatusException;

import java.net.URI;
import java.sql.Date;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import io.delphiplatform.api.integration.util.MockCurrentDateService;
import io.delphiplatform.api.util.DateUtils;
import io.delphiplatform.api.util.model.ModelUtils;
import io.delphiplatform.api.v3.bigtable.entity.PlaylistPublicDaily;
import io.delphiplatform.api.v3.constant.CountryCodeConstant;
import io.delphiplatform.api.v3.constant.DspConstants;
import io.delphiplatform.api.v3.model.Dsp;
import io.delphiplatform.api.v3.model.ImageSimple;
import io.delphiplatform.api.v3.model.PlaylistOwner;
import io.delphiplatform.api.v3.model.PublicPlaylist;
import io.delphiplatform.api.v3.model.service.ImageService;
import io.delphiplatform.api.v3.model.spotify.SpotifyAlbum;
import io.delphiplatform.api.v3.model.spotify.SpotifyArtist;
import io.delphiplatform.api.v3.model.spotify.SpotifyTrack;
import io.delphiplatform.api.v3.model.trackposition.PublicPlaylistTrackPosition;
import io.delphiplatform.api.v3.rdb.repository.spotify.SpotifyAlbumRepository;
import io.delphiplatform.api.v3.rdb.service.playlist_public.PlaylistPublicService;
import se.michaelthelin.spotify.SpotifyApi;
import se.michaelthelin.spotify.enums.ModelObjectType;
import se.michaelthelin.spotify.exceptions.detailed.NotFoundException;
import se.michaelthelin.spotify.model_objects.specification.AlbumSimplified;
import se.michaelthelin.spotify.model_objects.specification.ArtistSimplified;
import se.michaelthelin.spotify.model_objects.specification.ExternalId;
import se.michaelthelin.spotify.model_objects.specification.Followers;
import se.michaelthelin.spotify.model_objects.specification.Image;
import se.michaelthelin.spotify.model_objects.specification.Paging;
import se.michaelthelin.spotify.model_objects.specification.Playlist;
import se.michaelthelin.spotify.model_objects.specification.PlaylistTrack;
import se.michaelthelin.spotify.model_objects.specification.Track;
import se.michaelthelin.spotify.model_objects.specification.User;
import se.michaelthelin.spotify.requests.IRequest;
import se.michaelthelin.spotify.requests.data.playlists.GetPlaylistRequest;
import se.michaelthelin.spotify.requests.data.playlists.GetPlaylistsItemsRequest;

import static io.delphiplatform.api.v3.constant.DspConstants.SPOTIFY;
import static io.delphiplatform.api.v3.constant.DspConstants.SPOTIFY_NAME;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyCollection;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;

class SpotifyApiServiceTest {

    static class SpotifyApiRequestExecutorForMock extends SpotifyApiRequestExecutor {

        private static final Pattern PLAYLISTS_ITEMS_PATTERN = Pattern.compile("/v1/playlists/(\\w+)/tracks");
        private static final Pattern PLAYLIST_PATTERN = Pattern.compile("/v1/playlists/(\\w+)");

        public SpotifyApiRequestExecutorForMock(SpotifyApi spotifyApi) {
            super(spotifyApi);
        }

        @Override
        public <T> T executeAuthenticated(IRequest<T> request) throws NotFoundException {
            if (request instanceof GetPlaylistsItemsRequest) {
                return getPlaylistsItemsRequest(extractRequestArgument(request, PLAYLISTS_ITEMS_PATTERN));
            }
            if (request instanceof GetPlaylistRequest) {
                return getPlaylistRequest(extractRequestArgument(request, PLAYLIST_PATTERN));
            }
            throw new RuntimeException("Request not mapped: " + request.getUri().getPath());
        }

        public <T> T getPlaylistsItemsRequest(String playlistId) throws NotFoundException {
            return null;
        }

        public <T> T getPlaylistRequest(String playlistId) throws NotFoundException {
            return null;
        }

        private <T> String extractRequestArgument(IRequest<T> request, Pattern pattern) {
            Matcher matcher = pattern.matcher(request.getUri().getPath());
            if (matcher.matches()) {
                return matcher.group(1);
            }
            throw new RuntimeException("Error happens on Request arguments extraction");
        }
    }

    @Mock
    private ImageService imageService;
    @Mock
    private PlaylistPublicService playlistPublicService;
    @Mock
    private SpotifyApiRequestExecutorForMock requestExecutor;
    @Mock
    private SpotifyAlbumRepository spotifyAlbumRepository;

    private SpotifyApiService spotifyApiService;

    @BeforeEach
    void setup() {
        playlistPublicService = mock(PlaylistPublicService.class);
        imageService = mock(ImageService.class);
        SpotifyApi spotifyApi = SpotifyApi.builder()
            .setAccessToken("access_token")
            .setClientId("client_id")
            .build();
        requestExecutor = spy(new SpotifyApiRequestExecutorForMock(spotifyApi));
        spotifyApiService = new SpotifyApiService(playlistPublicService,
            new SpotifyApiPlaylistConvertor(imageService),
            new SpotifyApiPlaylistTrackConvertor(),
            requestExecutor, spotifyAlbumRepository);
    }

    static PlaylistTrack buildPlaylistTrackFor(ModelObjectType type, LocalDate date) {
        return new PlaylistTrack.Builder()
            .setAddedAt(Date.from(date.atStartOfDay().toInstant(ZoneOffset.UTC)))
            .setTrack(new Track.Builder()
                .setType(type)
                .build())
            .build();
    }

    static PlaylistTrack buildPlaylistTrackFor(ModelObjectType type, LocalDate date, SpotifyTrack referencedTrack) {
        assertAll("ReferencedTrack not initialized properly",
            () -> assertNotNull(referencedTrack),
            () -> assertNotNull(referencedTrack.getAlbum()),
            () -> assertNotNull(referencedTrack.getAlbum().getImage()),
            () -> assertNotNull(referencedTrack.getArtists())
        );
        return new PlaylistTrack.Builder()
            .setAddedAt(Date.from(date.atStartOfDay().toInstant(ZoneOffset.UTC)))
            .setTrack(new Track.Builder()
                .setType(type)
                .setId(referencedTrack.getTrackId())
                .setName(referencedTrack.getName())
                .setUri(referencedTrack.getImageUrl())
                .setPopularity(referencedTrack.getTrackPopularity().intValue())
                .setPreviewUrl(referencedTrack.getImageUrl())
                .setExternalIds(new ExternalId.Builder()
                    .setExternalIds(Map.of("isrc", referencedTrack.getIsrc()))
                    .build())
                .setAlbum(new AlbumSimplified.Builder()
                    .setId(referencedTrack.getAlbum().getAlbumId())
                    .setName(referencedTrack.getAlbum().getName())
                    .setReleaseDate(referencedTrack.getAlbum().getReleaseDate().toString())
                    .setImages(Stream.of(referencedTrack.getAlbum().getImage()).map(imageUrl ->
                        new Image.Builder()
                            .setUrl(imageUrl)
                            .build()).toArray(Image[]::new))
                    .build())
                .setArtists(referencedTrack.getArtists().stream()
                    .map(spotifyArtist -> new ArtistSimplified.Builder()
                        .setId(spotifyArtist.getArtistId())
                        .setName(spotifyArtist.getName())
                        .setUri(spotifyArtist.getUrl())
                        .build())
                    .collect(Collectors.toList())
                    .toArray(ArtistSimplified[]::new))
                .build())
            .build();
    }

    @Test
    void getPlaylistsItemsAsTrackPositions() throws NotFoundException {
        String playlistId = "spotify_playlist";
        List<String> playlistIds = List.of(playlistId);
        LocalDate addedAtDate = LocalDate.of(2020, 5, 16);
        List<SpotifyArtist> referencedArtists = List.of(SpotifyArtist.builder()
            .artistId("artist_id")
            .name("artist_name")
            .url("artist_url")
            .build());

        SpotifyAlbum referencedAlbum = new SpotifyAlbum();
        referencedAlbum.setAlbumId("album_id");
        referencedAlbum.setName("album_name");
        referencedAlbum.setImage("image_url");
        referencedAlbum.setUpc(null);
        referencedAlbum.setReleaseDate(LocalDate.of(2022, 10, 18));

        SpotifyTrack referencedTrack = new SpotifyTrack();
        referencedTrack.setTrackId("track_id");
        referencedTrack.setName("track_name");
        referencedTrack.setImageUrl("image_url");
        referencedTrack.setReleaseDate(referencedAlbum.getReleaseDate());
        referencedTrack.setIsrc("USSM12200612");
        referencedTrack.setTrackPopularity(100L);
        referencedTrack.setArtists(referencedArtists);
        referencedTrack.setAlbum(referencedAlbum);

        PublicPlaylistTrackPosition referencedTrackPosition = new PublicPlaylistTrackPosition();
        referencedTrackPosition.trackId(referencedTrack.getTrackId());
        referencedTrackPosition.setSpotifyTrack(referencedTrack);
        referencedTrackPosition.isrc(referencedTrack.getIsrc().toUpperCase());
        referencedTrackPosition.setCurrent(0L);
        referencedTrackPosition.earliestPositionDateTime(addedAtDate.atStartOfDay());
        referencedTrackPosition.setPlaylistId(playlistId);
        referencedTrackPosition.dsp(DspConstants.SPOTIFY);

        PlaylistTrack[] items = {
            buildPlaylistTrackFor(ModelObjectType.TRACK, addedAtDate, referencedTrack),
            buildPlaylistTrackFor(ModelObjectType.EPISODE, addedAtDate, referencedTrack),
        };
        Paging<PlaylistTrack> playlistItemsPaging = new Paging.Builder<PlaylistTrack>()
            .setItems(items)
            .setLimit(100)
            .setTotal(items.length)
            .build();

        when(requestExecutor.getPlaylistsItemsRequest(anyString())).thenReturn(playlistItemsPaging);

        List<PublicPlaylistTrackPosition> playlistTrackPositions = spotifyApiService.getPlaylistsItemsAsTrackPositions(playlistIds, false)
            .collect(Collectors.toList());

        assertAll(
            () -> assertEquals(1, playlistTrackPositions.size()),
            () -> assertEquals(referencedTrackPosition, playlistTrackPositions.get(0))
        );
    }

    @Test
    void getPlaylistsAsPlaylistPublicDaily_forNotExistedPlaylistId() throws NotFoundException {
        MockCurrentDateService.setMockDateTime(LocalDateTime.parse("2023-02-13T00:00:00"));

        String notExistedPlaylistId = "spotify_playlist_1";
        String existedPlaylistId = "spotify_playlist_2";
        List<String> playlistIds = List.of(notExistedPlaylistId, existedPlaylistId);
        PlaylistTrack[] items = {
            buildPlaylistTrackFor(ModelObjectType.TRACK, DateUtils.getCurrentDate().minusDays(1)),
            buildPlaylistTrackFor(ModelObjectType.EPISODE, DateUtils.getCurrentDate().minusDays(1))
        };
        Paging<PlaylistTrack> playlistItemsPaging = new Paging.Builder<PlaylistTrack>()
            .setItems(items)
            .setLimit(100)
            .setTotal(items.length)
            .build();

        when(requestExecutor.getPlaylistsItemsRequest(ModelUtils.removeDspPrefixFromPlaylistId(notExistedPlaylistId)))
            .thenThrow(new NotFoundException());
        when(requestExecutor.getPlaylistsItemsRequest(ModelUtils.removeDspPrefixFromPlaylistId(existedPlaylistId)))
            .thenReturn(playlistItemsPaging);

        List<PlaylistPublicDaily> playlistDaily = spotifyApiService.getPlaylistsAsPlaylistPublicDaily(playlistIds)
            .collect(Collectors.toList());

        assertAll(
            () -> assertEquals(1, playlistDaily.size()),
            () -> assertTrue(
                playlistDaily.stream().map(PlaylistPublicDaily::getNumberOfTracks).allMatch(numTracks -> numTracks == items.length - 1)),
            () -> assertTrue(playlistDaily.stream().map(PlaylistPublicDaily::getCountryCode).allMatch(CountryCodeConstant.GLOBAL::equals)),
            () -> assertTrue(
                playlistDaily.stream().map(PlaylistPublicDaily::getReportDate)
                    .allMatch(reportDate -> DateUtils.getCurrentDate().equals(reportDate))),
            () -> assertTrue(
                playlistDaily.stream().map(PlaylistPublicDaily::getPlaylistId).collect(Collectors.toSet()).contains(existedPlaylistId))
        );
    }

    @Test
    void getPlaylistsAsPlaylistPublicDaily() throws NotFoundException {
        MockCurrentDateService.setMockDateTime(LocalDateTime.parse("2023-02-13T00:00:00"));

        List<String> playlistIds = List.of("spotify_playlist_1", "spotify_playlist_2");
        PlaylistTrack[] items = {
            buildPlaylistTrackFor(ModelObjectType.TRACK, DateUtils.getCurrentDate().minusDays(1)),
            buildPlaylistTrackFor(ModelObjectType.TRACK, DateUtils.getCurrentDate().minusDays(1))
        };
        Paging<PlaylistTrack> playlistItemsPaging = new Paging.Builder<PlaylistTrack>()
            .setItems(items)
            .setLimit(100)
            .setTotal(items.length)
            .build();

        when(requestExecutor.getPlaylistsItemsRequest(anyString())).thenReturn(playlistItemsPaging);

        List<PlaylistPublicDaily> playlistDaily = spotifyApiService.getPlaylistsAsPlaylistPublicDaily(playlistIds)
            .collect(Collectors.toList());

        assertAll(
            () -> assertEquals(playlistDaily.size(), playlistIds.size()),
            () -> assertTrue(
                playlistDaily.stream().map(PlaylistPublicDaily::getNumberOfTracks).allMatch(numTracks -> numTracks == items.length)),
            () -> assertTrue(playlistDaily.stream().map(PlaylistPublicDaily::getCountryCode).allMatch(CountryCodeConstant.GLOBAL::equals)),
            () -> assertTrue(
                playlistDaily.stream().map(PlaylistPublicDaily::getReportDate)
                    .allMatch(reportDate -> DateUtils.getCurrentDate().equals(reportDate))),
            () -> assertTrue(
                playlistDaily.stream().map(PlaylistPublicDaily::getPlaylistId).collect(Collectors.toSet()).containsAll(playlistIds))
        );
    }

    @Test
    void getPlaylistAsPublicPlaylist_forNotExistedPlaylistId() throws NotFoundException {
        String notExistedPlaylistId = "playlist_id";
        when(requestExecutor.getPlaylistRequest(anyString())).thenThrow(new NotFoundException());
        assertThrows(ResponseStatusException.class, () -> spotifyApiService.getPlaylistAsPublicPlaylist(notExistedPlaylistId));
    }

    @Test
    void getPlaylistAsPublicPlaylist() throws NotFoundException {
        MockCurrentDateService.setMockDateTime(LocalDateTime.parse("2023-02-13T00:00:00"));

        String playlistId = "spotify_playlist_id";
        PlaylistTrack[] items = {
            buildPlaylistTrackFor(ModelObjectType.TRACK, DateUtils.getCurrentDate().minusDays(1)),
            buildPlaylistTrackFor(ModelObjectType.TRACK, DateUtils.getCurrentDate().minusDays(1))
        };
        Paging<PlaylistTrack> tracks = new Paging.Builder<PlaylistTrack>()
            .setItems(items)
            .setLimit(100)
            .setTotal(items.length)
            .build();

        Dsp referencedDsp = new Dsp()
            .dspId(SPOTIFY)
            .name(SPOTIFY_NAME)
            .slug(SPOTIFY);
        ImageSimple referencedImageSimple = new ImageSimple()
            .uri(URI.create("image_uri"));
        PlaylistOwner referencedPlaylistOwner = PlaylistOwner.builder()
            .displayName("owner_display_name")
            .username("username")
            .dsp(referencedDsp)
            .build();
        PublicPlaylist referencedPublicPlaylist = new PublicPlaylist()
            .playlistId(playlistId)
            .dspPlaylistId(ModelUtils.removeDspPrefixFromPlaylistId(playlistId))
            .dsp(referencedDsp)
            .name("playlist_name")
            .uri(URI.create("playlist_uri"))
            .description("description")
            .followers(10)
            .numTracks(tracks.getTotal())
            .owner(referencedPlaylistOwner)
            .image(referencedImageSimple);

        Image[] images = {new Image.Builder()
            .setUrl(referencedImageSimple.getUri().toString())
            .build()};
        Playlist spotifyPlaylist = new Playlist.Builder()
            .setId(ModelUtils.removeDspPrefixFromPlaylistId(playlistId))
            .setName(referencedPublicPlaylist.getName())
            .setUri(referencedPublicPlaylist.getUri().get().toString())
            .setDescription(referencedPublicPlaylist.getDescription())
            .setFollowers(new Followers.Builder()
                .setTotal(referencedPublicPlaylist.getFollowers())
                .build())
            .setTracks(tracks)
            .setImages(images)
            .setOwner(new User.Builder()
                .setId(referencedPublicPlaylist.getOwner().getUsername())
                .setDisplayName(referencedPublicPlaylist.getOwner().getDisplayName())
                .build())
            .build();

        when(playlistPublicService.findAllById(anyCollection())).thenReturn(Collections.emptyList());
        when(imageService.resolveImage(anyString(), anyString(), any(), anyString())).thenReturn(referencedImageSimple);
        when(requestExecutor.getPlaylistRequest(anyString())).thenReturn(spotifyPlaylist);

        PublicPlaylist publicPlaylist = spotifyApiService.getPlaylistAsPublicPlaylist(playlistId);
        assertEquals(publicPlaylist, referencedPublicPlaylist);
    }
}