/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.streams.youtube.provider;

import org.apache.streams.core.StreamsDatum;
import org.apache.streams.google.gplus.configuration.UserInfo;
import org.apache.streams.local.queues.ThroughputQueue;
import org.apache.streams.util.api.requests.backoff.impl.ExponentialBackOffStrategy;
import org.apache.streams.youtube.YoutubeConfiguration;

import com.google.api.services.youtube.YouTube;
import com.google.api.services.youtube.model.Activity;
import com.google.api.services.youtube.model.ActivityContentDetails;
import com.google.api.services.youtube.model.ActivityContentDetailsUpload;
import com.google.api.services.youtube.model.ActivityListResponse;
import com.google.api.services.youtube.model.Video;
import com.google.api.services.youtube.model.VideoListResponse;
import com.google.api.services.youtube.model.VideoSnippet;
import org.joda.time.DateTime;
import org.junit.Before;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.BlockingQueue;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.anyLong;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

/**
 * Test for YoutubeUserActivityCollector.
 */
public class YoutubeUserActivityCollectorTest {
  private static final String USER_ID = "fake_user_id";
  private static final String IN_RANGE_IDENTIFIER = "data in range";
  private YoutubeConfiguration config;

  private static VideoListResponse createMockVideoListResponse(int numBefore, int numAfter, int numInRange, DateTime after, DateTime before, boolean page) {
    VideoListResponse feed = new VideoListResponse();
    List<Video> list = new LinkedList<>();

    for (int i = 0; i < numAfter; ++i) {
      com.google.api.client.util.DateTime published = new com.google.api.client.util.DateTime(after.getMillis() + 1000000);
      Video video = new Video();
      video.setSnippet(new VideoSnippet());
      video.getSnippet().setPublishedAt(published);
      list.add(video);
    }
    for (int i = 0; i < numInRange; ++i) {
      DateTime published;
      if ((before == null && after == null) || before == null) {
        published = DateTime.now(); // no date range or end time date range so just make the time now.
      } else if (after == null) {
        published = before.minusMillis(100000); //no beginning to range
      } else { // has to be in range
        long range = before.getMillis() - after.getMillis();
        published = after.plus(range / 2); //in the middle
      }
      com.google.api.client.util.DateTime ytPublished = new com.google.api.client.util.DateTime(published.getMillis());
      Video video = new Video();
      video.setSnippet(new VideoSnippet());
      video.getSnippet().setPublishedAt(ytPublished);
      video.getSnippet().setTitle(IN_RANGE_IDENTIFIER);
      list.add(video);
    }
    for (int i = 0; i < numBefore; ++i) {
      com.google.api.client.util.DateTime published = new com.google.api.client.util.DateTime((after.minusMillis(100000)).getMillis());
      Video video = new Video();
      video.setSnippet(new VideoSnippet());
      video.getSnippet().setPublishedAt(published);
      list.add(video);
    }
    if (page) {
      feed.setNextPageToken("A");
    } else {
      feed.setNextPageToken(null);
    }

    feed.setItems(list);

    return feed;
  }

  @Before
  public void setup() {
    this.config = new YoutubeConfiguration();
    this.config.setApiKey("API_KEY");
  }

  @Test
  public void testGetVideos() throws IOException {
    DateTime now = new DateTime(System.currentTimeMillis());
    YouTube youtube = buildYouTube(0, 1, 0, now, now.minus(10000));

    BlockingQueue<StreamsDatum> datumQueue = new ThroughputQueue<>();
    YoutubeUserActivityCollector collector = new YoutubeUserActivityCollector(youtube, datumQueue, new ExponentialBackOffStrategy(2), new UserInfo().withUserId(USER_ID), this.config);

    List<Video> video = collector.getVideoList("fake_video_id");

    assertNotNull(video.get(0));
  }

  @Test
  public void testGetVideosNull() throws IOException {
    DateTime now = new DateTime(System.currentTimeMillis());
    YouTube youtube = buildYouTube(0, 0, 0, now.plus(10000), now.minus(10000));

    BlockingQueue<StreamsDatum> datumQueue = new ThroughputQueue<>();
    YoutubeUserActivityCollector collector = new YoutubeUserActivityCollector(youtube, datumQueue, new ExponentialBackOffStrategy(2), new UserInfo().withUserId(USER_ID), this.config);

    List<Video> video = collector.getVideoList("fake_video_id");

    assertEquals(video.size(), 0);
  }

  @Test
  public void testProcessActivityFeed() throws IOException, InterruptedException {
    DateTime now = new DateTime(System.currentTimeMillis());
    YouTube youtube = buildYouTube(0, 0, 5, now.plus(3000000), now.minus(1000000));

    BlockingQueue<StreamsDatum> datumQueue = new ThroughputQueue<>();
    YoutubeUserActivityCollector collector = new YoutubeUserActivityCollector(youtube, datumQueue, new ExponentialBackOffStrategy(2), new UserInfo().withUserId(USER_ID), this.config);

    ActivityListResponse feed = buildActivityListResponse(1);

    collector.processActivityFeed(feed, new DateTime(System.currentTimeMillis()), null);

    assertEquals(collector.getDatumQueue().size(), 5);
  }

  @Test
  public void testProcessActivityFeedBefore() throws IOException, InterruptedException {
    DateTime now = new DateTime(System.currentTimeMillis());
    YouTube youtube = buildYouTube(5, 0, 0, now, now);

    BlockingQueue<StreamsDatum> datumQueue = new ThroughputQueue<>();
    YoutubeUserActivityCollector collector = new YoutubeUserActivityCollector(youtube, datumQueue, new ExponentialBackOffStrategy(2), new UserInfo().withUserId(USER_ID), this.config);

    ActivityListResponse feed = buildActivityListResponse(1);

    collector.processActivityFeed(feed, new DateTime(System.currentTimeMillis()), null);

    assertEquals(collector.getDatumQueue().size(), 0);
  }

  @Test
  public void testProcessActivityFeedAfter() throws IOException, InterruptedException {
    DateTime now = new DateTime(System.currentTimeMillis());
    YouTube youtube = buildYouTube(0, 5, 0, now, now);

    BlockingQueue<StreamsDatum> datumQueue = new ThroughputQueue<>();
    YoutubeUserActivityCollector collector = new YoutubeUserActivityCollector(youtube, datumQueue, new ExponentialBackOffStrategy(2), new UserInfo().withUserId(USER_ID), this.config);

    ActivityListResponse feed = buildActivityListResponse(1);

    collector.processActivityFeed(feed, new DateTime(now.getMillis() - 100000), null);

    assertEquals(collector.getDatumQueue().size(), 5);
  }

  @Test
  public void testProcessActivityFeedMismatchCount() throws IOException, InterruptedException {
    DateTime now = new DateTime(System.currentTimeMillis());
    YouTube youtube = buildYouTube(5, 5, 5, now, now.minus(100000));

    BlockingQueue<StreamsDatum> datumQueue = new ThroughputQueue<>();
    YoutubeUserActivityCollector collector = new YoutubeUserActivityCollector(youtube, datumQueue, new ExponentialBackOffStrategy(2), new UserInfo().withUserId(USER_ID), this.config);

    ActivityListResponse feed = buildActivityListResponse(1);

    collector.processActivityFeed(feed, new DateTime(now), null);

    assertEquals(collector.getDatumQueue().size(), 5);
  }

  @Test
  public void testProcessActivityFeedMismatchCountInRange() throws IOException, InterruptedException {
    DateTime now = new DateTime(System.currentTimeMillis());
    YouTube youtube = buildYouTube(5, 5, 5, now, now.minus(100000));

    BlockingQueue<StreamsDatum> datumQueue = new ThroughputQueue<>();
    YoutubeUserActivityCollector collector = new YoutubeUserActivityCollector(youtube, datumQueue, new ExponentialBackOffStrategy(2), new UserInfo().withUserId(USER_ID), this.config);

    ActivityListResponse feed = buildActivityListResponse(1);

    collector.processActivityFeed(feed, new DateTime(now), new DateTime(now).minus(100000));

    assertEquals(collector.getDatumQueue().size(), 5);
  }

  private ActivityListResponse buildActivityListResponse(int num) {
    ActivityListResponse activityListResponse = new ActivityListResponse();
    List<Activity> items = new ArrayList<>();

    for ( int x = 0; x < num; x++ ) {
      Activity activity = new Activity();

      ActivityContentDetails contentDetails = new ActivityContentDetails();
      ActivityContentDetailsUpload upload = new ActivityContentDetailsUpload();
      upload.setVideoId("video_id_" + x);
      contentDetails.setUpload(upload);

      activity.setId("id_" + x);
      activity.setContentDetails(contentDetails);

      items.add(activity);
    }

    activityListResponse.setItems(items);

    return activityListResponse;
  }

  private YouTube buildYouTube(int numBeforeRange, int numAfterRange, int numInRange, DateTime afterDate, DateTime beforeDate) {

    return createYoutubeMock(numBeforeRange, numAfterRange, numInRange, afterDate, beforeDate);

  }

  private YouTube createYoutubeMock(int numBefore, int numAfter, int numInRange, DateTime after, DateTime before) {
    YouTube youtube = mock(YouTube.class);

    final YouTube.Videos videos = createMockVideos(numBefore, numAfter, numInRange, after, before);
    doAnswer(invocationOnMock -> videos).when(youtube).videos();

    return youtube;
  }

  private YouTube.Videos createMockVideos(int numBefore, int numAfter, int numInRange, DateTime after, DateTime before) {
    YouTube.Videos videos = mock(YouTube.Videos.class);

    try {
      YouTube.Videos.List list = createMockVideosList(numBefore, numAfter, numInRange, after, before);
      when(videos.list(anyString())).thenReturn(list);
    } catch (IOException ex) {
      fail("Exception thrown while creating mock");
    }

    return videos;
  }

  private YouTube.Videos.List createMockVideosList(int numBefore, int numAfter, int numInRange, DateTime after, DateTime before) {
    YouTube.Videos.List list = mock(YouTube.Videos.List.class);

    when(list.setMaxResults(anyLong())).thenReturn(list);
    when(list.setPageToken(anyString())).thenReturn(list);
    when(list.setId(anyString())).thenReturn(list);
    when(list.setKey(anyString())).thenReturn(list);

    VideoListResponseAnswer answer = new VideoListResponseAnswer(numBefore, numAfter, numInRange, after, before);
    try {
      doAnswer(answer).when(list).execute();
    } catch (IOException ioe) {
      fail("Should not have thrown exception while creating mock. : " + ioe.getMessage());
    }

    return list;
  }

  private static class VideoListResponseAnswer implements Answer<VideoListResponse> {
    private int afterCount = 0;
    private int beforeCount = 0;
    private int inCount = 0;
    private int maxBatch = 100;

    private int numAfter;
    private int numInRange;
    private int numBefore;
    private DateTime after;
    private DateTime before;

    private VideoListResponseAnswer(int numBefore, int numAfter, int numInRange, DateTime after, DateTime before) {
      this.numBefore = numBefore;
      this.numAfter = numAfter;
      this.numInRange = numInRange;
      this.after = after;
      this.before = before;
    }

    @Override
    public VideoListResponse answer(InvocationOnMock invocationOnMock) throws Throwable {
      int totalCount = 0;
      int batchAfter = 0;
      int batchBefore = 0;
      int batchIn = 0;
      inCount = 0;
      afterCount = 0;
      beforeCount = 0;

      if (afterCount != numAfter) {
        if (numAfter - afterCount >= maxBatch) {
          afterCount += maxBatch;
          batchAfter += maxBatch;
          totalCount += batchAfter;
        } else {
          batchAfter += numAfter - afterCount;
          totalCount += numAfter - afterCount;
          afterCount = numAfter;
        }
      }
      if (totalCount < maxBatch && inCount != numInRange) {
        if (numInRange - inCount >= maxBatch - totalCount) {
          inCount += maxBatch - totalCount;
          batchIn += maxBatch - totalCount;
          totalCount += batchIn;
        } else {
          batchIn += numInRange - inCount;
          totalCount += numInRange - inCount;
          inCount = numInRange;
        }
      }
      if (totalCount < maxBatch && beforeCount != numBefore) {
        if (numBefore - batchBefore >= maxBatch - totalCount) {
          batchBefore += maxBatch - totalCount;
          totalCount = maxBatch;
          beforeCount += batchBefore;
        } else {
          batchBefore += numBefore - beforeCount;
          totalCount += numBefore - beforeCount;
          beforeCount = numBefore;
        }
      }

      return createMockVideoListResponse(batchBefore, batchAfter, batchIn, after, before, numAfter != afterCount || inCount != numInRange || beforeCount != numBefore);
    }
  }
}
