package com.avg.zaap.crashlibrary.publish.externalstorage;

import android.annotation.SuppressLint;
import android.content.Context;

import com.avg.zaap.crashlibrary.ReportData;
import com.avg.zaap.crashlibrary.publish.ICrashPublisher;
import com.avg.zaap.crashlibrary.publish.ICrashPublisherCallback;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.runners.MockitoJUnitRunner;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import static junit.framework.Assert.fail;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
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.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * A test class for the {@link ExternalStorageCrashPublisher} class.
 * <p/>
 * Created by marek.osvald on 29.4.2016.
 */
@RunWith(MockitoJUnitRunner.class)
public class ExternalStorageCrashPublisherTests {

    @SuppressLint("SdCardPath")
    private static final String FILE_PATH = "/data/data/crash/file1.txt";

    private static final int BUFFER_SIZE = 4096;
    private static final int REMAINDER = 100;
    private static final int EOF = -1;
    private static final int ZERO_OFFSET = 0;

    private ICrashPublisher mExternalStorageCrashPublisher;
    private Context mContext;
    private ReportData mReportData;
    private File mDestinationDirectory;
    private File mInputFile;
    private InputStream mInputStream;
    private File mOutputFile;
    private OutputStream mOutputStream;
    private byte[] mBuffer;
    private ICrashPublisherCallback mCallback;

    private Result mInputFileStreamResult = Result.OK;
    private Result mOutputFileStreamResult = Result.OK;

    @Captor
    ArgumentCaptor<String> mPublisherNameCaptor;
    @Captor
    ArgumentCaptor<Throwable> mThrowableCaptor;
    @Captor
    ArgumentCaptor<String> mOutputNameCaptor;
    @Captor
    ArgumentCaptor<String[]> mFilePathCaptor;
    @Captor
    ArgumentCaptor<ReportData> mReportDataCaptor;

    @Before
    public void setUp() throws Exception {
        mContext = mock(Context.class);
        mReportData = mock(ReportData.class);
        mDestinationDirectory = mock(File.class);
        mBuffer = new byte[BUFFER_SIZE];
        mInputFile = mock(File.class);
        mInputStream = mock(InputStream.class);
        mOutputFile = mock(File.class);
        mOutputStream = mock(OutputStream.class);
        when(mInputFile.delete()).thenReturn(true);
        mCallback = mock(ICrashPublisherCallback.class);

        mExternalStorageCrashPublisher = new ExternalStorageCrashPublisher(new IExternalStorageCrashPublisherFactory() {

            @Override
            public File openFile(String filePath) {
                assertThat(filePath, is(FILE_PATH));
                return mInputFile;
            }

            @Override
            public File openFile(File directory, String fileName) {
                return mOutputFile;
            }

            @Override
            public InputStream openFileInputStream(File file) throws FileNotFoundException {
                assertThat(file, is(mInputFile));

                switch (mInputFileStreamResult) {
                    case OK:
                        return mInputStream;

                    case EXCEPTION:
                        throw new FileNotFoundException();

                    default:
                        fail();
                        return null;
                }
            }

            @Override
            public OutputStream openFileOutputStream(File file) throws FileNotFoundException {
                assertThat(file, is(mOutputFile));

                switch (mOutputFileStreamResult) {
                    case OK:
                        return mOutputStream;

                    case EXCEPTION:
                        throw new FileNotFoundException();

                    default:
                        fail();
                        return null;
                }
            }

            @Override
            public byte[] getBuffer(int size) {
                assertThat(size, is(BUFFER_SIZE));
                return mBuffer;
            }

        });

        initializeCreateNewFile(Result.OK);
        initializeExternalFilesDir(Result.OK);
        initializeInputStream(Result.OK);
        initializeOutputStream(Result.OK);
        initializeOutputFile(false, true);
    }

    @Test
    public void publish_everythingOK_callsCallbackSuccess() throws Exception {
        // act
        mExternalStorageCrashPublisher.publish(mContext, mReportData, FILE_PATH, mCallback);

        // assert
        verify(mCallback, never()).onCrashPublisherFailure(anyString(), any(ReportData.class), any(Throwable.class));
        verify(mCallback, times(1)).onCrashPublisherSuccess(ExternalStorageCrashPublisher.class.getSimpleName(), mReportData);
    }

    @Test
    public void publish_outputFileAlreadyExistsDeletesFile_callsCallbackSuccess() throws Exception {
        // arrange
        initializeOutputFile(true, true);

        // act
        mExternalStorageCrashPublisher.publish(mContext, mReportData, FILE_PATH, mCallback);

        // assert
        verify(mCallback, never()).onCrashPublisherFailure(anyString(), any(ReportData.class), any(Throwable.class));
        verify(mCallback, times(1)).onCrashPublisherSuccess(ExternalStorageCrashPublisher.class.getSimpleName(), mReportData);
    }

    @Test
    public void publish_destinationNull_callsCallbackFailure() throws Exception {
        // arrange
        initializeExternalFilesDir(Result.EXCEPTION);

        // act
        mExternalStorageCrashPublisher.publish(mContext, mReportData, FILE_PATH, mCallback);

        // assert
        verify(mCallback, times(1)).onCrashPublisherFailure(ExternalStorageCrashPublisher.class.getSimpleName(), mReportData, null);
        verify(mCallback, never()).onCrashPublisherSuccess(anyString(), any(ReportData.class));
        verify(mInputStream, never()).close();
        verify(mOutputStream, never()).close();
    }

    @Test
    public void publish_outputFileAlreadyExistsUnableToDelete_callsCallbackFailure() throws Exception {
        // arrange
        initializeOutputFile(true, false);

        // act
        mExternalStorageCrashPublisher.publish(mContext, mReportData, FILE_PATH, mCallback);

        // assert
        verify(mCallback, times(1)).onCrashPublisherFailure(ExternalStorageCrashPublisher.class.getSimpleName(), mReportData, null);
        verify(mCallback, never()).onCrashPublisherSuccess(anyString(), any(ReportData.class));
        verify(mInputStream, never()).close();
        verify(mOutputStream, never()).close();
    }

    @Test
    public void publish_cannotCreateNewFile_callsCallbackFailure() throws Exception {
        // arrange
        initializeCreateNewFile(Result.EXCEPTION);

        // act
        mExternalStorageCrashPublisher.publish(mContext, mReportData, FILE_PATH, mCallback);

        // assert
        verify(mCallback, times(1)).onCrashPublisherFailure(ExternalStorageCrashPublisher.class.getSimpleName(), mReportData, null);
        verify(mCallback, never()).onCrashPublisherSuccess(anyString(), any(ReportData.class));
        verify(mInputStream, never()).close();
        verify(mOutputStream, never()).close();
    }

    @Test
    public void publish_inputFileNotFound_callsCallbackFailure() throws Exception {
        // arrange
        mInputFileStreamResult = Result.EXCEPTION;

        // act
        mExternalStorageCrashPublisher.publish(mContext, mReportData, FILE_PATH, mCallback);

        // assert
        verify(mCallback, times(1)).onCrashPublisherFailure(mPublisherNameCaptor.capture(), mReportDataCaptor.capture(), mThrowableCaptor.capture());
        assertThat(mPublisherNameCaptor.getValue(), is(ExternalStorageCrashPublisher.class.getSimpleName()));
        assertThat(mReportDataCaptor.getValue(), sameInstance(mReportData));
        assertThat(mThrowableCaptor.getValue(), instanceOf(FileNotFoundException.class));
        verify(mCallback, never()).onCrashPublisherSuccess(anyString(), any(ReportData.class));
        verify(mInputStream, never()).close();
        verify(mOutputStream, never()).close();
    }

    @Test
    public void publish_inputStreamException_callsCallbackFailure() throws Exception {
        // arrange
        initializeInputStream(Result.EXCEPTION);

        // act
        mExternalStorageCrashPublisher.publish(mContext, mReportData, FILE_PATH, mCallback);

        // assert
        verify(mCallback, times(1)).onCrashPublisherFailure(mPublisherNameCaptor.capture(), mReportDataCaptor.capture(), mThrowableCaptor.capture());
        assertThat(mPublisherNameCaptor.getValue(), is(ExternalStorageCrashPublisher.class.getSimpleName()));
        assertThat(mReportDataCaptor.getValue(), sameInstance(mReportData));
        assertThat(mThrowableCaptor.getValue(), instanceOf(IOException.class));
        verify(mCallback, never()).onCrashPublisherSuccess(anyString(), any(ReportData.class));
        verify(mInputStream, times(1)).close();
        verify(mOutputStream, times(1)).close();
    }

    @Test
    public void publish_outputStreamException_callsCallbackFailure() throws Exception {
        // arrange
        initializeOutputStream(Result.EXCEPTION);

        // act
        mExternalStorageCrashPublisher.publish(mContext, mReportData, FILE_PATH, mCallback);

        // assert
        verify(mCallback, times(1)).onCrashPublisherFailure(mPublisherNameCaptor.capture(), mReportDataCaptor.capture(), mThrowableCaptor.capture());
        assertThat(mPublisherNameCaptor.getValue(), is(ExternalStorageCrashPublisher.class.getSimpleName()));
        assertThat(mReportDataCaptor.getValue(), sameInstance(mReportData));
        assertThat(mThrowableCaptor.getValue(), instanceOf(IOException.class));
        verify(mCallback, never()).onCrashPublisherSuccess(anyString(), any(ReportData.class));
        verify(mInputStream, times(1)).close();
        verify(mOutputStream, times(1)).close();
    }

    private void initializeCreateNewFile(Result result) throws IOException {
        switch (result) {
            case OK:
                when(mOutputFile.createNewFile()).thenReturn(true);
                break;

            case EXCEPTION:
                when(mOutputFile.createNewFile()).thenReturn(false);
                break;

            default:
                fail();
        }
    }

    private void initializeExternalFilesDir(Result result) throws IOException {
        switch (result) {
            case OK:
                when(mContext.getExternalFilesDir(null)).thenReturn(mDestinationDirectory);
                break;

            case EXCEPTION:
                when(mContext.getExternalFilesDir(null)).thenReturn(null);
                break;

            default:
                fail();
        }
    }

    private void initializeInputStream(Result result) throws Exception {
        switch (result) {
            case OK:
                when(mInputStream.read(mBuffer)).thenReturn(BUFFER_SIZE, REMAINDER, EOF);
                break;

            case EXCEPTION:
                when(mInputStream.read(mBuffer)).thenThrow(new IOException());
                break;

            default:
                fail();
        }
    }

    private void initializeOutputStream(Result result) throws Exception {
        switch (result) {
            case OK:
                doNothing().when(mOutputStream).write(mBuffer, ZERO_OFFSET, BUFFER_SIZE);
                doNothing().when(mOutputStream).write(mBuffer, ZERO_OFFSET, REMAINDER);
                doNothing().when(mOutputStream).write(mBuffer, ZERO_OFFSET, EOF);
                break;

            case EXCEPTION:
                doThrow(new IOException()).when(mOutputStream).write(mBuffer, ZERO_OFFSET, BUFFER_SIZE);
                doThrow(new IOException()).when(mOutputStream).write(mBuffer, ZERO_OFFSET, REMAINDER);
                doThrow(new IOException()).when(mOutputStream).write(mBuffer, ZERO_OFFSET, EOF);
                break;

            default:
                fail();
        }
    }

    private void initializeOutputFile(boolean exists, boolean delete) {
        when(mOutputFile.exists()).thenReturn(exists);
        when(mOutputFile.delete()).thenReturn(delete);
    }

    enum Result {
        OK, EXCEPTION
    }

}
