package io.delphiplatform.api.integration;

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import io.delphiplatform.api.extapi.SpotifyApiRequestExecutor;
import io.delphiplatform.api.integration.annotation.BigTable;
import io.delphiplatform.api.integration.annotation.SqlScript;
import io.delphiplatform.api.integration.dto.SpotifyApiPlaylistTrackDto;
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.model.IncludeTracklistPlaylist;
import io.delphiplatform.api.v3.model.trackposition.PlaylistsTrackPositionsCurrentTracklist;
import io.delphiplatform.api.v3.model.trackposition.PlaylistsTrackPositionsCurrentTracklists;
import se.michaelthelin.spotify.enums.ModelObjectType;
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.Image;
import se.michaelthelin.spotify.model_objects.specification.PlaylistTrack;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class TrackPositionsPlaylistsCurrentTracklistIT extends BaseIT {

    @Autowired
    private SpotifyApiRequestExecutor spotifyAPIRequestExecutor;

    /**
     * Checking population for the current track position for Spotify, including numTrack, playlistDates, trackInfo options
     * <p>
     * 4 positions are created, 2 of them have the same ISRC to check isTopTrackForIsrc. All positions are populated by lifetime metrics, only
     * part of positions are populated by SpotifyTrack. Lifetime metrics, numTrack and playlistDates have simple check for not-null values.
     * SpotifyTrack check against not-null anyMatch condition ISRC duplicates are sorted by 'current' field, checked that there are 2 duplicates,
     * top one has isTopTrackForIsrc=true and second one has isTopTrackForIsrc=false
     */
    @Test
    @BigTable(table = "playlist_tracks", json =
        "/scripts/TrackPositionsPlaylistsCurrentTracklistIT/BT_playlist_tracks_getPublicTrackPositionsPlaylists_spotify.json")
    @BigTable(table = "spotify_fact_public_playlist_daily", json =
        "/scripts/TrackPositionsPlaylistsCurrentTracklistIT/BT_spotify_fact_public_playlist_daily_getPublicTrackPositionsPlaylists_spotify.json")
    @SqlScript(scripts = "/scripts/TrackPositionsPlaylistsCurrentTracklistIT/common_test_data_set_1.sql")
    public void getPublicTrackPositionsPlaylists_spotify() {
        MockCurrentDateService.setMockDateTime(LocalDateTime.parse("2023-02-13T00:00:00"));
        String spotifyPlaylistId = "spotify_1QgE14vwLmO3WnAYmInwoy";
        String duplicateIsrc = "TCAFS2164573";

        ResponseEntity<PlaylistsTrackPositionsCurrentTracklists> response = apiClient.sendAPIRequest(
            "/v3/public/track-positions/playlists/current-tracklist?"
                + "playlist_id=" + spotifyPlaylistId
                + "&include=" + String.format("%s", IncludeTracklistPlaylist.NUM_TRACKS.getValue())
                + "&include=" + String.format("%s", IncludeTracklistPlaylist.PLAYLIST_DATES.getValue())
                + "&include=" + String.format("%s", IncludeTracklistPlaylist.TRACK_INFO.getValue()),
            PlaylistsTrackPositionsCurrentTracklists.class
        );

        List<PlaylistsTrackPositionsCurrentTracklist> trackPositions = response.getBody().getItems();
        List<PlaylistsTrackPositionsCurrentTracklist> duplicatedTrackPositions = trackPositions.stream()
            .filter(p -> duplicateIsrc.equals(p.getIsrc()))
            .sorted(Comparator.comparing(PlaylistsTrackPositionsCurrentTracklist::getCurrent))
            .collect(Collectors.toList());

        assertEquals(HttpStatus.OK, response.getStatusCode());
        assertEquals(4, response.getBody().getCount());
        assertEquals(2, duplicatedTrackPositions.size());
        assertTrue(duplicatedTrackPositions.get(0).getIsTopTrackForIsrc());
        assertFalse(duplicatedTrackPositions.get(1).getIsTopTrackForIsrc());
        assertTrue(trackPositions.stream().anyMatch(p -> p.getSpotifyTrack() != null));
        assertTrue(trackPositions.stream()
            .allMatch(p ->
                Stream.of(
                    p.getEarliestPosition(),
                    p.getEarliestPositionDateTime(),
                    p.getLastAddedDateTime(),
                    p.getCurrent(),
                    p.getNumTracks(),
                    p.getPlaylistDates(),
                    p.getIsrc(),
                    p.getTrackId(),
                    p.getNumDaysOn()
                ).allMatch(Objects::nonNull)
            ));
    }

    /**
     * Checking population for the current track position for Apple, including numTrack, playlistDates, trackInfo options
     * <p>
     * 4 positions are created, 2 of them have the same ISRC to check isTopTrackForIsrc. All positions are populated by lifetime metrics, only
     * part of positions are populated by AppleTrack. Lifetime metrics, numTrack and playlistDates have simple check for not-null values.
     * AppleTrack check against not-null anyMatch condition ISRC duplicates are sorted by 'current' field, checked that there are 2 duplicates,
     * top one has isTopTrackForIsrc=true and second one has isTopTrackForIsrc=false
     */
    @Test
    @BigTable(table = "playlist_tracks", json =
        "/scripts/TrackPositionsPlaylistsCurrentTracklistIT/BT_playlist_tracks_getPublicTrackPositionsPlaylists_apple.json")
    @BigTable(table = "apple_fact_public_playlist_daily", json =
        "/scripts/TrackPositionsPlaylistsCurrentTracklistIT/BT_apple_fact_public_playlist_daily_getPublicTrackPositionsPlaylists_apple.json")
    @SqlScript(scripts = "/scripts/TrackPositionsPlaylistsCurrentTracklistIT/common_test_data_set_1.sql")
    public void getPublicTrackPositionsPlaylists_apple() {
        MockCurrentDateService.setMockDateTime(LocalDateTime.parse("2023-02-13T00:00:00"));
        String applePlaylistId = "apple_pl.59c4071e8048473683da7fcf0e903c79";
        String duplicateIsrc = "IT00D9400208";

        ResponseEntity<PlaylistsTrackPositionsCurrentTracklists> response = apiClient.sendAPIRequest(
            "/v3/public/track-positions/playlists/current-tracklist?"
                + "playlist_id=" + applePlaylistId
                + "&include=" + String.format("%s", IncludeTracklistPlaylist.NUM_TRACKS.getValue())
                + "&include=" + String.format("%s", IncludeTracklistPlaylist.PLAYLIST_DATES.getValue())
                + "&include=" + String.format("%s", IncludeTracklistPlaylist.TRACK_INFO.getValue()),
            PlaylistsTrackPositionsCurrentTracklists.class
        );

        List<PlaylistsTrackPositionsCurrentTracklist> trackPositions = response.getBody().getItems();
        List<PlaylistsTrackPositionsCurrentTracklist> duplicatedTrackPositions = trackPositions.stream()
            .filter(p -> duplicateIsrc.equals(p.getIsrc()))
            .sorted(Comparator.comparing(PlaylistsTrackPositionsCurrentTracklist::getCurrent))
            .collect(Collectors.toList());

        assertEquals(HttpStatus.OK, response.getStatusCode());
        assertEquals(4, response.getBody().getCount());
        assertEquals(2, duplicatedTrackPositions.size());
        assertTrue(duplicatedTrackPositions.get(0).getIsTopTrackForIsrc());
        assertFalse(duplicatedTrackPositions.get(1).getIsTopTrackForIsrc());
        assertTrue(trackPositions.stream().anyMatch(p -> p.getAppleTrack() != null));
        assertTrue(trackPositions.stream()
            .allMatch(p ->
                Stream.of(
                    p.getEarliestPosition(),
                    p.getEarliestPositionDateTime(),
                    p.getLastAddedDateTime(),
                    p.getCurrent(),
                    p.getNumTracks(),
                    p.getPlaylistDates(),
                    p.getIsrc(),
                    p.getTrackId(),
                    p.getNumDaysOn()
                ).allMatch(Objects::nonNull)
            ));
    }

    /**
     * Test checks how SpotifyAPI-retrieved track position is populated using its BT-retrieved counterpart, depending on fields like 'reportDate',
     * 'previousPosition', 'previousPositionDateTime'.
     * <p>
     * Main fields under test: trendChange14Days, previousPositionChange14Days, previousPositionDateTime
     * <p>
     * also - getLastDateChange14Days, earliestPosition
     * <p>
     * THIS TEST ASSUMES that position item from Spotify-API and from BT has EQUAL 'current_position' (so data in BT is considered 'fresh')
     * another scenarios set is tested separately
     */
    @Test
    @BigTable(table = "playlist_tracks", json =
        "/scripts/TrackPositionsPlaylistsCurrentTracklistIT/BT_playlist_tracks_getPublicTrackPositionsPlaylistsCurrentTracklist_spotifyAPITrackPositionEnrichment_dependingOnBTPositionData_currentPositionsMatched.json")
    @SqlScript(scripts = "/scripts/TrackPositionsPlaylistsCurrentTracklistIT/common_test_data_set_1.sql")
    public void getPublicTrackPositionsPlaylistsCurrentTracklist_spotifyAPITrackPositionEnrichment_dependingOnBTPositionData_currentPositionsMatched() {
        MockCurrentDateService.setMockDateTime(LocalDateTime.parse("2023-02-13T00:00:00"));
        LocalDate dateCurrent = DateUtils.getCurrentDate();

        String spotifyPlaylistId = "spotify_spotifyApiNotLocal1";

        //track1:
        //yesterday curent=10, prev=null
        //today current=0, prev = 10
        String isrc1 = "TCAFS2164573";
        String spotifyTrackId1 = "0XMJSXPBD1vsYCSXBSbMQ9";
        String spotifyTrackName1 = "spotifyTrackName1";
        LocalDateTime previousPositionDateTime1 = dateCurrent.minusDays(1).atStartOfDay();
        int currentPosition1Yesterday = 10;
        int currentPosition1Today = 0;
        int earliestPosition1 = 10;
        int spotifyCurrentPosition1 = currentPosition1Today;

        //track2:
        //today current=1, prev = null
        String isrc2 = "FR96X2215071";
        String spotifyTrackId2 = "6lJneQb8Hz81Gz21XhJiFE";
        String spotifyTrackName2 = "spotifyTrackName2";
        int currentPosition2Today = 1;
        int earliestPosition2 = 1;
        int spotifyCurrentPosition2 = currentPosition2Today;

        //track3:
        //monthAgo current=12, prev=null
        //today current=2, prev = 12
        String isrc3 = "ARIXB2105450";
        String spotifyTrackId3 = "79ea6axogIGzCvJIW6KsPM";
        String spotifyTrackName3 = "spotifyTrackName3";
        LocalDateTime previousPositionDateTime3 = dateCurrent.minusDays(30).atStartOfDay();
        int currentPosition3Today = 2;
        int earliestPosition3 = 12;
        int spotifyCurrentPosition3 = currentPosition3Today;

        //mock spotify api
        List<SpotifyApiPlaylistTrackDto> spotifyTracksOrderedByCurrentPos = new ArrayList<>();
        spotifyTracksOrderedByCurrentPos.add(spotifyCurrentPosition1, SpotifyApiPlaylistTrackDto.builder()
            .trackId(spotifyTrackId1)
            .trackName(spotifyTrackName1)
            .isrc(isrc1)
            .build());

        spotifyTracksOrderedByCurrentPos.add(spotifyCurrentPosition2, SpotifyApiPlaylistTrackDto.builder()
            .trackId(spotifyTrackId2)
            .trackName(spotifyTrackName2)
            .isrc(isrc2)
            .build());

        spotifyTracksOrderedByCurrentPos.add(spotifyCurrentPosition3, SpotifyApiPlaylistTrackDto.builder()
            .trackId(spotifyTrackId3)
            .trackName(spotifyTrackName3)
            .isrc(isrc3)
            .build());

        mockSpotifyApiExecutorPositionsOrderedByIndex(spotifyPlaylistId, spotifyTracksOrderedByCurrentPos);

        ResponseEntity<PlaylistsTrackPositionsCurrentTracklists> response = apiClient.sendAPIRequest(
            "/v3/public/track-positions/playlists/current-tracklist?"
                + "playlist_id=" + spotifyPlaylistId,
            PlaylistsTrackPositionsCurrentTracklists.class
        );

        List<PlaylistsTrackPositionsCurrentTracklist> trackPositions = response.getBody().getItems();

        //position 1
        PlaylistsTrackPositionsCurrentTracklist position1 = trackPositions.stream()
            .filter(p -> isrc1.equals(p.getIsrc())).findAny().get();

        assertEquals(isrc1, position1.getIsrc());
        assertEquals(spotifyTrackId1, position1.getTrackId());
        assertEquals(currentPosition1Today, position1.getCurrent());
        assertEquals(earliestPosition1, position1.getEarliestPosition());

        //14days trend and prev position are NOT null since position's 'current' changed regarding yesterday value
        assertEquals(currentPosition1Today - currentPosition1Yesterday, position1.getTrendChange14Days());
        assertEquals(currentPosition1Yesterday, position1.getPreviousPositionChange14Days());

        assertEquals(previousPositionDateTime1, position1.getPreviousPositionDateTime());
        assertEquals(previousPositionDateTime1.toLocalDate(), position1.getLastDateChange14Days());

        //position 2
        PlaylistsTrackPositionsCurrentTracklist position2 = trackPositions.stream()
            .filter(p -> isrc2.equals(p.getIsrc())).findAny().get();

        assertEquals(isrc2, position2.getIsrc());
        assertEquals(spotifyTrackId2, position2.getTrackId());
        assertEquals(currentPosition2Today, position2.getCurrent());
        assertEquals(earliestPosition2, position2.getEarliestPosition());

        //14days trend and prev position are null since position hasn't changed yet ('previous' is null)
        assertEquals(null, position2.getTrendChange14Days());
        assertEquals(null, position2.getPreviousPositionChange14Days());
        //previousPositionDateTime is null since position hasn't changed yet ('previous' is null)
        assertEquals(null, position2.getPreviousPositionDateTime());
        assertEquals(null, position2.getLastDateChange14Days());

        //position 3
        PlaylistsTrackPositionsCurrentTracklist position3 = trackPositions.stream()
            .filter(p -> isrc3.equals(p.getIsrc())).findAny().get();

        assertEquals(isrc3, position3.getIsrc());
        assertEquals(spotifyTrackId3, position3.getTrackId());
        assertEquals(currentPosition3Today, position3.getCurrent());
        assertEquals(earliestPosition3, position3.getEarliestPosition());

        //14days trend and prev position are null since position changed before 14days range
        assertEquals(null, position3.getTrendChange14Days());
        assertEquals(null, position3.getPreviousPositionChange14Days());

        //previousPositionDateTime is NOT null as its value is actually known and is not limited tp 14 days
        assertEquals(previousPositionDateTime3, position3.getPreviousPositionDateTime());
        //for spotify API enrichment lastDateChange14Days is set same as previousPositionDateTime
        assertEquals(null, position3.getLastDateChange14Days());
    }

    /**
     * Test checks how SpotifyAPI-retrieved track position is populated using its BT-retrieved counterpart, depending on fields like 'reportDate',
     * 'previousPosition', 'previousPositionDateTime'.
     * <p>
     * Main fields under test: trendChange14Days, previousPositionChange14Days, previousPositionDateTime
     * <p>
     * also - getLastDateChange14Days, earliestPosition
     * <p>
     * THIS TEST ASSUMES that position item from Spotify-API and from BT has NON-EQUAL 'current_position' (so data in BT is NOT considered
     * 'fresh') - another scenarios set is tested separately
     * <p>
     * THIS TEST ASSUMES that BT item's 'report_date' is BEFORE today
     */
    @Test
    @BigTable(table = "playlist_tracks", json =
        "/scripts/TrackPositionsPlaylistsCurrentTracklistIT/BT_playlist_tracks_getCurTrcklst_spotifyAPITrackPositionEnrichment_dependingOnBTPositionData_currentPositionsNotMatched_reportDateBeforeToday.json")
    @SqlScript(scripts = "/scripts/TrackPositionsPlaylistsCurrentTracklistIT/common_test_data_set_1.sql")
    public void getCurTrcklst_spotifyAPITrackPositionEnrichment_dependingOnBTPositionData_currentPositionsNotMatched_reportDateBeforeToday() {
        MockCurrentDateService.setMockDateTime(LocalDateTime.parse("2023-02-13T00:00:00"));
        LocalDate dateCurrent = DateUtils.getCurrentDate();
        LocalDate btReportDate = dateCurrent.minusDays(1);

        String spotifyPlaylistId = "spotify_spotifyApiNotLocal1";

        //track1:
        //twoDaysBefore curent=10, prev=null
        //yesterday current=0, prev = 10
        String isrc1 = "TCAFS2164573";
        String spotifyTrackId1 = "0XMJSXPBD1vsYCSXBSbMQ9";
        String spotifyTrackName1 = "spotifyTrackName1";
        LocalDateTime previousPositionDateTime1 = btReportDate.atStartOfDay();
        int currentPosition1Yesterday = 0;
        int earliestPosition1 = 10;
        int spotifyCurrentPosition1 = currentPosition1Yesterday + 2; //e.g. in Spotify API position changed to +2

        //track2:
        //yesterday current=1, prev = null
        String isrc2 = "FR96X2215071";
        String spotifyTrackId2 = "6lJneQb8Hz81Gz21XhJiFE";
        String spotifyTrackName2 = "spotifyTrackName2";
        LocalDateTime previousPositionDateTime2 = btReportDate.atStartOfDay();
        int currentPosition2Yesterday = 1;
        int earliestPosition2 = 1;
        int spotifyCurrentPosition2 = currentPosition2Yesterday + 2; //e.g. in Spotify API position changed to +2

        //mock spotify api
        List<SpotifyApiPlaylistTrackDto> spotifyTracksOrderedByCurrentPos = new ArrayList<>();
        //create 2 dummy records to occupy list positions 0 and 1 and simulate real tracks 'current' position change
        spotifyTracksOrderedByCurrentPos.add(0, SpotifyApiPlaylistTrackDto.builder()
            .trackId("dummy1")
            .trackName("dummy1")
            .isrc("US0000100000")
            .build());
        spotifyTracksOrderedByCurrentPos.add(1, SpotifyApiPlaylistTrackDto.builder()
            .trackId("dummy2")
            .trackName("dummy2")
            .isrc("US0000200000")
            .build());

        spotifyTracksOrderedByCurrentPos.add(spotifyCurrentPosition1, SpotifyApiPlaylistTrackDto.builder()
            .trackId(spotifyTrackId1)
            .trackName(spotifyTrackName1)
            .isrc(isrc1)
            .build());

        spotifyTracksOrderedByCurrentPos.add(spotifyCurrentPosition2, SpotifyApiPlaylistTrackDto.builder()
            .trackId(spotifyTrackId2)
            .trackName(spotifyTrackName2)
            .isrc(isrc2)
            .build());

        mockSpotifyApiExecutorPositionsOrderedByIndex(spotifyPlaylistId, spotifyTracksOrderedByCurrentPos);

        ResponseEntity<PlaylistsTrackPositionsCurrentTracklists> response = apiClient.sendAPIRequest(
            "/v3/public/track-positions/playlists/current-tracklist?"
                + "playlist_id=" + spotifyPlaylistId,
            PlaylistsTrackPositionsCurrentTracklists.class
        );

        List<PlaylistsTrackPositionsCurrentTracklist> trackPositions = response.getBody().getItems();

        //position 1
        PlaylistsTrackPositionsCurrentTracklist position1 = trackPositions.stream()
            .filter(p -> isrc1.equals(p.getIsrc())).findAny().get();

        assertEquals(isrc1, position1.getIsrc());
        assertEquals(spotifyTrackId1, position1.getTrackId());
        //'current' position is taken from Spotify API item
        assertEquals(spotifyCurrentPosition1, position1.getCurrent());
        assertEquals(earliestPosition1, position1.getEarliestPosition());

        //14days trend and prev position are set using 'current' pos of BT record since BT record is of yesterday's date and
        //in Spotify API there already is today's data available and current pos changed from yesterday one
        assertEquals(spotifyCurrentPosition1 - currentPosition1Yesterday, position1.getTrendChange14Days());
        assertEquals(currentPosition1Yesterday, position1.getPreviousPositionChange14Days());
        //previousPositionDateTime is set to yesterday's date because now yesterday is considered to be day of position change
        assertEquals(previousPositionDateTime1, position1.getPreviousPositionDateTime());
        assertEquals(previousPositionDateTime1.toLocalDate(), position1.getLastDateChange14Days());

        //position 2
        PlaylistsTrackPositionsCurrentTracklist position2 = trackPositions.stream()
            .filter(p -> isrc2.equals(p.getIsrc())).findAny().get();

        assertEquals(isrc2, position2.getIsrc());
        assertEquals(spotifyTrackId2, position2.getTrackId());
        //'current' position is taken from Spotify API item
        assertEquals(spotifyCurrentPosition2, position2.getCurrent());
        assertEquals(earliestPosition2, position2.getEarliestPosition());

        //despite 'latest' BT record doesn't have 'previous' position set - there now IS 'previous' position since BT record
        // is of yesterday's date and in Spotify API there already is today's data available and current pos changed from yesterday one
        //so 14days trend and prev position are set using 'current' pos of 'latest' BT record
        assertEquals(spotifyCurrentPosition1 - currentPosition1Yesterday, position1.getTrendChange14Days());
        assertEquals(currentPosition1Yesterday, position1.getPreviousPositionChange14Days());
        //previousPositionDateTime is set to yesterday's date because now yesterday is considered to be day of position change
        assertEquals(previousPositionDateTime2, position1.getPreviousPositionDateTime());
        assertEquals(previousPositionDateTime2.toLocalDate(), position1.getLastDateChange14Days());
    }

    /**
     * Test checks how SpotifyAPI-retrieved track position is populated using its BT-retrieved counterpart, depending on fields like 'reportDate',
     * 'previousPosition', 'previousPositionDateTime'.
     * <p>
     * Main fields under test: trendChange14Days, previousPositionChange14Days, previousPositionDateTime
     * <p>
     * also - getLastDateChange14Days, earliestPosition
     * <p>
     * THIS TEST ASSUMES that position item from Spotify-API and from BT has NON-EQUAL 'current_position' (so data in BT is NOT considered
     * 'fresh') - another scenarios set is tested separately
     * <p>
     * THIS TEST ASSUMES that BT item's 'report_date' is EQUAL today
     */
    @Test
    @BigTable(table = "playlist_tracks", json =
        "/scripts/TrackPositionsPlaylistsCurrentTracklistIT/BT_playlist_tracks_getCurTrcklst_spotifyAPITrackPositionEnrichment_dependingOnBTPositionData_currentPositionsNotMatched_reportDateIsToday.json")
    @SqlScript(scripts = "/scripts/TrackPositionsPlaylistsCurrentTracklistIT/common_test_data_set_1.sql")
    public void getCurTrcklst_spotifyAPITrackPositionEnrichment_dependingOnBTPositionData_currentPositionsNotMatched_reportDateIsToday() {
        MockCurrentDateService.setMockDateTime(LocalDateTime.parse("2023-02-13T00:00:00"));
        LocalDate dateCurrent = DateUtils.getCurrentDate();
        LocalDate btReportDate = dateCurrent;
        String spotifyPlaylistId = "spotify_spotifyApiNotLocal1";

        //track1:
        //yesterday curent=10, prev=null
        //today current=0, prev = 10
        String isrc1 = "TCAFS2164573";
        String spotifyTrackId1 = "0XMJSXPBD1vsYCSXBSbMQ9";
        String spotifyTrackName1 = "spotifyTrackName1";
        LocalDateTime previousPositionDateTime1 = btReportDate.minusDays(1).atStartOfDay(); //yesterday's date
        int currentPosition1Yesterday = 10;
        int currentPosition1Today = 0;
        int earliestPosition1 = 10;
        int spotifyCurrentPosition1 = currentPosition1Today + 2; //e.g. in Spotify API position changed to +2

        //track2:
        //yesterday current=1, prev = null
        String isrc2 = "FR96X2215071";
        String spotifyTrackId2 = "6lJneQb8Hz81Gz21XhJiFE";
        String spotifyTrackName2 = "spotifyTrackName2";
        int currentPosition2Yesterday = 1;
        int earliestPosition2 = 1;
        int spotifyCurrentPosition2 = currentPosition2Yesterday + 2; //e.g. in Spotify API position changed to +2

        //mock spotify api
        List<SpotifyApiPlaylistTrackDto> spotifyTracksOrderedByCurrentPos = new ArrayList<>();
        //create 2 dummy records to occupy list positions 0 and 1 and simulate real tracks 'current' position change
        spotifyTracksOrderedByCurrentPos.add(0, SpotifyApiPlaylistTrackDto.builder()
            .trackId("dummy1")
            .trackName("dummy1")
            .isrc("US0000100000")
            .build());
        spotifyTracksOrderedByCurrentPos.add(1, SpotifyApiPlaylistTrackDto.builder()
            .trackId("dummy2")
            .trackName("dummy2")
            .isrc("US0000200000")
            .build());

        spotifyTracksOrderedByCurrentPos.add(spotifyCurrentPosition1, SpotifyApiPlaylistTrackDto.builder()
            .trackId(spotifyTrackId1)
            .trackName(spotifyTrackName1)
            .isrc(isrc1)
            .build());

        spotifyTracksOrderedByCurrentPos.add(spotifyCurrentPosition2, SpotifyApiPlaylistTrackDto.builder()
            .trackId(spotifyTrackId2)
            .trackName(spotifyTrackName2)
            .isrc(isrc2)
            .build());

        mockSpotifyApiExecutorPositionsOrderedByIndex(spotifyPlaylistId, spotifyTracksOrderedByCurrentPos);

        ResponseEntity<PlaylistsTrackPositionsCurrentTracklists> response = apiClient.sendAPIRequest(
            "/v3/public/track-positions/playlists/current-tracklist?"
                + "playlist_id=" + spotifyPlaylistId,
            PlaylistsTrackPositionsCurrentTracklists.class
        );

        List<PlaylistsTrackPositionsCurrentTracklist> trackPositions = response.getBody().getItems();

        //position 1
        PlaylistsTrackPositionsCurrentTracklist position1 = trackPositions.stream()
            .filter(p -> isrc1.equals(p.getIsrc())).findAny().get();

        assertEquals(isrc1, position1.getIsrc());
        assertEquals(spotifyTrackId1, position1.getTrackId());
        //'current' position is taken from Spotify API item
        assertEquals(spotifyCurrentPosition1, position1.getCurrent());
        assertEquals(earliestPosition1, position1.getEarliestPosition());

        //14days trend and prev position are set using 'current' pos of 'second-latest' (yesterday) BT record since though 'latest' BT record is of today's date -
        //in Spotify API there already is more fresh today's data available and current pos changed from BT today's one
        assertEquals(spotifyCurrentPosition1 - currentPosition1Yesterday, position1.getTrendChange14Days());
        assertEquals(currentPosition1Yesterday, position1.getPreviousPositionChange14Days());
        //previousPositionDateTime is set to 'second-latest' (yesterday) date because now yesterday is considered to be day of position change
        assertEquals(previousPositionDateTime1, position1.getPreviousPositionDateTime());
        assertEquals(previousPositionDateTime1.toLocalDate(), position1.getLastDateChange14Days());

        //position 2
        PlaylistsTrackPositionsCurrentTracklist position2 = trackPositions.stream()
            .filter(p -> isrc2.equals(p.getIsrc())).findAny().get();

        assertEquals(isrc2, position2.getIsrc());
        assertEquals(spotifyTrackId2, position2.getTrackId());
        //'current' position is taken from Spotify API item
        assertEquals(spotifyCurrentPosition2, position2.getCurrent());
        assertEquals(earliestPosition2, position2.getEarliestPosition());

        //14days trend and prev position are null because there is no 'second-latest' BT record found (within 14 days or possible at all).
        //Though 'latest' BT record is of today's date - in Spotify API there already is more fresh today's data available and current pos changed
        //from BT today's one (so BT today's 'current' has invalid value and cannot be used)
        assertEquals(null, position2.getTrendChange14Days());
        assertEquals(null, position2.getPreviousPositionChange14Days());
        //previousPositionDateTime is null in any case, even if position had in fact changed at some point e.g. 2 month ago - ideally in this case
        //we would like to return it (since this field is not 14-days scoped) but finding this data would be too problematic
        assertEquals(null, position2.getPreviousPositionDateTime());
        assertEquals(null, position2.getLastDateChange14Days());
    }

    /**
     * Position's list index will be equal to its 'current position' field
     */
    private void mockSpotifyApiExecutorPositionsOrderedByIndex(
        String playlistId,
        List<SpotifyApiPlaylistTrackDto> tracks
    ) {
        String playlistIdUnprefixed = ModelUtils.removeDspPrefixFromPlaylistId(playlistId);

        Mockito.when(spotifyAPIRequestExecutor.getPlaylistsItems(playlistIdUnprefixed)).thenReturn(
            tracks.stream().map(t -> new PlaylistTrack.Builder()
                .setTrack(new se.michaelthelin.spotify.model_objects.specification.Track.Builder()
                    .setType(ModelObjectType.TRACK)
                    .setId(t.getTrackId())
                    .setName(t.getTrackName())
                    .setExternalIds(new ExternalId.Builder()
                        .setExternalIds(Map.of("isrc", t.getIsrc()))
                        .build())
                    .setArtists(new ArtistSimplified[0])
                    .setAlbum(new AlbumSimplified.Builder()
                        .setImages(new Image[0])
                        .build())
                    .build())
                .build()

            ).collect(Collectors.toList())
        );
    }
}
