package com.avg.zaap.crashlibrary.core;

import android.content.Context;
import android.os.RemoteException;

import com.avg.zaap.crashlibrary.BuildConfig;
import com.avg.zaap.crashlibrary.CrashLib;
import com.avg.zaap.crashlibrary.ICrashServiceCommunicator;
import com.avg.zaap.crashlibrary.ReportData;
import com.avg.zaap.crashlibrary.config.CrashLibConfig;
import com.avg.zaap.crashlibrary.core.hash.IHashGenerator;
import com.avg.zaap.crashlibrary.core.helper.process.IProcessHelper;
import com.avg.zaap.crashlibrary.provider.CrashProviderException;
import com.avg.zaap.crashlibrary.provider.ICrashSyncProvider;
import com.avg.zaap.crashlibrary.provider.stacktrace.IStackTraceWriter;
import com.avg.zaap.crashlibrary.provider.stacktrace.StackTraceWriterException;
import com.avg.zaap.crashlibrary.publish.cap.ICapHelper;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.powermock.reflect.Whitebox;

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

import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.powermock.api.mockito.PowerMockito.verifyStatic;

/**
 * Tests for {@link CrashUncaughtExceptionHandler}.
 * <p/>
 * Created by marek.osvald on 17. 5. 2016.
 */
@RunWith(PowerMockRunner.class)
@PrepareForTest({StackTraceElement.class, CrashUncaughtExceptionHandler.class, android.os.Process.class, UUID.class, Class.class})
public class CrashUncaughtExceptionHandlerTests {

    private static final String PATH_TO_LOGS_DIR = "absolute/path";
    private static final String PATH_ALL_STACK_TRACES = "path4";
    private static final String PATH_CRASHING_STACK_TRACES = "path5";

    private static final String CRASH_CODE = "hash";
    private static final String ERROR_MESSAGE = "errorDetails";
    private static final String ERROR_DETAILS = "errorDetails";

    private static final String MOCK_FILE_NAME = "mockFileName";
    private static final String MOCK_CLASS_NAME = "mockClassName";
    private static final String MOCK_METHOD_NAME = "mockMethodName";
    private static final String MOCK_CURRENT_PROCESS_NAME = "mockCurrentProcessName";

    private static final String MOCK_CAP_EXCEPTION_CODE = "mockCapExceptionCode";
    private static final int MOCK_CAP_FAULTING_OFFSET = 123;
    private static final String MOCK_CAP_MODULE_NAME = String.format("%s_%s_%s", MOCK_FILE_NAME, MOCK_CLASS_NAME, MOCK_METHOD_NAME);
    private static final String MOCK_CAP_GUID = "00000000-0000-0001-0000-000000000001";

    private static final java.lang.String SYNC_PROVIDER_1_PATH = "path1";
    private static final java.lang.String SYNC_PROVIDER_2_PATH = "path2";
    private static final java.lang.String SYNC_PROVIDER_3_PATH = "path3";

    private static final int MOCK_MY_PID = 630;
    private static final int NO_ATTEMPTS = 0;
    private static final long NEVER = 0;

    private ICrashUncaughtExceptionHandlerFactory mFactory;
    private Context mContext;
    private CrashUncaughtExceptionHandler mCrashUncaughtExceptionHandler;
    private IProcessHelper mProcessHelper;
    private ICapHelper mCapHelper;
    private ReportData mReportData;
    private IStackTraceWriter mStackTraceWriter;
    private IHashGenerator mHashGenerator;
    private ServiceConnectionHelper mServiceConnectionHelper;
    private ICrashUncaughtExceptionHandlerHelper mHelper;
    private Throwable mException;
    private ICrashServiceCommunicator mCrashServiceCommunicator;
    private boolean mReportAndroidCrashCalled;
    private ICrashSyncProvider mSyncProvider1;
    private ICrashSyncProvider mSyncProvider2;
    private ICrashSyncProvider mSyncProvider3;
    private StackTraceElement[] mExceptionStackTrace;
    private ArgumentCaptor<List> mFilesToPackCaptor;

    @Before
    public void setUp() throws Exception {
        PowerMockito.mockStatic(android.os.Process.class);
        PowerMockito.when(android.os.Process.myPid()).thenReturn(MOCK_MY_PID);

        mReportAndroidCrashCalled = false;

        mContext = mock(Context.class);
        File file = mock(File.class);
        when(mContext.getFilesDir()).thenReturn(file);
        when(file.getAbsolutePath()).thenReturn(PATH_TO_LOGS_DIR);

        initCrashLibConfig(false, false, null);

        mProcessHelper = mock(IProcessHelper.class);
        when(mProcessHelper.getCurrentProcessName(mContext)).thenReturn(MOCK_CURRENT_PROCESS_NAME);

        mCapHelper = mock(ICapHelper.class);
        when(mCapHelper.getExceptionCodeFromCrashCode(CRASH_CODE)).thenReturn(MOCK_CAP_EXCEPTION_CODE);

        mReportData = mock(ReportData.class);

        mException = mock(Throwable.class);
        mExceptionStackTrace = new StackTraceElement[2];

        StackTraceElement topStackTraceElement = PowerMockito.mock(StackTraceElement.class);
        PowerMockito.when(topStackTraceElement.getLineNumber()).thenReturn(MOCK_CAP_FAULTING_OFFSET);
        PowerMockito.when(topStackTraceElement.getClassName()).thenReturn(MOCK_FILE_NAME);
        PowerMockito.when(topStackTraceElement.getFileName()).thenReturn(MOCK_FILE_NAME);
        PowerMockito.when(topStackTraceElement.getClassName()).thenReturn(MOCK_CLASS_NAME);
        PowerMockito.when(topStackTraceElement.getMethodName()).thenReturn(MOCK_METHOD_NAME);

        mExceptionStackTrace[0] = topStackTraceElement;
        mExceptionStackTrace[1] = PowerMockito.mock(StackTraceElement.class);
        when(mException.getStackTrace()).thenReturn(mExceptionStackTrace);
        when(mException.getMessage()).thenReturn(ERROR_MESSAGE);

        mStackTraceWriter = mock(IStackTraceWriter.class);
        when(mStackTraceWriter.writeAllStackTraces(PATH_TO_LOGS_DIR)).thenReturn(PATH_ALL_STACK_TRACES);
        when(mStackTraceWriter.writeCrashingThreadStackTrace(PATH_TO_LOGS_DIR, mException)).thenReturn(PATH_CRASHING_STACK_TRACES);

        mHashGenerator = mock(IHashGenerator.class);
        when(mHashGenerator.generateHash(mExceptionStackTrace)).thenReturn(CRASH_CODE);

        mFilesToPackCaptor = ArgumentCaptor.forClass(List.class);

        mServiceConnectionHelper = mock(ServiceConnectionHelper.class);

        mHelper = mock(ICrashUncaughtExceptionHandlerHelper.class);
        when(mHelper.getErrorDetails(mException)).thenReturn(ERROR_DETAILS);

        PowerMockito.mockStatic(UUID.class);
        PowerMockito.when(UUID.randomUUID()).thenReturn(new UUID(1, 1));

        mFactory = new ICrashUncaughtExceptionHandlerFactory() {

            @Override
            public IStackTraceWriter getStackTraceWriter() {
                return mStackTraceWriter;
            }

            @Override
            public IHashGenerator getHashGenerator() {
                return mHashGenerator;
            }

            @Override
            public ServiceConnectionHelper getServiceConnectionHelper(ICrashReportingServiceCallback callback) {
                return mServiceConnectionHelper;
            }

            @Override
            public ICrashUncaughtExceptionHandlerHelper getHelper() {
                return mHelper;
            }

            @Override
            public IProcessHelper getProcessHelper() {
                return mProcessHelper;
            }

            @Override
            public ICapHelper getCapHelper() {
                return mCapHelper;
            }

            @Override
            public ReportData getReportData() {
                return mReportData;
            }

        };

        mCrashServiceCommunicator = mock(ICrashServiceCommunicator.class);
    }

    private void initCrashLibConfig(boolean disableStackTraceAllThreads, boolean disableStackTraceCrashingThread, List<ICrashSyncProvider> syncProviders) throws CrashProviderException {

        CrashLibConfig.Builder builder = new CrashLibConfig.Builder()
                .syncProviders(syncProviders);

        if (disableStackTraceAllThreads) {
            builder.disableStackTraceAllThreads();
        }

        if (disableStackTraceCrashingThread) {
            builder.disableStackTraceCrashingThread();
        }

        final CrashLibConfig crashLibConfig = builder.build();
        Whitebox.setInternalState(CrashLib.class, "sConfig", crashLibConfig);
    }

    @Test
    public void constructor_thisProcessIsService_doNotBindTOService() throws Exception {
        //prepare
        when(mProcessHelper.isReportingServiceProcess(mContext)).thenReturn(true);

        //test
        mCrashUncaughtExceptionHandler = new CrashUncaughtExceptionHandler(mContext, mFactory);

        //verify
        verify(mServiceConnectionHelper, times(0)).startServiceConnectionThread(mContext);
    }

    @Test
    public void constructor_thisProcessIsNotServiceItIsNotBound_bindTOService() throws Exception {
        //prepare
        when(mProcessHelper.isReportingServiceProcess(mContext)).thenReturn(false);

        //test
        mCrashUncaughtExceptionHandler = new CrashUncaughtExceptionHandler(mContext, mFactory);

        //verify
        verify(mServiceConnectionHelper, times(1)).startServiceConnectionThread(mContext);
    }

    @Test
    public void uncaughtException_valid_crashReported() throws Exception {
        //prepare
        List<ICrashSyncProvider> syncProviders = new ArrayList<>();
        mSyncProvider1 = mock(ICrashSyncProvider.class);
        when(mSyncProvider1.provideData(PATH_TO_LOGS_DIR)).thenReturn(SYNC_PROVIDER_1_PATH);
        mSyncProvider2 = mock(ICrashSyncProvider.class);
        when(mSyncProvider2.provideData(PATH_TO_LOGS_DIR)).thenReturn(SYNC_PROVIDER_2_PATH);
        mSyncProvider3 = mock(ICrashSyncProvider.class);
        when(mSyncProvider3.provideData(PATH_TO_LOGS_DIR)).thenReturn(SYNC_PROVIDER_3_PATH);

        syncProviders.add(mSyncProvider1);
        syncProviders.add(mSyncProvider2);
        syncProviders.add(mSyncProvider3);

        initCrashLibConfig(false, false, syncProviders);

        when(mProcessHelper.isReportingServiceProcess(mContext)).thenReturn(true);

        //noinspection unchecked
        doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                mReportAndroidCrashCalled = true;
                return null;
            }
        }).when(mCrashServiceCommunicator).reportAndroidCrash(mReportData);

        //test
        mCrashUncaughtExceptionHandler = new CrashUncaughtExceptionHandler(mContext, mFactory);
        mCrashUncaughtExceptionHandler.onServiceConnected(mCrashServiceCommunicator);
        mCrashUncaughtExceptionHandler.uncaughtException(mock(Thread.class), mException);

        //verify
        assertTrue(mReportAndroidCrashCalled);

        // common
        verify(mReportData, times(1)).setCrashCode(CRASH_CODE);
        verify(mReportData, times(1)).setErrorName(mException.getClass().getSimpleName());
        verify(mReportData, times(1)).setErrorMessage(ERROR_MESSAGE);
        verify(mReportData, times(1)).setErrorDetails(ERROR_DETAILS);
        //noinspection unchecked
        verify(mReportData, times(1)).setFilesToPack(mFilesToPackCaptor.capture());
        assertThat(mFilesToPackCaptor.getValue().size(), is(5));
        assertThat(mFilesToPackCaptor.getValue().contains(SYNC_PROVIDER_1_PATH), is(true));
        assertThat(mFilesToPackCaptor.getValue().contains(SYNC_PROVIDER_2_PATH), is(true));
        assertThat(mFilesToPackCaptor.getValue().contains(SYNC_PROVIDER_3_PATH), is(true));
        assertThat(mFilesToPackCaptor.getValue().contains(PATH_CRASHING_STACK_TRACES), is(true));
        assertThat(mFilesToPackCaptor.getValue().contains(PATH_ALL_STACK_TRACES), is(true));

        // CAP
        verify(mReportData, times(1)).setCapExceptionCode(MOCK_CAP_EXCEPTION_CODE);
        verify(mReportData, times(1)).setCapFaultingOffset(MOCK_CAP_FAULTING_OFFSET);
        verify(mReportData, times(1)).setCapModuleName(MOCK_CAP_MODULE_NAME);
        verify(mReportData, times(1)).setCapModuleVersion(CrashLib.getConfig().getApplicationVersion());
        verify(mReportData, times(1)).setCapProcessName(MOCK_CURRENT_PROCESS_NAME);
        verify(mReportData, times(1)).setCapProcessVersion(BuildConfig.VERSION_NAME);
        verify(mReportData, times(1)).setCapGUID(MOCK_CAP_GUID);

        // offline
        verify(mReportData, times(1)).setPublishAttempts(NO_ATTEMPTS);
        verify(mReportData, times(1)).setLastAttemptTimestamp(NEVER);

        verifyNoMoreInteractions(mReportData);

        verifyStatic(times(1));
        android.os.Process.killProcess(MOCK_MY_PID);
    }

    @Test
    public void uncaughtException_throwsStackTraceWriterExceptionDuringWritingAllThreads_fileNotAddedCrashReported() throws Exception {
        //prepare
        initCrashLibConfig(false, true, null);
        when(mProcessHelper.isReportingServiceProcess(mContext)).thenReturn(true);
        when(mStackTraceWriter.writeAllStackTraces(PATH_TO_LOGS_DIR)).thenThrow(new StackTraceWriterException());

        //noinspection unchecked
        doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                mReportAndroidCrashCalled = true;
                return null;
            }
        }).when(mCrashServiceCommunicator).reportAndroidCrash(mReportData);

        //test
        mCrashUncaughtExceptionHandler = new CrashUncaughtExceptionHandler(mContext, mFactory);
        mCrashUncaughtExceptionHandler.onServiceConnected(mCrashServiceCommunicator);
        mCrashUncaughtExceptionHandler.uncaughtException(mock(Thread.class), mException);

        //verify
        assertTrue(mReportAndroidCrashCalled);

        // common
        verify(mReportData, times(1)).setCrashCode(CRASH_CODE);
        verify(mReportData, times(1)).setErrorName(mException.getClass().getSimpleName());
        verify(mReportData, times(1)).setErrorMessage(ERROR_MESSAGE);
        verify(mReportData, times(1)).setErrorDetails(ERROR_DETAILS);
        //noinspection unchecked
        verify(mReportData, times(1)).setFilesToPack(mFilesToPackCaptor.capture());
        assertThat(mFilesToPackCaptor.getValue().isEmpty(), is(true));

        // CAP
        verify(mReportData, times(1)).setCapExceptionCode(MOCK_CAP_EXCEPTION_CODE);
        verify(mReportData, times(1)).setCapFaultingOffset(MOCK_CAP_FAULTING_OFFSET);
        verify(mReportData, times(1)).setCapModuleName(MOCK_CAP_MODULE_NAME);
        verify(mReportData, times(1)).setCapModuleVersion(CrashLib.getConfig().getApplicationVersion());
        verify(mReportData, times(1)).setCapProcessName(MOCK_CURRENT_PROCESS_NAME);
        verify(mReportData, times(1)).setCapProcessVersion(BuildConfig.VERSION_NAME);
        verify(mReportData, times(1)).setCapGUID(MOCK_CAP_GUID);

        // offline
        verify(mReportData, times(1)).setPublishAttempts(NO_ATTEMPTS);
        verify(mReportData, times(1)).setLastAttemptTimestamp(NEVER);

        verifyNoMoreInteractions(mReportData);

        verifyStatic(times(1));
        android.os.Process.killProcess(MOCK_MY_PID);
    }

    @Test
    public void uncaughtException_throwsStackTraceWriterExceptionDuringWritingCrashingThread_fileNotAddedCrashReported() throws Exception {
        //prepare
        initCrashLibConfig(true, false, null);
        when(mProcessHelper.isReportingServiceProcess(mContext)).thenReturn(true);
        when(mStackTraceWriter.writeCrashingThreadStackTrace(PATH_TO_LOGS_DIR, mException)).thenThrow(new StackTraceWriterException());

        //noinspection unchecked
        doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                mReportAndroidCrashCalled = true;
                return null;
            }
        }).when(mCrashServiceCommunicator).reportAndroidCrash(mReportData);

        //test
        mCrashUncaughtExceptionHandler = new CrashUncaughtExceptionHandler(mContext, mFactory);
        mCrashUncaughtExceptionHandler.onServiceConnected(mCrashServiceCommunicator);
        mCrashUncaughtExceptionHandler.uncaughtException(mock(Thread.class), mException);

        //verify
        assertTrue(mReportAndroidCrashCalled);

        // common
        verify(mReportData, times(1)).setCrashCode(CRASH_CODE);
        verify(mReportData, times(1)).setErrorName(mException.getClass().getSimpleName());
        verify(mReportData, times(1)).setErrorMessage(ERROR_MESSAGE);
        verify(mReportData, times(1)).setErrorDetails(ERROR_DETAILS);
        //noinspection unchecked
        verify(mReportData, times(1)).setFilesToPack(mFilesToPackCaptor.capture());
        assertThat(mFilesToPackCaptor.getValue().isEmpty(), is(true));

        // CAP
        verify(mReportData, times(1)).setCapExceptionCode(MOCK_CAP_EXCEPTION_CODE);
        verify(mReportData, times(1)).setCapFaultingOffset(MOCK_CAP_FAULTING_OFFSET);
        verify(mReportData, times(1)).setCapModuleName(MOCK_CAP_MODULE_NAME);
        verify(mReportData, times(1)).setCapModuleVersion(CrashLib.getConfig().getApplicationVersion());
        verify(mReportData, times(1)).setCapProcessName(MOCK_CURRENT_PROCESS_NAME);
        verify(mReportData, times(1)).setCapProcessVersion(BuildConfig.VERSION_NAME);
        verify(mReportData, times(1)).setCapGUID(MOCK_CAP_GUID);

        // offline
        verify(mReportData, times(1)).setPublishAttempts(NO_ATTEMPTS);
        verify(mReportData, times(1)).setLastAttemptTimestamp(NEVER);

        verifyNoMoreInteractions(mReportData);

        verifyStatic(times(1));
        android.os.Process.killProcess(MOCK_MY_PID);
    }

    @Test
    public void uncaughtException_throwsCrashProviderExceptionDuringCallingSyncProviders_fileNotAddedCrashReported() throws Exception {
        //prepare
        List<ICrashSyncProvider> syncProviders = new ArrayList<>();
        mSyncProvider1 = mock(ICrashSyncProvider.class);
        when(mSyncProvider1.provideData(PATH_TO_LOGS_DIR)).thenThrow(new CrashProviderException());
        mSyncProvider2 = mock(ICrashSyncProvider.class);
        when(mSyncProvider2.provideData(PATH_TO_LOGS_DIR)).thenReturn(SYNC_PROVIDER_2_PATH);
        mSyncProvider3 = mock(ICrashSyncProvider.class);
        when(mSyncProvider3.provideData(PATH_TO_LOGS_DIR)).thenReturn(SYNC_PROVIDER_3_PATH);

        syncProviders.add(mSyncProvider1);
        syncProviders.add(mSyncProvider2);
        syncProviders.add(mSyncProvider3);

        initCrashLibConfig(true, true, syncProviders);

        when(mProcessHelper.isReportingServiceProcess(mContext)).thenReturn(true);

        //noinspection unchecked
        doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                mReportAndroidCrashCalled = true;
                return null;
            }
        }).when(mCrashServiceCommunicator).reportAndroidCrash(mReportData);

        //test
        mCrashUncaughtExceptionHandler = new CrashUncaughtExceptionHandler(mContext, mFactory);
        mCrashUncaughtExceptionHandler.onServiceConnected(mCrashServiceCommunicator);
        mCrashUncaughtExceptionHandler.uncaughtException(mock(Thread.class), mException);

        //verify
        assertTrue(mReportAndroidCrashCalled);

        // common
        verify(mReportData, times(1)).setCrashCode(CRASH_CODE);
        verify(mReportData, times(1)).setErrorName(mException.getClass().getSimpleName());
        verify(mReportData, times(1)).setErrorMessage(ERROR_MESSAGE);
        verify(mReportData, times(1)).setErrorDetails(ERROR_DETAILS);
        //noinspection unchecked
        verify(mReportData, times(1)).setFilesToPack(mFilesToPackCaptor.capture());
        assertThat(mFilesToPackCaptor.getValue().size(), is(2));
        assertThat(mFilesToPackCaptor.getValue().contains(SYNC_PROVIDER_1_PATH), is(false));
        assertThat(mFilesToPackCaptor.getValue().contains(SYNC_PROVIDER_2_PATH), is(true));
        assertThat(mFilesToPackCaptor.getValue().contains(SYNC_PROVIDER_3_PATH), is(true));
        assertThat(mFilesToPackCaptor.getValue().contains(PATH_CRASHING_STACK_TRACES), is(false));
        assertThat(mFilesToPackCaptor.getValue().contains(PATH_ALL_STACK_TRACES), is(false));

        // CAP
        verify(mReportData, times(1)).setCapExceptionCode(MOCK_CAP_EXCEPTION_CODE);
        verify(mReportData, times(1)).setCapFaultingOffset(MOCK_CAP_FAULTING_OFFSET);
        verify(mReportData, times(1)).setCapModuleName(MOCK_CAP_MODULE_NAME);
        verify(mReportData, times(1)).setCapModuleVersion(CrashLib.getConfig().getApplicationVersion());
        verify(mReportData, times(1)).setCapProcessName(MOCK_CURRENT_PROCESS_NAME);
        verify(mReportData, times(1)).setCapProcessVersion(BuildConfig.VERSION_NAME);
        verify(mReportData, times(1)).setCapGUID(MOCK_CAP_GUID);

        // offline
        verify(mReportData, times(1)).setPublishAttempts(NO_ATTEMPTS);
        verify(mReportData, times(1)).setLastAttemptTimestamp(NEVER);

        verifyNoMoreInteractions(mReportData);

        verifyStatic(times(1));
        android.os.Process.killProcess(MOCK_MY_PID);
    }

    @Test
    public void uncaughtException_reportFails_doesNotCrash() throws Exception {
        //prepare
        List<ICrashSyncProvider> syncProviders = new ArrayList<>();
        mSyncProvider1 = mock(ICrashSyncProvider.class);
        when(mSyncProvider1.provideData(PATH_TO_LOGS_DIR)).thenThrow(new CrashProviderException());
        mSyncProvider2 = mock(ICrashSyncProvider.class);
        when(mSyncProvider2.provideData(PATH_TO_LOGS_DIR)).thenReturn(SYNC_PROVIDER_2_PATH);
        mSyncProvider3 = mock(ICrashSyncProvider.class);
        when(mSyncProvider3.provideData(PATH_TO_LOGS_DIR)).thenReturn(SYNC_PROVIDER_3_PATH);

        syncProviders.add(mSyncProvider1);
        syncProviders.add(mSyncProvider2);
        syncProviders.add(mSyncProvider3);

        initCrashLibConfig(true, true, syncProviders);

        when(mProcessHelper.isReportingServiceProcess(mContext)).thenReturn(true);

        //noinspection unchecked
        doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                mReportAndroidCrashCalled = true;
                throw new RemoteException();
            }
        }).when(mCrashServiceCommunicator).reportAndroidCrash(mReportData);

        //test
        mCrashUncaughtExceptionHandler = new CrashUncaughtExceptionHandler(mContext, mFactory);
        mCrashUncaughtExceptionHandler.onServiceConnected(mCrashServiceCommunicator);
        mCrashUncaughtExceptionHandler.uncaughtException(mock(Thread.class), mException);

        //verify
        assertTrue(mReportAndroidCrashCalled);

        // common
        verify(mReportData, times(1)).setCrashCode(CRASH_CODE);
        verify(mReportData, times(1)).setErrorName(mException.getClass().getSimpleName());
        verify(mReportData, times(1)).setErrorMessage(ERROR_MESSAGE);
        verify(mReportData, times(1)).setErrorDetails(ERROR_DETAILS);
        //noinspection unchecked
        verify(mReportData, times(1)).setFilesToPack(mFilesToPackCaptor.capture());
        assertThat(mFilesToPackCaptor.getValue().size(), is(2));
        assertThat(mFilesToPackCaptor.getValue().contains(SYNC_PROVIDER_1_PATH), is(false));
        assertThat(mFilesToPackCaptor.getValue().contains(SYNC_PROVIDER_2_PATH), is(true));
        assertThat(mFilesToPackCaptor.getValue().contains(SYNC_PROVIDER_3_PATH), is(true));
        assertThat(mFilesToPackCaptor.getValue().contains(PATH_CRASHING_STACK_TRACES), is(false));
        assertThat(mFilesToPackCaptor.getValue().contains(PATH_ALL_STACK_TRACES), is(false));

        // CAP
        verify(mReportData, times(1)).setCapExceptionCode(MOCK_CAP_EXCEPTION_CODE);
        verify(mReportData, times(1)).setCapFaultingOffset(MOCK_CAP_FAULTING_OFFSET);
        verify(mReportData, times(1)).setCapModuleName(MOCK_CAP_MODULE_NAME);
        verify(mReportData, times(1)).setCapModuleVersion(CrashLib.getConfig().getApplicationVersion());
        verify(mReportData, times(1)).setCapProcessName(MOCK_CURRENT_PROCESS_NAME);
        verify(mReportData, times(1)).setCapProcessVersion(BuildConfig.VERSION_NAME);
        verify(mReportData, times(1)).setCapGUID(MOCK_CAP_GUID);

        // offline
        verify(mReportData, times(1)).setPublishAttempts(NO_ATTEMPTS);
        verify(mReportData, times(1)).setLastAttemptTimestamp(NEVER);

        verifyNoMoreInteractions(mReportData);

        verifyStatic(times(1));
        android.os.Process.killProcess(MOCK_MY_PID);
    }

    @Test
    public void uncaughtException_hashIsNull_crashNotReported() throws Exception {
        //prepare
        initCrashLibConfig(true, true, null);
        when(mHashGenerator.generateHash(mExceptionStackTrace)).thenReturn(null);
        when(mProcessHelper.isReportingServiceProcess(mContext)).thenReturn(true);

        //test
        mCrashUncaughtExceptionHandler = new CrashUncaughtExceptionHandler(mContext, mFactory);
        mCrashUncaughtExceptionHandler.onServiceConnected(mCrashServiceCommunicator);
        mCrashUncaughtExceptionHandler.uncaughtException(mock(Thread.class), mException);

        //verify
        assertFalse(mReportAndroidCrashCalled);
        //noinspection unchecked
        verify(mCrashServiceCommunicator, times(0)).reportAndroidCrash(any(ReportData.class));

        // common
        verify(mReportData, times(1)).setCrashCode(null);
        verify(mReportData, times(1)).setErrorName(mException.getClass().getSimpleName());
        verify(mReportData, times(1)).setErrorMessage(ERROR_MESSAGE);
        verify(mReportData, times(1)).setErrorDetails(ERROR_DETAILS);
        //noinspection unchecked
        verify(mReportData, times(1)).setFilesToPack(mFilesToPackCaptor.capture());
        assertThat(mFilesToPackCaptor.getValue().isEmpty(), is(true));

        // CAP
        verify(mReportData, times(1)).setCapExceptionCode(null);
        verify(mReportData, times(1)).setCapFaultingOffset(MOCK_CAP_FAULTING_OFFSET);
        verify(mReportData, times(1)).setCapModuleName(MOCK_CAP_MODULE_NAME);
        verify(mReportData, times(1)).setCapModuleVersion(CrashLib.getConfig().getApplicationVersion());
        verify(mReportData, times(1)).setCapProcessName(MOCK_CURRENT_PROCESS_NAME);
        verify(mReportData, times(1)).setCapProcessVersion(BuildConfig.VERSION_NAME);
        verify(mReportData, times(1)).setCapGUID(MOCK_CAP_GUID);

        // offline
        verify(mReportData, times(1)).setPublishAttempts(NO_ATTEMPTS);
        verify(mReportData, times(1)).setLastAttemptTimestamp(NEVER);

        verifyNoMoreInteractions(mReportData);

        verifyStatic(times(1));
        android.os.Process.killProcess(MOCK_MY_PID);
    }

    @Test
    public void uncaughtException_serviceIsNull_crashNotReported() throws Exception {
        //prepare
        initCrashLibConfig(true, true, null);
        when(mProcessHelper.isReportingServiceProcess(mContext)).thenReturn(true);

        //test
        mCrashUncaughtExceptionHandler = new CrashUncaughtExceptionHandler(mContext, mFactory);
        mCrashUncaughtExceptionHandler.uncaughtException(mock(Thread.class), mException);

        //verify
        assertFalse(mReportAndroidCrashCalled);
        //noinspection unchecked
        verify(mCrashServiceCommunicator, times(0)).reportAndroidCrash(mReportData);

        // common
        verify(mReportData, times(1)).setCrashCode(CRASH_CODE);
        verify(mReportData, times(1)).setErrorName(mException.getClass().getSimpleName());
        verify(mReportData, times(1)).setErrorMessage(ERROR_MESSAGE);
        verify(mReportData, times(1)).setErrorDetails(ERROR_DETAILS);
        //noinspection unchecked
        verify(mReportData, times(1)).setFilesToPack(mFilesToPackCaptor.capture());
        assertThat(mFilesToPackCaptor.getValue().isEmpty(), is(true));

        // CAP
        verify(mReportData, times(1)).setCapExceptionCode(MOCK_CAP_EXCEPTION_CODE);
        verify(mReportData, times(1)).setCapFaultingOffset(MOCK_CAP_FAULTING_OFFSET);
        verify(mReportData, times(1)).setCapModuleName(MOCK_CAP_MODULE_NAME);
        verify(mReportData, times(1)).setCapModuleVersion(CrashLib.getConfig().getApplicationVersion());
        verify(mReportData, times(1)).setCapProcessName(MOCK_CURRENT_PROCESS_NAME);
        verify(mReportData, times(1)).setCapProcessVersion(BuildConfig.VERSION_NAME);
        verify(mReportData, times(1)).setCapGUID(MOCK_CAP_GUID);

        // offline
        verify(mReportData, times(1)).setPublishAttempts(NO_ATTEMPTS);
        verify(mReportData, times(1)).setLastAttemptTimestamp(NEVER);

        verifyNoMoreInteractions(mReportData);

        verifyStatic(times(1));
        android.os.Process.killProcess(MOCK_MY_PID);
    }

    @Test
    public void onServiceConnected_calledMoreTimesWithTheSameCrashServiceCommunicator_serviceSetOnlyFirstTime() throws Exception {
        //prepare
        initCrashLibConfig(true, true, null);
        when(mProcessHelper.isReportingServiceProcess(mContext)).thenReturn(true);

        //test
        mCrashUncaughtExceptionHandler = new CrashUncaughtExceptionHandler(mContext, mFactory);
        mCrashUncaughtExceptionHandler.onServiceConnected(mCrashServiceCommunicator);
        mCrashUncaughtExceptionHandler.onServiceConnected(mCrashServiceCommunicator);
        mCrashUncaughtExceptionHandler.onServiceConnected(mCrashServiceCommunicator);
        mCrashUncaughtExceptionHandler.uncaughtException(mock(Thread.class), mException);

        //verify
        assertFalse(mReportAndroidCrashCalled);
        //noinspection unchecked
        verify(mCrashServiceCommunicator, times(1)).reportAndroidCrash(mReportData);

        // common
        verify(mReportData, times(1)).setCrashCode(CRASH_CODE);
        verify(mReportData, times(1)).setErrorName(mException.getClass().getSimpleName());
        verify(mReportData, times(1)).setErrorMessage(ERROR_MESSAGE);
        verify(mReportData, times(1)).setErrorDetails(ERROR_DETAILS);
        //noinspection unchecked
        verify(mReportData, times(1)).setFilesToPack(mFilesToPackCaptor.capture());
        assertThat(mFilesToPackCaptor.getValue().isEmpty(), is(true));

        // CAP
        verify(mReportData, times(1)).setCapExceptionCode(MOCK_CAP_EXCEPTION_CODE);
        verify(mReportData, times(1)).setCapFaultingOffset(MOCK_CAP_FAULTING_OFFSET);
        verify(mReportData, times(1)).setCapModuleName(MOCK_CAP_MODULE_NAME);
        verify(mReportData, times(1)).setCapModuleVersion(CrashLib.getConfig().getApplicationVersion());
        verify(mReportData, times(1)).setCapProcessName(MOCK_CURRENT_PROCESS_NAME);
        verify(mReportData, times(1)).setCapProcessVersion(BuildConfig.VERSION_NAME);
        verify(mReportData, times(1)).setCapGUID(MOCK_CAP_GUID);

        // offline
        verify(mReportData, times(1)).setPublishAttempts(NO_ATTEMPTS);
        verify(mReportData, times(1)).setLastAttemptTimestamp(NEVER);

        verifyNoMoreInteractions(mReportData);

        verifyStatic(times(1));
        android.os.Process.killProcess(MOCK_MY_PID);
    }

    @Test
    public void onServiceConnected_calledMoreTimesWithDifferentCrashServiceCommunicator_serviceSetOnlyFirstTime() throws Exception {
        //prepare
        initCrashLibConfig(true, true, null);
        when(mProcessHelper.isReportingServiceProcess(mContext)).thenReturn(true);

        ICrashServiceCommunicator crashServiceCommunicator2 = mock(ICrashServiceCommunicator.class);
        ICrashServiceCommunicator crashServiceCommunicator3 = mock(ICrashServiceCommunicator.class);

        //test
        mCrashUncaughtExceptionHandler = new CrashUncaughtExceptionHandler(mContext, mFactory);
        mCrashUncaughtExceptionHandler.onServiceConnected(mCrashServiceCommunicator);
        mCrashUncaughtExceptionHandler.onServiceConnected(crashServiceCommunicator2);
        mCrashUncaughtExceptionHandler.onServiceConnected(crashServiceCommunicator3);
        mCrashUncaughtExceptionHandler.uncaughtException(mock(Thread.class), mException);

        //verify
        assertFalse(mReportAndroidCrashCalled);
        //noinspection unchecked
        verify(mCrashServiceCommunicator, times(1)).reportAndroidCrash(mReportData);
        //noinspection unchecked
        verify(crashServiceCommunicator2, times(0)).reportAndroidCrash(mReportData);
        //noinspection unchecked
        verify(crashServiceCommunicator3, times(0)).reportAndroidCrash(mReportData);

        // common
        verify(mReportData, times(1)).setCrashCode(CRASH_CODE);
        verify(mReportData, times(1)).setErrorName(mException.getClass().getSimpleName());
        verify(mReportData, times(1)).setErrorMessage(ERROR_MESSAGE);
        verify(mReportData, times(1)).setErrorDetails(ERROR_DETAILS);
        //noinspection unchecked
        verify(mReportData, times(1)).setFilesToPack(mFilesToPackCaptor.capture());
        assertThat(mFilesToPackCaptor.getValue().isEmpty(), is(true));

        // CAP
        verify(mReportData, times(1)).setCapExceptionCode(MOCK_CAP_EXCEPTION_CODE);
        verify(mReportData, times(1)).setCapFaultingOffset(MOCK_CAP_FAULTING_OFFSET);
        verify(mReportData, times(1)).setCapModuleName(MOCK_CAP_MODULE_NAME);
        verify(mReportData, times(1)).setCapModuleVersion(CrashLib.getConfig().getApplicationVersion());
        verify(mReportData, times(1)).setCapProcessName(MOCK_CURRENT_PROCESS_NAME);
        verify(mReportData, times(1)).setCapProcessVersion(BuildConfig.VERSION_NAME);
        verify(mReportData, times(1)).setCapGUID(MOCK_CAP_GUID);

        // offline
        verify(mReportData, times(1)).setPublishAttempts(NO_ATTEMPTS);
        verify(mReportData, times(1)).setLastAttemptTimestamp(NEVER);

        verifyNoMoreInteractions(mReportData);

        verifyStatic(times(1));
        android.os.Process.killProcess(MOCK_MY_PID);
    }

}
