package com.avg.zaap.crashlibrary;

import android.content.Context;
import android.os.RemoteException;
import android.support.annotation.NonNull;

import com.avg.zaap.crashlibrary.config.CrashLibConfig;
import com.avg.zaap.crashlibrary.offline.IConnectivityHelper;
import com.avg.zaap.crashlibrary.offline.Strategy;
import com.avg.zaap.crashlibrary.offline.publisher.IOfflinePublisher;
import com.avg.zaap.crashlibrary.offline.publisher.IOfflinePublisherCallback;
import com.avg.zaap.crashlibrary.publish.ICrashPublisher;
import com.avg.zaap.crashlibrary.publish.ICrashPublisherCallback;
import com.avg.zaap.crashlibrary.publish.packer.IBinPacker;

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

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

import static junit.framework.Assert.fail;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.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;

/**
 * A test class for {@link CrashPublishingDelegate}.
 * <p/>
 * Created by marek.osvald on 8.7.2016.
 */
public class CrashPublishingDelegateTests {

    private static final String MOCK_CRASH_CODE = "mockCrashCode";

    private static final String MOCK_FILE_PATH_1 = "mockFilePath1";
    private static final String MOCK_FILE_PATH_2 = "mockFilePath2";
    private static final String MOCK_FILE_PATH_3 = "mockFilePath3";

    @SuppressWarnings("SpellCheckingInspection")
    private static final String MOCK_BINPACKED_FILE = "mockBinPackedFile";
    private static final String MOCK_FILES_DIR_ABSOLUTE_PATH = "mockFilesDirAbsolutePath";

    private static final int PUBLISHER_COUNT = 2;
    private static final int NONE = 0;

    private static final String PUBLISHER_ID_1 = "publisherID1";
    private static final String PUBLISHER_ID_2 = "publisherID2";

    private static final String MOCK_MESSAGE = "mockMessage";

    private CrashPublishingDelegate mCrashPublishingDelegate;
    private IOfflinePublisherCallback mCallbackAssert;
    private Context mContext;
    private ReportData mReportData;
    private Runnable mLastRunnable;
    private IBinPacker mBinPacker;
    @SuppressWarnings("FieldCanBeLocal")
    private File mockFilesDir;
    private File mFile1;
    private File mFile2;
    private File mFile3;
    private File mFile;
    private IConnectivityHelper mConnectivityHelper;
    private IOfflinePublisher mOfflinePublisher;
    private ICrashPublisher mCrashPublisher;
    private ICrashPublisher mCrashPublisher2;
    private Thread mThread;
    private List<String> mFilePaths;
    private ICrashServiceCallback mCallback;
    private Throwable mThrowable;

    @Before
    public void setUp() throws Exception {
        mContext = mock(Context.class);

        mFilePaths = new ArrayList<>(3);
        mFilePaths.add(MOCK_FILE_PATH_1);
        mFilePaths.add(MOCK_FILE_PATH_2);
        mFilePaths.add(MOCK_FILE_PATH_3);

        mReportData = mock(ReportData.class);
        when(mReportData.getCrashCode()).thenReturn(MOCK_CRASH_CODE);
        when(mReportData.getFilesToPack()).thenReturn(mFilePaths);

        mockFilesDir = mock(File.class);
        when(mockFilesDir.getAbsolutePath()).thenReturn(MOCK_FILES_DIR_ABSOLUTE_PATH);
        when(mContext.getFilesDir()).thenReturn(mockFilesDir);

        mFile1 = mock(File.class);
        mFile2 = mock(File.class);
        mFile3 = mock(File.class);
        mFile = mock(File.class);
        mConnectivityHelper = mock(IConnectivityHelper.class);
        mOfflinePublisher = mock(IOfflinePublisher.class);

        mThread = mock(Thread.class);
        doAnswer(new Answer<Void>() {

            @Override
            public Void answer(InvocationOnMock invocation) throws Throwable {
                mLastRunnable.run();
                return null;
            }

        }).when(mThread).start();

        mBinPacker = mock(IBinPacker.class);

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

        initializeCrashLibConfig(Strategy.ALL);

        mCallback = mock(ICrashServiceCallback.class);
        initializeCallback(Result.OK);
        initializeBinPacker(Result.OK);
        initializeFile(Result.OK);

        CrashPublishingDelegate crashPublishingDelegateInner = new CrashPublishingDelegate(mContext, new ICrashPublishingDelegateFactory() {

            @Override
            public Thread getThread(Runnable runnable) {
                mLastRunnable = runnable;
                return mThread;
            }

            @Override
            public IBinPacker getBinPacker(String filePath) throws IBinPacker.BinPackerException {
                return mBinPacker;
            }

            @Override
            public File openFile(String filePath) {
                switch (filePath) {
                    case MOCK_FILE_PATH_1:
                        return mFile1;

                    case MOCK_FILE_PATH_2:
                        return mFile2;

                    case MOCK_FILE_PATH_3:
                        return mFile3;

                    case MOCK_BINPACKED_FILE:
                        return mFile;

                    default:
                        fail();
                        return null;
                }
            }

            @Override
            public IConnectivityHelper getConnectivityHelper(Context context) {
                assertThat(context, sameInstance(mContext));
                return mConnectivityHelper;
            }

            @Override
            public IOfflinePublisher getOfflinePublisher(@NonNull IOfflinePublisherCallback
                                                                 callback) {
                mCallbackAssert = callback;
                return mOfflinePublisher;
            }

        }
        );

        mCrashPublishingDelegate = spy(crashPublishingDelegateInner);
        assertThat((CrashPublishingDelegate) mCallbackAssert, sameInstance(crashPublishingDelegateInner));
        initializeConnectivityHelper(true);
        initializeDeleteFiles(true, true, true);
    }

    @Test
    public void publishCrash_binPackerOK_binPackerOnSuccessCalled() throws Exception {
        // act
        mCrashPublishingDelegate.publishCrash(mReportData, mCallback);

        // assert
        verify(mThread, times(1 + CrashLib.getConfig().getPublishers().size())).start();
        String[] files = new String[mFilePaths.size()];
        mFilePaths.toArray(files);
        verify(mBinPacker, times(1)).pack(MOCK_CRASH_CODE, files);
        verify(mCrashPublishingDelegate, times(1)).onBinPackerSuccess(mReportData, MOCK_BINPACKED_FILE);
    }

    @Test
    public void publishOfflineCrash_nothing_incrementsAttemptsPublishesCrash() throws Exception {
        // TODO: 5.9.2016 FIX

        // act
        mCrashPublishingDelegate.publishOfflineCrash(MOCK_BINPACKED_FILE, mReportData, mCallback);

        // assert
        verify(mReportData, times(1)).incrementsPublishAttempts();
        verify(mThread, times(CrashLib.getConfig().getPublishers().size())).start();
    }

    @Test
    public void publishCrash_binPackerFailure_binPackerOnFailure() throws Exception {
        // arrange
        initializeBinPacker(Result.EXCEPTION);

        // act
        mCrashPublishingDelegate.publishCrash(mReportData, mCallback);

        // assert
        verify(mThread, times(1)).start();
        String[] files = new String[mFilePaths.size()];
        mFilePaths.toArray(files);
        verify(mBinPacker, times(1)).pack(MOCK_CRASH_CODE, files);
        verify(mCrashPublishingDelegate, times(1)).onBinPackerFailure(mReportData, mThrowable);
    }

    @Test
    public void onBinPackerSuccess_connectionSuitable_callsPublishers() throws Exception {
        // act
        mCrashPublishingDelegate.onBinPackerSuccess(mReportData, MOCK_BINPACKED_FILE);

        // assert
        verify(mThread, times(CrashLib.getConfig().getPublishers().size())).start();
        verify(mCrashPublisher, times(1)).publish(mContext, mReportData, MOCK_BINPACKED_FILE, mCrashPublishingDelegate);
        verify(mCrashPublisher2, times(1)).publish(mContext, mReportData, MOCK_BINPACKED_FILE, mCrashPublishingDelegate);
        verify(mOfflinePublisher, never()).publish(any(Context.class), any(ReportData.class), anyString());
        //noinspection ResultOfMethodCallIgnored
        verify(mFile1, times(1)).delete();
        //noinspection ResultOfMethodCallIgnored
        verify(mFile2, times(1)).delete();
        //noinspection ResultOfMethodCallIgnored
        verify(mFile3, times(1)).delete();
    }

    @Test
    public void onBinPackerSuccess_cannotDeleteFiles_callsPublishers() throws Exception {
        // arrange
        initializeDeleteFiles(false, false, false);

        // act
        mCrashPublishingDelegate.onBinPackerSuccess(mReportData, MOCK_BINPACKED_FILE);

        // assert
        verify(mThread, times(CrashLib.getConfig().getPublishers().size())).start();
        verify(mCrashPublisher, times(1)).publish(mContext, mReportData, MOCK_BINPACKED_FILE, mCrashPublishingDelegate);
        verify(mCrashPublisher2, times(1)).publish(mContext, mReportData, MOCK_BINPACKED_FILE, mCrashPublishingDelegate);
        verify(mOfflinePublisher, never()).publish(any(Context.class), any(ReportData.class), anyString());
        //noinspection ResultOfMethodCallIgnored
        verify(mFile1, times(1)).delete();
        //noinspection ResultOfMethodCallIgnored
        verify(mFile2, times(1)).delete();
        //noinspection ResultOfMethodCallIgnored
        verify(mFile3, times(1)).delete();
    }


    @Test
    public void onBinPackerSuccess_connectionNotSuitable_callsPublishers() throws Exception {
        // arrange
        initializeConnectivityHelper(false);

        // act
        mCrashPublishingDelegate.onBinPackerSuccess(mReportData, MOCK_BINPACKED_FILE);

        // assert
        verify(mThread, times(1)).start(); // just the offline publisher
        verify(mOfflinePublisher, times(1)).publish(mContext, mReportData, MOCK_BINPACKED_FILE);
        verify(mCrashPublisher, never()).publish(any(Context.class), any(ReportData.class), anyString(), any(ICrashPublisherCallback.class));
        verify(mCrashPublisher2, never()).publish(any(Context.class), any(ReportData.class), anyString(), any(ICrashPublisherCallback.class));
        //noinspection ResultOfMethodCallIgnored
        verify(mFile1, times(1)).delete();
        //noinspection ResultOfMethodCallIgnored
        verify(mFile2, times(1)).delete();
        //noinspection ResultOfMethodCallIgnored
        verify(mFile3, times(1)).delete();
    }

    @Test
    public void onBinPackerFailure_remoteException_doesNotCrash() throws Exception {
        // arrange
        initializeBinPacker(Result.EXCEPTION);
        initializeCallback(Result.EXCEPTION);

        // act
        mCrashPublishingDelegate.publishCrash(mReportData, mCallback);

        // assert
        verify(mCallback, times(1)).onPublisherResult(0, CrashLib.getConfig().getPublishers().size());
        //noinspection ResultOfMethodCallIgnored
        verify(mFile1, times(1)).delete();
        //noinspection ResultOfMethodCallIgnored
        verify(mFile2, times(1)).delete();
        //noinspection ResultOfMethodCallIgnored
        verify(mFile3, times(1)).delete();
    }

    @Test
    public void onBinPackerFailure_nothing_callsCallback() throws Exception {
        // arrange
        initializeBinPacker(Result.EXCEPTION);

        // act
        mCrashPublishingDelegate.publishCrash(mReportData, mCallback);

        // assert
        verify(mCallback, times(1)).onPublisherResult(0, CrashLib.getConfig().getPublishers().size());
    }

    @Test
    public void onCrashPublisherSuccess_strategyAllDoubleSuccess_callsCallback() throws Exception {
        // arrange
        mCrashPublishingDelegate.publishCrash(mReportData, mCallback);

        // act
        mCrashPublishingDelegate.onCrashPublisherSuccess(PUBLISHER_ID_1, mReportData);
        mCrashPublishingDelegate.onCrashPublisherSuccess(PUBLISHER_ID_2, mReportData);

        // assert
        verify(mCallback, times(1)).onPublisherResult(PUBLISHER_COUNT, NONE);
    }

    @Test
    public void onCrashPublisherSuccess_strategyAllCallbackException_doesNotCrash() throws Exception {
        // arrange
        initializeCallback(Result.EXCEPTION);
        mCrashPublishingDelegate.publishCrash(mReportData, mCallback);

        // act
        mCrashPublishingDelegate.onCrashPublisherSuccess(PUBLISHER_ID_1, mReportData);
        mCrashPublishingDelegate.onCrashPublisherSuccess(PUBLISHER_ID_2, mReportData);

        // assert
        verify(mCallback, times(1)).onPublisherResult(PUBLISHER_COUNT, NONE);
    }

    @Test
    public void onCrashPublisherSuccess_strategyAllFileDeleteFails_doesNotCrash() throws Exception {
        // arrange
        initializeFile(Result.EXCEPTION);
        mCrashPublishingDelegate.publishCrash(mReportData, mCallback);

        // act
        mCrashPublishingDelegate.onCrashPublisherSuccess(PUBLISHER_ID_1, mReportData);
        mCrashPublishingDelegate.onCrashPublisherSuccess(PUBLISHER_ID_2, mReportData);

        // assert
        verify(mCallback, times(1)).onPublisherResult(PUBLISHER_COUNT, NONE);
    }

    @Test
    public void onCrashPublisherSuccess_strategyAnyDoubleSuccess_callsCallback() throws Exception {
        // arrange
        initializeCrashLibConfig(Strategy.ANY);
        mCrashPublishingDelegate.publishCrash(mReportData, mCallback);

        // act
        mCrashPublishingDelegate.onCrashPublisherSuccess(PUBLISHER_ID_1, mReportData);
        mCrashPublishingDelegate.onCrashPublisherSuccess(PUBLISHER_ID_2, mReportData);

        // assert
        verify(mCallback, times(1)).onPublisherResult(PUBLISHER_COUNT, NONE);
    }

    @Test
    public void onCrashPublisherSuccess_strategyAnyCallbackException_doesNotCrash() throws Exception {
        // arrange
        initializeCrashLibConfig(Strategy.ANY);
        initializeCallback(Result.EXCEPTION);
        mCrashPublishingDelegate.publishCrash(mReportData, mCallback);

        // act
        mCrashPublishingDelegate.onCrashPublisherSuccess(PUBLISHER_ID_1, mReportData);
        mCrashPublishingDelegate.onCrashPublisherSuccess(PUBLISHER_ID_2, mReportData);

        // assert
        verify(mCallback, times(1)).onPublisherResult(PUBLISHER_COUNT, NONE);
    }

    @Test
    public void onCrashPublisherSuccess_strategyAnyFileDeleteFails_doesNotCrash() throws Exception {
        // arrange
        initializeCrashLibConfig(Strategy.ANY);
        initializeFile(Result.EXCEPTION);
        mCrashPublishingDelegate.publishCrash(mReportData, mCallback);

        // act
        mCrashPublishingDelegate.onCrashPublisherSuccess(PUBLISHER_ID_1, mReportData);
        mCrashPublishingDelegate.onCrashPublisherSuccess(PUBLISHER_ID_2, mReportData);

        // assert
        verify(mCallback, times(1)).onPublisherResult(PUBLISHER_COUNT, NONE);
    }

    @Test
    public void onCrashPublisherSuccess_strategyBestEffortDoubleSuccess_callsCallback() throws Exception {
        // arrange
        initializeCrashLibConfig(Strategy.BEST_EFFORT);
        mCrashPublishingDelegate.publishCrash(mReportData, mCallback);

        // act
        mCrashPublishingDelegate.onCrashPublisherSuccess(PUBLISHER_ID_1, mReportData);
        mCrashPublishingDelegate.onCrashPublisherSuccess(PUBLISHER_ID_2, mReportData);

        // assert
        verify(mCallback, times(1)).onPublisherResult(PUBLISHER_COUNT, NONE);
    }

    @Test
    public void onCrashPublisherSuccess_strategyBestEffortCallbackException_doesNotCrash() throws Exception {
        // arrange
        initializeCrashLibConfig(Strategy.BEST_EFFORT);
        initializeCallback(Result.EXCEPTION);
        mCrashPublishingDelegate.publishCrash(mReportData, mCallback);

        // act
        mCrashPublishingDelegate.onCrashPublisherSuccess(PUBLISHER_ID_1, mReportData);
        mCrashPublishingDelegate.onCrashPublisherSuccess(PUBLISHER_ID_2, mReportData);

        // assert
        verify(mCallback, times(1)).onPublisherResult(PUBLISHER_COUNT, NONE);
    }

    @Test
    public void onCrashPublisherSuccess_strategyBestEffortFileDeleteFails_doesNotCrash() throws Exception {
        // arrange
        initializeCrashLibConfig(Strategy.BEST_EFFORT);
        initializeFile(Result.EXCEPTION);
        mCrashPublishingDelegate.publishCrash(mReportData, mCallback);

        // act
        mCrashPublishingDelegate.onCrashPublisherSuccess(PUBLISHER_ID_1, mReportData);
        mCrashPublishingDelegate.onCrashPublisherSuccess(PUBLISHER_ID_2, mReportData);

        // assert
        verify(mCallback, times(1)).onPublisherResult(PUBLISHER_COUNT, NONE);
    }

    @Test
    public void onCrashPublisherFailure_strategyAllTwoFailures_callsCallback() throws Exception {
        // arrange
        mCrashPublishingDelegate.publishCrash(mReportData, mCallback);

        // act
        mCrashPublishingDelegate.onCrashPublisherFailure(PUBLISHER_ID_1, mReportData, null);
        mCrashPublishingDelegate.onCrashPublisherFailure(PUBLISHER_ID_2, mReportData, null);

        // assert
        verify(mOfflinePublisher, times(1)).publish(mContext, mReportData, MOCK_BINPACKED_FILE);
        verify(mCallback, times(1)).onPublisherResult(NONE, PUBLISHER_COUNT);
    }

    @Test
    public void onCrashPublisherFailure_strategyAllOneFailure_callsCallback() throws Exception {
        // arrange
        mCrashPublishingDelegate.publishCrash(mReportData, mCallback);

        // act
        mCrashPublishingDelegate.onCrashPublisherFailure(PUBLISHER_ID_1, mReportData, null);
        mCrashPublishingDelegate.onCrashPublisherSuccess(PUBLISHER_ID_2, mReportData);

        // assert
        verify(mOfflinePublisher, times(1)).publish(mContext, mReportData, MOCK_BINPACKED_FILE);
        verify(mCallback, times(1)).onPublisherResult(1, 1);
    }

    @Test
    public void onCrashPublisherFailure_strategyAnyTwoFailures_callsCallback() throws Exception {
        // arrange
        initializeCrashLibConfig(Strategy.ANY);
        mCrashPublishingDelegate.publishCrash(mReportData, mCallback);

        // act
        mCrashPublishingDelegate.onCrashPublisherFailure(PUBLISHER_ID_1, mReportData, null);
        mCrashPublishingDelegate.onCrashPublisherFailure(PUBLISHER_ID_2, mReportData, null);

        // assert
        verify(mOfflinePublisher, times(1)).publish(mContext, mReportData, MOCK_BINPACKED_FILE);
        verify(mCallback, times(1)).onPublisherResult(NONE, PUBLISHER_COUNT);
    }

    @Test
    public void onCrashPublisherFailure_strategyAnyOneFailure_callsCallback() throws Exception {
        // arrange
        initializeCrashLibConfig(Strategy.ANY);
        mCrashPublishingDelegate.publishCrash(mReportData, mCallback);

        // act
        mCrashPublishingDelegate.onCrashPublisherFailure(PUBLISHER_ID_1, mReportData, null);
        mCrashPublishingDelegate.onCrashPublisherSuccess(PUBLISHER_ID_2, mReportData);

        // assert
        verify(mOfflinePublisher, never()).publish(any(Context.class), any(ReportData.class), anyString());
        verify(mCallback, times(1)).onPublisherResult(1, 1);
    }

    @Test
    public void onCrashPublisherFailure_strategyBestEffortTwoFailures_callsCallback() throws Exception {
        // arrange
        initializeCrashLibConfig(Strategy.BEST_EFFORT);
        mCrashPublishingDelegate.publishCrash(mReportData, mCallback);

        // act
        mCrashPublishingDelegate.onCrashPublisherFailure(PUBLISHER_ID_1, mReportData, null);
        mCrashPublishingDelegate.onCrashPublisherFailure(PUBLISHER_ID_2, mReportData, null);

        // assert
        verify(mOfflinePublisher, never()).publish(any(Context.class), any(ReportData.class), anyString());
        verify(mCallback, times(1)).onPublisherResult(NONE, PUBLISHER_COUNT);
    }

    @Test
    public void onCrashPublisherFailure_strategyBestEffortOneFailure_callsCallback() throws Exception {
        // arrange
        initializeCrashLibConfig(Strategy.BEST_EFFORT);
        mCrashPublishingDelegate.publishCrash(mReportData, mCallback);

        // act
        mCrashPublishingDelegate.onCrashPublisherFailure(PUBLISHER_ID_1, mReportData, null);
        mCrashPublishingDelegate.onCrashPublisherSuccess(PUBLISHER_ID_2, mReportData);

        // assert
        verify(mOfflinePublisher, never()).publish(any(Context.class), any(ReportData.class), anyString());
        verify(mCallback, times(1)).onPublisherResult(1, 1);
    }

    @Test
    public void onSuccess_nothing_doesNotCrash() throws Exception {
        // act
        mCrashPublishingDelegate.onSuccess();
    }

    @Test
    public void onFailure_throwableNull_doesNotCrash() throws Exception {
        // act
        mCrashPublishingDelegate.onFailure(MOCK_MESSAGE);
    }

    @Test
    public void onFailure_nothing_doesNotCrash() throws Exception {
        // act
        mCrashPublishingDelegate.onFailure(MOCK_MESSAGE);
    }

    private void initializeCrashLibConfig(Strategy strategy) {
        CrashLib.setConfig(new CrashLibConfig.Builder()
                .publishers(new ArrayList<ICrashPublisher>() {{
                    add(mCrashPublisher);
                    add(mCrashPublisher2);
                }})
                .strategy(strategy)
                .build());
    }

    private void initializeBinPacker(Result result) throws Exception {
        String[] files = new String[mFilePaths.size()];
        mFilePaths.toArray(files);

        switch (result) {
            case OK:
                when(mBinPacker.pack(MOCK_CRASH_CODE, files)).thenReturn(MOCK_BINPACKED_FILE);
                break;

            case EXCEPTION:
                mThrowable = new IBinPacker.BinPackerException();
                when(mBinPacker.pack(MOCK_CRASH_CODE, files)).thenThrow(mThrowable);
                break;

            default:
                fail();
        }
    }

    private void initializeCallback(Result result) throws Exception {
        switch (result) {
            case OK:
                doNothing().when(mCallback).onPublisherResult(anyInt(), anyInt());
                break;

            case EXCEPTION:
                doThrow(new RemoteException()).when(mCallback).onPublisherResult(anyInt(), anyInt());
                break;

            default:
                fail();
        }
    }

    private void initializeFile(Result result) throws Exception {
        switch (result) {
            case OK:
                when(mFile.delete()).thenReturn(true);
                break;

            case EXCEPTION:
                when(mFile.delete()).thenReturn(false);
                break;

            default:
                fail();
        }
    }

    private void initializeConnectivityHelper(boolean connectionSuitable) {
        ConnectionStatus connectionStatus = mock(ConnectionStatus.class);

        when(mConnectivityHelper.getConnectionStatus()).thenReturn(connectionStatus);
        when(mConnectivityHelper.isConnectivitySuitable(connectionStatus)).thenReturn(connectionSuitable);
    }

    private void initializeDeleteFiles(boolean file1, boolean file2, boolean file3) {
        when(mFile1.delete()).thenReturn(file1);
        when(mFile2.delete()).thenReturn(file2);
        when(mFile3.delete()).thenReturn(file3);
    }

    enum Result {
        OK, EXCEPTION
    }

}
