package com.avg.zaap.crashlibrary.offline.uploader;

import android.content.Context;

import com.avg.zaap.crashlibrary.CrashLib;
import com.avg.zaap.crashlibrary.ICrashPublishingDelegate;
import com.avg.zaap.crashlibrary.ICrashServiceCallback;
import com.avg.zaap.crashlibrary.ReportData;
import com.avg.zaap.crashlibrary.config.CrashLibConfig;
import com.avg.zaap.crashlibrary.offline.Strategy;
import com.avg.zaap.crashlibrary.offline.publisher.metadata.ICrashMetadataReader;
import com.avg.zaap.crashlibrary.publish.ICrashPublisher;

import org.junit.Before;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import java.io.File;
import java.io.FilenameFilter;
import java.util.ArrayList;
import java.util.List;

import static org.hamcrest.CoreMatchers.sameInstance;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * A test class for {@link OfflineUploader}.
 * <p/>
 * Created by marek.osvald on 8.9.2016.
 */
public class OfflineUploaderTests {

    private static final String OFFLINE_REPORT_DIRECTORY_NAME = "offline";
    private static final String CRASH_REPORT_FILENAME1 = "crashReport1.bp";
    private static final String CRASH_REPORT_FILENAME2 = "crashReport2.bp";
    private static final long MOCK_LAST_ATTEMPT_TIMESTAMP = 123;

    private OfflineUploader mOfflineUploader;
    private Context mContext;
    private File mFilesDir;
    private Thread mThread;
    private Runnable mRunnable;
    private File mOfflineDirectory;
    private File mCrashReportFile1;
    private File mCrashReportFile2;
    private FilenameFilter mFilenameFilter;
    private ICrashPublishingDelegate mCrashPublishingDelegate;
    private ICrashMetadataReader mCrashMetadataReader;
    private IDelayHelper mDelayHelper;
    private ReportData mReportData;
    private ICrashPublisher mCrashPublisher1;
    private ICrashPublisher mCrashPublisher2;

    @Before
    public void setUp() throws Exception {
        mContext = mock(Context.class);
        mFilesDir = mock(File.class);
        when(mContext.getFilesDir()).thenReturn(mFilesDir);

        mThread = mock(Thread.class);
        mOfflineDirectory = mock(File.class);

        mCrashReportFile1 = mock(File.class);
        when(mCrashReportFile1.getAbsolutePath()).thenReturn(CRASH_REPORT_FILENAME1);
        when(mCrashReportFile1.delete()).thenReturn(true);
        mCrashReportFile2 = mock(File.class);
        when(mCrashReportFile2.getAbsolutePath()).thenReturn(CRASH_REPORT_FILENAME2);
        when(mCrashReportFile2.delete()).thenReturn(true);

        mFilenameFilter = mock(FilenameFilter.class);
        mCrashPublishingDelegate = mock(ICrashPublishingDelegate.class);

        mCrashMetadataReader = mock(ICrashMetadataReader.class);
        when(mCrashMetadataReader.readMetadata(any(File.class))).thenAnswer(new Answer<ReportData>() {
            @Override
            public ReportData answer(InvocationOnMock invocation) throws Throwable {
                File file = (File) invocation.getArguments()[0];

                if (file == mCrashReportFile1 || file == mCrashReportFile2) {
                    return mReportData;
                }

                fail();
                return null;
            }
        });

        mDelayHelper = mock(IDelayHelper.class);
        mReportData = mock(ReportData.class);
        when(mReportData.getLastAttemptTimestamp()).thenReturn(MOCK_LAST_ATTEMPT_TIMESTAMP);

        mCrashPublisher1 = mock(ICrashPublisher.class);
        mCrashPublisher2 = mock(ICrashPublisher.class);

        mOfflineUploader = new OfflineUploader(mContext, new IOfflineUploaderFactory() {
            @Override
            public Thread getThread(Runnable runnable) {
                mRunnable = runnable;
                doAnswer(new Answer() {
                    @Override
                    public Object answer(InvocationOnMock invocation) throws Throwable {
                        mRunnable.run();
                        return null;
                    }
                }).when(mThread).start();
                return mThread;
            }

            @Override
            public File openFile(File directory, String fileName) {
                if (directory == mFilesDir && fileName.equals(OFFLINE_REPORT_DIRECTORY_NAME)) {
                    return mOfflineDirectory;
                }

                if (directory == mOfflineDirectory) {
                    switch (fileName) {
                        case CRASH_REPORT_FILENAME1:
                            return mCrashReportFile1;

                        case CRASH_REPORT_FILENAME2:
                            return mCrashReportFile2;

                        default:
                            fail();
                            return null;
                    }
                }

                fail();
                return null;
            }

            @Override
            public FilenameFilter getCrashReportFilenameFilter() {
                return mFilenameFilter;
            }

            @Override
            public ICrashPublishingDelegate getCrashPublishingDelegate(Context context) {
                assertThat(context, sameInstance(mContext));
                return mCrashPublishingDelegate;
            }

            @Override
            public ICrashMetadataReader getCrashMetadataReader() {
                return mCrashMetadataReader;
            }

            @Override
            public IDelayHelper getDelayHelper() {
                return mDelayHelper;
            }

        });

        initializeOfflineDirectory(true, false);
        initializeCrashLibConfig(Strategy.ALL);
        initializeDelayHelper(true);
    }

    @Test
    public void startUploader_noReportsToProcess_doesNotCrash() throws Exception {
        // act
        mOfflineUploader.startUploader();

        // assert
        verify(mThread, times(1)).start();
    }

    @Test
    public void startUploader_crashDirectoryDoesNotExist_doesNotCrash() throws Exception {
        // arrange
        initializeOfflineDirectory(false, false);

        // act
        mOfflineUploader.startUploader();

        // assert
        verify(mThread, times(1)).start();
    }

    @Test
    public void startUploader_crashDirectoryIsAFile_doesNotCrash() throws Exception {
        // arrange
        initializeOfflineDirectory(true, true);

        // act
        mOfflineUploader.startUploader();

        // assert
        verify(mThread, times(1)).start();
    }

    @Test
    public void startUploader_singleReportFile_publishesReport() throws Exception {
        // arrange
        initializeCrashFiles(new String[]{CRASH_REPORT_FILENAME1});

        // act
        mOfflineUploader.startUploader();

        // assert
        verify(mThread, times(1)).start();
    }

    @Test
    public void startUploader_multipleReportFiles_publishesReport() throws Exception {
        // arrange
        initializeCrashFiles(new String[]{CRASH_REPORT_FILENAME1, CRASH_REPORT_FILENAME2});

        // act
        mOfflineUploader.startUploader();

        // assert
        verify(mThread, times(1)).start();
    }

    @Test
    public void startUploader_reportDataNull_doesNotPublishReport() throws Exception {
        // arrange
        initializeCrashFiles(new String[]{CRASH_REPORT_FILENAME1, CRASH_REPORT_FILENAME2});
        when(mCrashMetadataReader.readMetadata(mCrashReportFile1)).thenReturn(null);
        when(mCrashMetadataReader.readMetadata(mCrashReportFile2)).thenReturn(null);

        // act
        mOfflineUploader.startUploader();

        // assert
        verify(mThread, times(1)).start();
        verify(mCrashPublishingDelegate, never()).publishOfflineCrash(anyString(), any(ReportData.class), any(ICrashServiceCallback.class));
    }

    @Test
    public void startUploader_reportDataNullFilesCannotBeDeleted_doesNotPublishReport() throws Exception {
        // arrange
        initializeCrashFiles(new String[]{CRASH_REPORT_FILENAME1, CRASH_REPORT_FILENAME2});
        when(mCrashMetadataReader.readMetadata(mCrashReportFile1)).thenReturn(null);
        when(mCrashMetadataReader.readMetadata(mCrashReportFile2)).thenReturn(null);
        when(mCrashReportFile1.delete()).thenReturn(false);
        when(mCrashReportFile2.delete()).thenReturn(false);

        // act
        mOfflineUploader.startUploader();

        // assert
        verify(mThread, times(1)).start();
        verify(mCrashPublishingDelegate, never()).publishOfflineCrash(anyString(), any(ReportData.class), any(ICrashServiceCallback.class));
    }

    @Test
    public void startUploader_multipleReportFilesDelayInvalid_doesNotPublishReport() throws Exception {
        // arrange
        initializeCrashFiles(new String[]{CRASH_REPORT_FILENAME1, CRASH_REPORT_FILENAME2});
        initializeDelayHelper(false);

        // act
        mOfflineUploader.startUploader();

        // assert
        verify(mThread, times(1)).start();
        verify(mCrashPublishingDelegate, never()).publishOfflineCrash(anyString(), any(ReportData.class), any(ICrashServiceCallback.class));
    }

    @Test
    public void stopUploader_nothing_doesNotCrash() throws Exception {
        // act
        mOfflineUploader.stopUploader();
    }

    @Test
    public void onPublisherResult_strategyAllAllSuccessful_startsPublishingAnotherFile() throws Exception {
        // arrange
        initializeCrashFiles(new String[]{CRASH_REPORT_FILENAME1, CRASH_REPORT_FILENAME2});
        mOfflineUploader.startUploader();

        // act
        mOfflineUploader.onPublisherResult(CrashLib.getConfig().getPublishers().size(), 0);

        // assert
        verify(mCrashPublishingDelegate, times(1)).publishOfflineCrash(CRASH_REPORT_FILENAME1, mReportData, mOfflineUploader);
        verify(mCrashPublishingDelegate, times(1)).publishOfflineCrash(CRASH_REPORT_FILENAME2, mReportData, mOfflineUploader);
    }

    @Test
    public void onPublisherResult_strategyAllAllSuccessfulNoMoreReports_finishesPublishing() throws Exception {
        // arrange
        initializeCrashFiles(new String[]{CRASH_REPORT_FILENAME1});
        mOfflineUploader.startUploader();

        // act
        mOfflineUploader.onPublisherResult(CrashLib.getConfig().getPublishers().size(), 0);

        // assert
        verify(mCrashPublishingDelegate, times(1)).publishOfflineCrash(CRASH_REPORT_FILENAME1, mReportData, mOfflineUploader);
        verify(mCrashPublishingDelegate, never()).publishOfflineCrash(CRASH_REPORT_FILENAME2, mReportData, mOfflineUploader);
    }

    @Test
    public void onPublisherResult_strategyAllAllSuccessfulStoppedInTheMeantime_finishesPublishing() throws Exception {
        // arrange
        initializeCrashFiles(new String[]{CRASH_REPORT_FILENAME1, CRASH_REPORT_FILENAME2});
        mOfflineUploader.startUploader();
        mOfflineUploader.stopUploader();

        // act
        mOfflineUploader.onPublisherResult(CrashLib.getConfig().getPublishers().size(), 0);

        // assert
        verify(mCrashPublishingDelegate, times(1)).publishOfflineCrash(CRASH_REPORT_FILENAME1, mReportData, mOfflineUploader);
        verify(mCrashPublishingDelegate, never()).publishOfflineCrash(CRASH_REPORT_FILENAME2, mReportData, mOfflineUploader);
    }

    @Test
    public void onPublisherResult_strategyAllOneUnsuccessful_startsPublishingAnotherFile() throws Exception {
        // arrange
        initializeCrashFiles(new String[]{CRASH_REPORT_FILENAME1, CRASH_REPORT_FILENAME2});
        mOfflineUploader.startUploader();

        // act
        mOfflineUploader.onPublisherResult(CrashLib.getConfig().getPublishers().size() - 1, 1);

        // assert
        verify(mCrashPublishingDelegate, times(1)).publishOfflineCrash(CRASH_REPORT_FILENAME1, mReportData, mOfflineUploader);
        verify(mCrashPublishingDelegate, never()).publishOfflineCrash(CRASH_REPORT_FILENAME2, mReportData, mOfflineUploader);
    }

    @Test
    public void onPublisherResult_strategyAllAllUnsuccessful_startsPublishingAnotherFile() throws Exception {
        // arrange
        initializeCrashFiles(new String[]{CRASH_REPORT_FILENAME1, CRASH_REPORT_FILENAME2});
        mOfflineUploader.startUploader();

        // act
        mOfflineUploader.onPublisherResult(0, CrashLib.getConfig().getPublishers().size());

        // assert
        verify(mCrashPublishingDelegate, times(1)).publishOfflineCrash(CRASH_REPORT_FILENAME1, mReportData, mOfflineUploader);
        verify(mCrashPublishingDelegate, never()).publishOfflineCrash(CRASH_REPORT_FILENAME2, mReportData, mOfflineUploader);
    }

    @Test
    public void onPublisherResult_strategyAnyAllSuccessful_startsPublishingAnotherFile() throws Exception {
        // arrange
        initializeCrashLibConfig(Strategy.ANY);
        initializeCrashFiles(new String[]{CRASH_REPORT_FILENAME1, CRASH_REPORT_FILENAME2});
        mOfflineUploader.startUploader();

        // act
        mOfflineUploader.onPublisherResult(CrashLib.getConfig().getPublishers().size(), 0);

        // assert
        verify(mCrashPublishingDelegate, times(1)).publishOfflineCrash(CRASH_REPORT_FILENAME1, mReportData, mOfflineUploader);
        verify(mCrashPublishingDelegate, times(1)).publishOfflineCrash(CRASH_REPORT_FILENAME2, mReportData, mOfflineUploader);
    }

    @Test
    public void onPublisherResult_strategyAnyAllSuccessfulNoMoreReports_finishesPublishing() throws Exception {
        // arrange
        initializeCrashLibConfig(Strategy.ANY);
        initializeCrashFiles(new String[]{CRASH_REPORT_FILENAME1});
        mOfflineUploader.startUploader();

        // act
        mOfflineUploader.onPublisherResult(CrashLib.getConfig().getPublishers().size(), 0);

        // assert
        verify(mCrashPublishingDelegate, times(1)).publishOfflineCrash(CRASH_REPORT_FILENAME1, mReportData, mOfflineUploader);
        verify(mCrashPublishingDelegate, never()).publishOfflineCrash(CRASH_REPORT_FILENAME2, mReportData, mOfflineUploader);
    }

    @Test
    public void onPublisherResult_strategyAnyAllSuccessfulStoppedInTheMeantime_finishesPublishing() throws Exception {
        // arrange
        initializeCrashLibConfig(Strategy.ANY);
        initializeCrashFiles(new String[]{CRASH_REPORT_FILENAME1, CRASH_REPORT_FILENAME2});
        mOfflineUploader.startUploader();
        mOfflineUploader.stopUploader();

        // act
        mOfflineUploader.onPublisherResult(CrashLib.getConfig().getPublishers().size(), 0);

        // assert
        verify(mCrashPublishingDelegate, times(1)).publishOfflineCrash(CRASH_REPORT_FILENAME1, mReportData, mOfflineUploader);
        verify(mCrashPublishingDelegate, never()).publishOfflineCrash(CRASH_REPORT_FILENAME2, mReportData, mOfflineUploader);
    }

    @Test
    public void onPublisherResult_strategyAnyOneUnsuccessful_startsPublishingAnotherFile() throws Exception {
        // arrange
        initializeCrashLibConfig(Strategy.ANY);
        initializeCrashFiles(new String[]{CRASH_REPORT_FILENAME1, CRASH_REPORT_FILENAME2});
        mOfflineUploader.startUploader();

        // act
        mOfflineUploader.onPublisherResult(CrashLib.getConfig().getPublishers().size() - 1, 1);

        // assert
        verify(mCrashPublishingDelegate, times(1)).publishOfflineCrash(CRASH_REPORT_FILENAME1, mReportData, mOfflineUploader);
        verify(mCrashPublishingDelegate, times(1)).publishOfflineCrash(CRASH_REPORT_FILENAME2, mReportData, mOfflineUploader);
    }

    @Test
    public void onPublisherResult_strategyAnyAllUnsuccessful_startsPublishingAnotherFile() throws Exception {
        // arrange
        initializeCrashLibConfig(Strategy.ANY);
        initializeCrashFiles(new String[]{CRASH_REPORT_FILENAME1, CRASH_REPORT_FILENAME2});
        mOfflineUploader.startUploader();

        // act
        mOfflineUploader.onPublisherResult(0, CrashLib.getConfig().getPublishers().size());

        // assert
        verify(mCrashPublishingDelegate, times(1)).publishOfflineCrash(CRASH_REPORT_FILENAME1, mReportData, mOfflineUploader);
        verify(mCrashPublishingDelegate, never()).publishOfflineCrash(CRASH_REPORT_FILENAME2, mReportData, mOfflineUploader);
    }

    @Test
    public void onPublisherResult_strategyBestEffortAllSuccessful_startsPublishingAnotherFile() throws Exception {
        // arrange
        initializeCrashLibConfig(Strategy.BEST_EFFORT);
        initializeCrashFiles(new String[]{CRASH_REPORT_FILENAME1, CRASH_REPORT_FILENAME2});
        mOfflineUploader.startUploader();

        // act
        mOfflineUploader.onPublisherResult(CrashLib.getConfig().getPublishers().size(), 0);

        // assert
        verify(mCrashPublishingDelegate, times(1)).publishOfflineCrash(CRASH_REPORT_FILENAME1, mReportData, mOfflineUploader);
        verify(mCrashPublishingDelegate, times(1)).publishOfflineCrash(CRASH_REPORT_FILENAME2, mReportData, mOfflineUploader);
    }

    @Test
    public void onPublisherResult_strategyBestEffortAllSuccessfulNoMoreReports_finishesPublishing() throws Exception {
        // arrange
        initializeCrashLibConfig(Strategy.BEST_EFFORT);
        initializeCrashFiles(new String[]{CRASH_REPORT_FILENAME1});
        mOfflineUploader.startUploader();

        // act
        mOfflineUploader.onPublisherResult(CrashLib.getConfig().getPublishers().size(), 0);

        // assert
        verify(mCrashPublishingDelegate, times(1)).publishOfflineCrash(CRASH_REPORT_FILENAME1, mReportData, mOfflineUploader);
        verify(mCrashPublishingDelegate, never()).publishOfflineCrash(CRASH_REPORT_FILENAME2, mReportData, mOfflineUploader);
    }

    @Test
    public void onPublisherResult_strategyBestEffortAllSuccessfulStoppedInTheMeantime_finishesPublishing() throws Exception {
        // arrange
        initializeCrashLibConfig(Strategy.BEST_EFFORT);
        initializeCrashFiles(new String[]{CRASH_REPORT_FILENAME1, CRASH_REPORT_FILENAME2});
        mOfflineUploader.startUploader();
        mOfflineUploader.stopUploader();

        // act
        mOfflineUploader.onPublisherResult(CrashLib.getConfig().getPublishers().size(), 0);

        // assert
        verify(mCrashPublishingDelegate, times(1)).publishOfflineCrash(CRASH_REPORT_FILENAME1, mReportData, mOfflineUploader);
        verify(mCrashPublishingDelegate, never()).publishOfflineCrash(CRASH_REPORT_FILENAME2, mReportData, mOfflineUploader);
    }

    @Test
    public void onPublisherResult_strategyBestEffortOneUnsuccessful_startsPublishingAnotherFile() throws Exception {
        // arrange
        initializeCrashLibConfig(Strategy.BEST_EFFORT);
        initializeCrashFiles(new String[]{CRASH_REPORT_FILENAME1, CRASH_REPORT_FILENAME2});
        mOfflineUploader.startUploader();

        // act
        mOfflineUploader.onPublisherResult(CrashLib.getConfig().getPublishers().size() - 1, 1);

        // assert
        verify(mCrashPublishingDelegate, times(1)).publishOfflineCrash(CRASH_REPORT_FILENAME1, mReportData, mOfflineUploader);
        verify(mCrashPublishingDelegate, times(1)).publishOfflineCrash(CRASH_REPORT_FILENAME2, mReportData, mOfflineUploader);
    }

    @Test
    public void onPublisherResult_strategyBestEffortAllUnsuccessful_startsPublishingAnotherFile() throws Exception {
        // arrange
        initializeCrashLibConfig(Strategy.BEST_EFFORT);
        initializeCrashFiles(new String[]{CRASH_REPORT_FILENAME1, CRASH_REPORT_FILENAME2});
        mOfflineUploader.startUploader();

        // act
        mOfflineUploader.onPublisherResult(0, CrashLib.getConfig().getPublishers().size());

        // assert
        verify(mCrashPublishingDelegate, times(1)).publishOfflineCrash(CRASH_REPORT_FILENAME1, mReportData, mOfflineUploader);
        verify(mCrashPublishingDelegate, times(1)).publishOfflineCrash(CRASH_REPORT_FILENAME2, mReportData, mOfflineUploader);
    }

    @Test(expected = UnsupportedOperationException.class)
    public void asBinder() throws Exception {
        // act
        mOfflineUploader.asBinder();
    }

    private void initializeOfflineDirectory(boolean exists, boolean isFile) {
        when(mOfflineDirectory.exists()).thenReturn(exists);
        when(mOfflineDirectory.isFile()).thenReturn(isFile);
    }

    private void initializeCrashFiles(String[] files) {
        when(mOfflineDirectory.list(mFilenameFilter)).thenReturn(files);
    }

    private void initializeCrashLibConfig(Strategy strategy) {
        List<ICrashPublisher> publishers = new ArrayList<>();
        publishers.add(mCrashPublisher1);
        publishers.add(mCrashPublisher2);

        CrashLib.setConfig(new CrashLibConfig.Builder()
                .strategy(strategy)
                .publishers(publishers)
                .build());
    }

    private void initializeDelayHelper(boolean isAttemptValid) {
        when(mDelayHelper.isAttemptValid(MOCK_LAST_ATTEMPT_TIMESTAMP)).thenReturn(isAttemptValid);
    }

}
