/*
 * Copyright (c) 2018 LingoChamp Inc.
 *
 * Licensed 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 com.liulishuo.filedownloader;

import android.os.Handler;

import com.liulishuo.filedownloader.exception.FileDownloadNetworkPolicyException;
import com.liulishuo.filedownloader.exception.FileDownloadOutOfSpaceException;
import com.liulishuo.filedownloader.exception.FileDownloadSecurityException;
import com.liulishuo.filedownloader.progress.ProgressAssist;
import com.liulishuo.filedownloader.retry.RetryAssist;
import com.liulishuo.okdownload.DownloadTask;
import com.liulishuo.okdownload.SpeedCalculator;
import com.liulishuo.okdownload.core.cause.EndCause;
import com.liulishuo.okdownload.core.exception.DownloadSecurityException;
import com.liulishuo.okdownload.core.exception.NetworkPolicyException;
import com.liulishuo.okdownload.core.exception.PreAllocateException;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;

import java.util.ArrayList;
import java.util.List;

import static org.assertj.core.api.Java6Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;


@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class CompatListenerAssistTest {

    @Mock
    private CompatListenerAssist.CompatListenerAssistCallback callback;
    @Mock
    private Handler uiHander;
    private CompatListenerAssist compatListenerAssist;

    @Before
    public void setup() {
        initMocks(this);

        compatListenerAssist = spy(new CompatListenerAssist(callback, uiHander));
    }

    @Test
    public void taskStart() {
        final DownloadTask mockDownloadTask = mock(DownloadTask.class);
        final DownloadTaskAdapter mockTaskAdapter = mock(DownloadTaskAdapter.class);
        when(mockDownloadTask.getTag(DownloadTaskAdapter.KEY_TASK_ADAPTER)).thenReturn(null);

        compatListenerAssist.taskStart(mockDownloadTask);
        verify(callback, never()).pending(any(DownloadTaskAdapter.class), anyLong(), anyLong());
        verify(callback, never()).started(any(DownloadTaskAdapter.class));

        when(mockDownloadTask.getTag(DownloadTaskAdapter.KEY_TASK_ADAPTER))
                .thenReturn(mockTaskAdapter);
        when(mockTaskAdapter.getSoFarBytesInLong()).thenReturn(1L);
        when(mockTaskAdapter.getTotalBytesInLong()).thenReturn(2L);

        compatListenerAssist.taskStart(mockDownloadTask);

        verify(callback).pending(mockTaskAdapter, 1L, 2L);
        verify(callback).started(mockTaskAdapter);
    }

    @Test
    public void connectStart() {
        final DownloadTask mockTask = mock(DownloadTask.class);
        final DownloadTaskAdapter mockTaskAdapter = mock(DownloadTaskAdapter.class);
        final ProgressAssist mockProgressAssist = mock(ProgressAssist.class);
        when(mockTask.getTag(DownloadTaskAdapter.KEY_TASK_ADAPTER)).thenReturn(null);
        when(mockTaskAdapter.getProgressAssist()).thenReturn(mockProgressAssist);

        assertThat(compatListenerAssist.taskConnected.get()).isFalse();

        compatListenerAssist.connectStart(mockTask);
        assertThat(compatListenerAssist.taskConnected.get()).isTrue();
        verify(mockTaskAdapter, never()).getSoFarBytesInLong();
        verify(mockTaskAdapter, never()).getTotalBytesInLong();
        verify(mockTaskAdapter, never()).getProgressAssist();
        verify(callback, never()).connected(
                any(DownloadTaskAdapter.class), anyString(), anyBoolean(), anyLong(), anyLong());

        compatListenerAssist.taskConnected.set(false);
        when(mockTask.getTag(DownloadTaskAdapter.KEY_TASK_ADAPTER)).thenReturn(mockTaskAdapter);
        when(mockTaskAdapter.getSoFarBytesInLong()).thenReturn(1L);
        when(mockTaskAdapter.getTotalBytesInLong()).thenReturn(2L);

        compatListenerAssist.connectStart(mockTask);

        verify(callback).connected(
                mockTaskAdapter,
                compatListenerAssist.getEtag(),
                compatListenerAssist.isResumable(),
                1L,
                2L);
        verify(mockProgressAssist).calculateCallbackMinIntervalBytes(2);
        verify(mockProgressAssist).initSofarBytes(1);
    }

    @Test
    public void fetchProgress() {
        final DownloadTask mockTask = mock(DownloadTask.class);
        final DownloadTaskAdapter mockTaskAdapter = mock(DownloadTaskAdapter.class);
        final ProgressAssist mockProgressAssist = mock(ProgressAssist.class);
        when(mockTask.getTag(DownloadTaskAdapter.KEY_TASK_ADAPTER)).thenReturn(null);
        when(mockTaskAdapter.getProgressAssist()).thenReturn(mockProgressAssist);

        compatListenerAssist.fetchProgress(mockTask, 5);
        verify(mockTaskAdapter, never()).getProgressAssist();
        verify(mockProgressAssist, never()).onProgress(mockTaskAdapter, 5, callback);

        when(mockTask.getTag(DownloadTaskAdapter.KEY_TASK_ADAPTER)).thenReturn(mockTaskAdapter);

        compatListenerAssist.fetchProgress(mockTask, 5);
        verify(mockProgressAssist).onProgress(mockTaskAdapter, 5, callback);
    }

    @Test
    public void taskEnd_doNothing() {
        final DownloadTask mockTask = mock(DownloadTask.class);
        when(mockTask.getTag(DownloadTaskAdapter.KEY_TASK_ADAPTER)).thenReturn(null);

        compatListenerAssist.taskEnd(mockTask, EndCause.CANCELED, null);
        verify(compatListenerAssist, never())
                .handleError(any(DownloadTaskAdapter.class), any(Exception.class));
        verify(compatListenerAssist, never())
                .handleCanceled(any(DownloadTaskAdapter.class));
        verify(compatListenerAssist, never())
                .handleWarn(
                        any(DownloadTaskAdapter.class), any(EndCause.class), any(Exception.class));
        verify(compatListenerAssist, never())
                .handleComplete(any(DownloadTaskAdapter.class));
        verify(compatListenerAssist, never()).onTaskFinish(any(DownloadTaskAdapter.class));
    }

    @Test
    public void taskEnd_handleError() {
        final DownloadTask mockTask = mock(DownloadTask.class);
        final DownloadTaskAdapter mockTaskAdapter = mock(DownloadTaskAdapter.class);
        final ProgressAssist mockProgressAssist = mock(ProgressAssist.class);
        when(mockTask.getTag(DownloadTaskAdapter.KEY_TASK_ADAPTER)).thenReturn(mockTaskAdapter);
        when(mockTaskAdapter.getProgressAssist()).thenReturn(mockProgressAssist);
        doNothing().when(compatListenerAssist).handleError(mockTaskAdapter, null);
        doNothing().when(compatListenerAssist).onTaskFinish(mockTaskAdapter);

        compatListenerAssist.taskEnd(mockTask, EndCause.PRE_ALLOCATE_FAILED, null);
        compatListenerAssist.taskEnd(mockTask, EndCause.ERROR, null);

        verify(compatListenerAssist, times(2)).handleError(mockTaskAdapter, null);
        verify(compatListenerAssist, times(2)).onTaskFinish(mockTaskAdapter);
        verify(mockProgressAssist, times(2)).clearProgress();
    }

    @Test
    public void taskEnd_handleCanceled() {
        final DownloadTask mockTask = mock(DownloadTask.class);
        final DownloadTaskAdapter mockTaskAdapter = mock(DownloadTaskAdapter.class);
        final ProgressAssist mockProgressAssist = mock(ProgressAssist.class);
        when(mockTask.getTag(DownloadTaskAdapter.KEY_TASK_ADAPTER)).thenReturn(mockTaskAdapter);
        when(mockTaskAdapter.getProgressAssist()).thenReturn(mockProgressAssist);
        doNothing().when(compatListenerAssist).handleCanceled(mockTaskAdapter);
        doNothing().when(compatListenerAssist).onTaskFinish(mockTaskAdapter);

        compatListenerAssist.taskEnd(mockTask, EndCause.CANCELED, null);

        verify(compatListenerAssist).handleCanceled(mockTaskAdapter);
        verify(compatListenerAssist).onTaskFinish(mockTaskAdapter);
        verify(mockProgressAssist).clearProgress();
    }

    @Test
    public void taskEnd_handleWarn() {
        final DownloadTask mockTask = mock(DownloadTask.class);
        final DownloadTaskAdapter mockTaskAdapter = mock(DownloadTaskAdapter.class);
        final SpeedCalculator mockSpeedCalculator = mock(SpeedCalculator.class);
        final ProgressAssist mockProgressAssist = spy(new ProgressAssist(-1, mockSpeedCalculator));
        when(mockTask.getTag(DownloadTaskAdapter.KEY_TASK_ADAPTER)).thenReturn(mockTaskAdapter);
        when(mockTaskAdapter.getProgressAssist()).thenReturn(mockProgressAssist);
        doNothing().when(compatListenerAssist).handleWarn(
                any(DownloadTaskAdapter.class), any(EndCause.class), any(Exception.class));
        doNothing().when(compatListenerAssist).onTaskFinish(mockTaskAdapter);

        compatListenerAssist.taskEnd(mockTask, EndCause.FILE_BUSY, null);
        compatListenerAssist.taskEnd(mockTask, EndCause.SAME_TASK_BUSY, null);

        verify(compatListenerAssist).handleWarn(mockTaskAdapter, EndCause.FILE_BUSY, null);
        verify(compatListenerAssist).handleWarn(mockTaskAdapter, EndCause.SAME_TASK_BUSY, null);
        verify(compatListenerAssist, times(2)).onTaskFinish(mockTaskAdapter);
        verify(mockProgressAssist, times(2)).clearProgress();
        verify(mockSpeedCalculator, times(2)).flush();
    }

    @Test
    public void taskEnd_handleComplete() {
        final DownloadTask mockTask = mock(DownloadTask.class);
        final DownloadTaskAdapter mockTaskAdapter = mock(DownloadTaskAdapter.class);
        final ProgressAssist mockProgressAssist = mock(ProgressAssist.class);
        when(mockTask.getTag(DownloadTaskAdapter.KEY_TASK_ADAPTER)).thenReturn(mockTaskAdapter);
        when(mockTaskAdapter.getProgressAssist()).thenReturn(mockProgressAssist);
        doNothing().when(compatListenerAssist).handleComplete(mockTaskAdapter);
        doNothing().when(compatListenerAssist).onTaskFinish(mockTaskAdapter);

        compatListenerAssist.taskEnd(mockTask, EndCause.COMPLETED, null);

        verify(compatListenerAssist).handleComplete(mockTaskAdapter);
        verify(compatListenerAssist).onTaskFinish(mockTaskAdapter);
        verify(mockProgressAssist).clearProgress();
    }

    @Test
    public void handleCanceled() {
        final DownloadTaskAdapter mockTaskAdapter = mock(DownloadTaskAdapter.class);
        final ProgressAssist mockProgressAssist = mock(ProgressAssist.class);
        when(mockTaskAdapter.getProgressAssist()).thenReturn(mockProgressAssist);
        when(mockProgressAssist.getSofarBytes()).thenReturn(1L);
        when(mockTaskAdapter.getTotalBytesInLong()).thenReturn(2L);

        compatListenerAssist.handleCanceled(mockTaskAdapter);
        verify(callback).paused(mockTaskAdapter, 1L, 2L);
    }

    @Test
    public void handleWarn() {
        final DownloadTaskAdapter mockTaskAdapter = mock(DownloadTaskAdapter.class);
        compatListenerAssist.handleWarn(mockTaskAdapter, null, null);

        verify(callback).warn(mockTaskAdapter);
    }

    @Test
    public void handleError_interceptByRetry() {
        final DownloadTaskAdapter mockTaskAdapter = mock(DownloadTaskAdapter.class);
        final ProgressAssist mockProgressAssist = mock(ProgressAssist.class);
        final RetryAssist mockRetryAssist = mock(RetryAssist.class);
        final DownloadTask mockTask = mock(DownloadTask.class);
        when(mockTaskAdapter.getDownloadTask()).thenReturn(mockTask);
        when(mockTaskAdapter.getProgressAssist()).thenReturn(mockProgressAssist);
        when(mockTaskAdapter.getRetryAssist()).thenReturn(mockRetryAssist);
        when(mockProgressAssist.getSofarBytes()).thenReturn(1L);
        when(mockRetryAssist.canRetry()).thenReturn(true);
        when(mockRetryAssist.getRetriedTimes()).thenReturn(1);

        compatListenerAssist.handleError(mockTaskAdapter, null);
        verify(callback).retry(mockTaskAdapter, null, 2, 1L);
        verify(mockRetryAssist).doRetry(mockTask);
        verify(callback, never()).error(any(DownloadTaskAdapter.class), any(Throwable.class));
    }

    @Test
    public void handleError_withNetWorkPolicyException() {
        final DownloadTaskAdapter mockTaskAdapter = mock(DownloadTaskAdapter.class);
        when(mockTaskAdapter.getRetryAssist()).thenReturn(null);
        final Exception netWorkPolicyException = mock(NetworkPolicyException.class);

        compatListenerAssist.handleError(mockTaskAdapter, netWorkPolicyException);
        verify(callback, never()).retry(
                any(DownloadTaskAdapter.class), any(Throwable.class), anyInt(), anyLong());
        verify(callback).error(any(DownloadTaskAdapter.class),
                any(FileDownloadNetworkPolicyException.class));
    }

    @Test
    public void handleError_withPreAllocateException() {
        final DownloadTaskAdapter mockTaskAdapter = mock(DownloadTaskAdapter.class);
        final ProgressAssist mockProgressAssist = mock(ProgressAssist.class);
        when(mockTaskAdapter.getProgressAssist()).thenReturn(mockProgressAssist);
        when(mockTaskAdapter.getRetryAssist()).thenReturn(null);
        when(mockProgressAssist.getSofarBytes()).thenReturn(1L);
        final Exception mockException = mock(PreAllocateException.class);

        compatListenerAssist.handleError(mockTaskAdapter, mockException);

        verify(callback, never()).retry(
                any(DownloadTaskAdapter.class), any(Throwable.class), anyInt(), anyLong());
        final ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
        final ArgumentCaptor<DownloadTaskAdapter> taskCaptor = ArgumentCaptor
                .forClass(DownloadTaskAdapter.class);
        verify(callback).error(taskCaptor.capture(), throwableCaptor.capture());
        assertThat(taskCaptor.getValue()).isEqualTo(mockTaskAdapter);
        assertThat(throwableCaptor.getValue())
                .isExactlyInstanceOf(FileDownloadOutOfSpaceException.class);
    }

    @Test
    public void handleError_withDownloadSecurityException() {
        final DownloadTaskAdapter mockTaskAdapter = mock(DownloadTaskAdapter.class);
        final ProgressAssist mockProgressAssist = mock(ProgressAssist.class);
        when(mockTaskAdapter.getProgressAssist()).thenReturn(mockProgressAssist);
        when(mockTaskAdapter.getRetryAssist()).thenReturn(null);
        when(mockProgressAssist.getSofarBytes()).thenReturn(1L);
        final Exception mockException = mock(DownloadSecurityException.class);

        compatListenerAssist.handleError(mockTaskAdapter, mockException);

        verify(callback, never()).retry(
                any(DownloadTaskAdapter.class), any(Throwable.class), anyInt(), anyLong());
        final ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
        final ArgumentCaptor<DownloadTaskAdapter> taskCaptor = ArgumentCaptor
                .forClass(DownloadTaskAdapter.class);
        verify(callback).error(taskCaptor.capture(), throwableCaptor.capture());
        assertThat(taskCaptor.getValue()).isEqualTo(mockTaskAdapter);
        assertThat(throwableCaptor.getValue())
                .isExactlyInstanceOf(FileDownloadSecurityException.class);
    }

    @Test
    public void handleError_withOtherException() {
        final DownloadTaskAdapter mockTaskAdapter = mock(DownloadTaskAdapter.class);
        when(mockTaskAdapter.getRetryAssist()).thenReturn(null);
        final Exception exception = mock(Exception.class);

        compatListenerAssist.handleError(mockTaskAdapter, exception);
        verify(callback, never()).retry(
                any(DownloadTaskAdapter.class), any(Throwable.class), anyInt(), anyLong());
        verify(callback).error(any(DownloadTaskAdapter.class), any(Throwable.class));
    }

    @Test
    public void onTaskFinish() {
        final FileDownloadList fileDownloadList = spy(new FileDownloadList());
        FileDownloadList.setSingleton(fileDownloadList);
        final List<BaseDownloadTask.FinishListener> finishListeners = new ArrayList<>();
        final BaseDownloadTask.FinishListener mockFinishListener =
                mock(BaseDownloadTask.FinishListener.class);
        finishListeners.add(mockFinishListener);
        final DownloadTaskAdapter mockTaskAdapter = mock(DownloadTaskAdapter.class);
        when(mockTaskAdapter.getFinishListeners()).thenReturn(finishListeners);

        compatListenerAssist.onTaskFinish(mockTaskAdapter);

        verify(mockFinishListener).over(mockTaskAdapter);
        verify(fileDownloadList).remove(mockTaskAdapter);
    }

    @Test
    public void handleComplete_syncCallback() throws Throwable {
        final DownloadTaskAdapter mockTaskAdapter = mock(DownloadTaskAdapter.class);
        final DownloadTask mockTask = mock(DownloadTask.class);
        when(mockTaskAdapter.getDownloadTask()).thenReturn(mockTask);
        when(mockTask.isAutoCallbackToUIThread()).thenReturn(false);

        compatListenerAssist.taskConnected.set(true);
        compatListenerAssist.handleComplete(mockTaskAdapter);

        verify(callback).blockComplete(mockTaskAdapter);
        verify(callback).completed(mockTaskAdapter);
        assertThat(compatListenerAssist.isReuseOldFile()).isFalse();
    }

    @Test
    public void handleComplete_syncCallback_error() throws Throwable {
        final DownloadTaskAdapter mockTaskAdapter = mock(DownloadTaskAdapter.class);
        final DownloadTask mockTask = mock(DownloadTask.class);
        final Exception mockException =  mock(Exception.class);
        when(mockTaskAdapter.getDownloadTask()).thenReturn(mockTask);
        when(mockTask.isAutoCallbackToUIThread()).thenReturn(false);
        doNothing().when(compatListenerAssist)
                .handleError(any(DownloadTaskAdapter.class), any(Exception.class));
        doThrow(mockException).when(callback).blockComplete(mockTaskAdapter);

        compatListenerAssist.taskConnected.set(false);
        compatListenerAssist.handleComplete(mockTaskAdapter);

        verify(callback, never()).completed(mockTaskAdapter);
        verify(compatListenerAssist).handleError(
                any(DownloadTaskAdapter.class), any(Exception.class));
        assertThat(compatListenerAssist.isReuseOldFile()).isTrue();
    }

    @Test
    public void handleBlockComplete() throws Throwable {
        final DownloadTaskAdapter mockTaskAdapter = mock(DownloadTaskAdapter.class);
        doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) {
                final Runnable runnable = invocation.getArgument(0);
                runnable.run();
                return null;
            }
        }).when(uiHander).post(any(Runnable.class));

        compatListenerAssist.handleBlockComplete(mockTaskAdapter);

        verify(callback).blockComplete(mockTaskAdapter);
        verify(callback).completed(mockTaskAdapter);
    }

    @Test
    public void handleBlockComplete_error() throws Throwable {
        final DownloadTaskAdapter mockTaskAdapter = mock(DownloadTaskAdapter.class);
        doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) {
                final Runnable runnable = invocation.getArgument(0);
                runnable.run();
                return null;
            }
        }).when(uiHander).post(any(Runnable.class));
        doThrow(mock(Exception.class)).when(callback).blockComplete(mockTaskAdapter);
        doNothing().when(compatListenerAssist)
                .handleError(any(DownloadTaskAdapter.class), any(Exception.class));

        compatListenerAssist.handleBlockComplete(mockTaskAdapter);

        verify(compatListenerAssist)
                .handleError(any(DownloadTaskAdapter.class), any(Exception.class));
        verify(callback, never()).completed(mockTaskAdapter);
    }
}