package com.avg.zaap.crashlibrary.offline.publisher.metadata;

import com.avg.zaap.crashlibrary.ReportData;

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.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Reader;
import java.util.Arrays;
import java.util.List;

import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
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;

/**
 * A test class for {@link CrashMetadataReader}.
 * <p/>
 * Created by marek.osvald on 5.8.2016.
 */
@RunWith(MockitoJUnitRunner.class)
public class CrashMetadataReaderTests {

    private static final char SEPARATOR = ':';

    // common
    private static final String MOCK_CRASH_CODE = "mockCrashCode";
    private static final String MOCK_ERROR_NAME = "mockErrorName";
    private static final String MOCK_ERROR_MESSAGE = "mockErrorMessage";
    @SuppressWarnings("SpellCheckingInspection")
    private static final String MOCK_ERROR_DETAILS = "mockErrorDetails\\nline1\\nline2";
    private static final String MOCK_ERROR_DETAILS_UNESCAPED = "mockErrorDetails\nline1\nline2";
    private static final int MOCK_FILES_COUNT = 3;
    private static final String MOCK_FILE_NAME_1 = "file1";
    private static final String MOCK_FILE_NAME_2 = "file2";
    private static final String MOCK_FILE_NAME_3 = "file3";
    private static final String MOCK_FILES_TO_PACK = MOCK_FILE_NAME_1 + SEPARATOR + MOCK_FILE_NAME_2 + SEPARATOR + MOCK_FILE_NAME_3;

    // CAP
    private static final String MOCK_CAP_EXCEPTION_CODE = "mockCapExceptionCode";
    private static final String MOCK_CAP_FAULTING_OFFSET = "1";
    private static final int MOCK_CAP_FAULTING_OFFSET_INT = Integer.valueOf(MOCK_CAP_FAULTING_OFFSET);
    private static final String MOCK_CAP_MODULE_NAME = "mockCapModuleName";
    private static final String MOCK_CAP_MODULE_VERSION = "mockCapModuleVersion";
    private static final String MOCK_CAP_PROCESS_NAME = "mockCapProcessName";
    private static final String MOCK_CAP_PROCESS_VERSION = "mockCapProcessVersion";
    private static final String MOCK_CAP_GUID = "1dad23aa-e7ce-4f51-8551-bc8c3d8b42e1";

    // offline
    private static final String MOCK_PUBLISH_ATTEMPTS = "1";
    private static final int MOCK_PUBLISH_ATTEMPTS_INT = Integer.valueOf(MOCK_PUBLISH_ATTEMPTS);
    private static final String MOCK_LAST_ATTEMPT_TIMESTAMP = "2";
    private static final int MOCK_LAST_ATTEMPT_TIMESTAMP_INT = Integer.valueOf(MOCK_LAST_ATTEMPT_TIMESTAMP);

    private static final String MOCK_NOT_A_NUMBER = "mockNotANumber";

    private static final String[] ANSWERS = new String[]{
            // common
            MOCK_CRASH_CODE,
            MOCK_ERROR_NAME,
            MOCK_ERROR_MESSAGE,
            MOCK_ERROR_DETAILS,
            MOCK_FILES_TO_PACK,

            // CAP
            MOCK_CAP_EXCEPTION_CODE,
            MOCK_CAP_FAULTING_OFFSET,
            MOCK_CAP_MODULE_NAME,
            MOCK_CAP_MODULE_VERSION,
            MOCK_CAP_PROCESS_NAME,
            MOCK_CAP_PROCESS_VERSION,
            MOCK_CAP_GUID,

            // offline
            MOCK_PUBLISH_ATTEMPTS,
            MOCK_LAST_ATTEMPT_TIMESTAMP
    };

    private ICrashMetadataReader mCrashMetadataReader;
    private ICrashMetadataHelper mCrashMetadataHelper;
    private File mDirectory;
    private File mFile;
    private ReportData mReportData;
    private Reader mReader;
    private BufferedReader mBufferedReader;
    private List<String> mList;

    @Captor
    private ArgumentCaptor<List> fileArrayArgumentCaptor;

    @Before
    public void setUp() throws Exception {
        mCrashMetadataHelper = mock(ICrashMetadataHelper.class);
        mDirectory = mock(File.class);
        mFile = mock(File.class);
        when(mFile.getParentFile()).thenReturn(mDirectory);
        mReportData = mock(ReportData.class);
        mReader = mock(Reader.class);
        mBufferedReader = mock(BufferedReader.class);
        //noinspection unchecked
        mList = mock(List.class);

        mCrashMetadataReader = new CrashMetadataReader(new ICrashMetadataReaderFactory() {

            @Override
            public ICrashMetadataHelper getCrashMetadataHelper() {
                return mCrashMetadataHelper;
            }

            @Override
            public File openFile(File directory, String fileName) {
                assertThat(directory, sameInstance(mDirectory));
                return mFile;
            }

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

            @Override
            public Reader getReader(File file) throws FileNotFoundException {
                assertThat(file, sameInstance(mFile));
                return mReader;
            }

            @Override
            public BufferedReader getBufferedReader(Reader reader) {
                assertThat(reader, sameInstance(mReader));
                return mBufferedReader;
            }

            @Override
            public List<String> getList(int capacity) {
                assertThat(capacity, is(MOCK_FILES_COUNT));
                return mList;
            }

        });

        initializeFile(Result.OK, 0);
        initializeReader(Result.OK);
    }

    @Test
    public void readMetadata_everythingOK_returnsReportData() throws Exception {
        // arrange

        // act
        ReportData result = mCrashMetadataReader.readMetadata(mFile);

        // assert
        assertThat(result, sameInstance(mReportData));

        // common

        verify(mReportData, times(1)).setCrashCode(MOCK_CRASH_CODE);
        verify(mReportData, times(1)).setErrorName(MOCK_ERROR_NAME);
        verify(mReportData, times(1)).setErrorMessage(MOCK_ERROR_MESSAGE);
        verify(mReportData, times(1)).setErrorDetails(MOCK_ERROR_DETAILS_UNESCAPED);
        verify(mReportData, times(1)).setFilesToPack(mList);

        //noinspection unchecked
        verify(mList, times(1)).addAll(fileArrayArgumentCaptor.capture());
        assertThat(fileArrayArgumentCaptor.getValue().size(), is(MOCK_FILES_COUNT));
        assertThat((String) fileArrayArgumentCaptor.getValue().get(0), is(MOCK_FILE_NAME_1));
        assertThat((String) fileArrayArgumentCaptor.getValue().get(1), is(MOCK_FILE_NAME_2));
        assertThat((String) fileArrayArgumentCaptor.getValue().get(2), is(MOCK_FILE_NAME_3));

        // CAP
        verify(mReportData, times(1)).setCapExceptionCode(MOCK_CAP_EXCEPTION_CODE);
        verify(mReportData, times(1)).setCapFaultingOffset(MOCK_CAP_FAULTING_OFFSET_INT);
        verify(mReportData, times(1)).setCapModuleName(MOCK_CAP_MODULE_NAME);
        verify(mReportData, times(1)).setCapModuleVersion(MOCK_CAP_MODULE_VERSION);
        verify(mReportData, times(1)).setCapProcessName(MOCK_CAP_PROCESS_NAME);
        verify(mReportData, times(1)).setCapProcessVersion(MOCK_CAP_PROCESS_VERSION);
        verify(mReportData, times(1)).setCapGUID(MOCK_CAP_GUID);

        // offline
        verify(mReportData, times(1)).setPublishAttempts(MOCK_PUBLISH_ATTEMPTS_INT);
        verify(mReportData, times(1)).setLastAttemptTimestamp(MOCK_LAST_ATTEMPT_TIMESTAMP_INT);

        verifyNoMoreInteractions(mReportData);

        verify(mBufferedReader, times(1)).close();
    }

    @Test
    public void readMetadata_IOException_returnsNull() throws Exception {
        // arrange
        initializeFile(Result.IO_EXCEPTION, 0);

        // act
        ReportData result = mCrashMetadataReader.readMetadata(mFile);

        // assert
        assertThat(result, nullValue());
    }

    @Test
    public void readMetadata_NumberFormatException_returnsNull() throws Exception {
        // arrange
        initializeFile(Result.NUMBER_FORMAT_EXCEPTION, 0);

        // act
        ReportData result = mCrashMetadataReader.readMetadata(mFile);

        // assert
        assertThat(result, nullValue());
    }

    @Test
    public void readMetadata_eof_returnsNull() throws Exception {
        for (int i = 0; i <= ANSWERS.length - 1; i++) {
            // arrange
            initializeFile(Result.EOF, i);

            // act
            ReportData result = mCrashMetadataReader.readMetadata(mFile);

            // assert
            assertThat(result, nullValue());
        }
    }

    @Test
    public void readMetadata_closeFails_returnNullDoesNotCrash() throws Exception {
        // arrange
        initializeReader(Result.IO_EXCEPTION);

        // act
        ReportData result = mCrashMetadataReader.readMetadata(mFile);

        // assert
        assertThat(result, sameInstance(mReportData));
    }

    private void initializeFile(Result result, int eofAfter) throws IOException {
        String[] answers;

        switch (result) {
            case OK:
                when(mBufferedReader.readLine()).thenReturn(ANSWERS[0], Arrays.copyOfRange(ANSWERS, 1, ANSWERS.length));
                break;

            case IO_EXCEPTION:
                when(mBufferedReader.readLine()).thenThrow(new IOException());
                break;

            case NUMBER_FORMAT_EXCEPTION:
                answers = new String[ANSWERS.length];
                System.arraycopy(ANSWERS, 0, answers, 0, ANSWERS.length);
                answers[6] = MOCK_NOT_A_NUMBER;
                when(mBufferedReader.readLine()).thenReturn(answers[0], Arrays.copyOfRange(answers, 1, answers.length));
                break;

            case EOF:
                switch (eofAfter) {
                    case 0:
                        when(mBufferedReader.readLine()).thenReturn(null);
                        break;

                    case 1:
                        when(mBufferedReader.readLine()).thenReturn(ANSWERS[0], new String[]{null});
                        break;

                    default:
                        answers = new String[eofAfter + 1];
                        System.arraycopy(ANSWERS, 0, answers, 0, eofAfter + 1);
                        answers[answers.length - 1] = null;
                        when(mBufferedReader.readLine()).thenReturn(answers[0], Arrays.copyOfRange(answers, 1, answers.length));
                }

                break;

            default:
                fail();
        }
    }

    private void initializeReader(Result result) throws IOException {
        switch (result) {
            case OK:
                doNothing().when(mBufferedReader).close();
                break;

            case IO_EXCEPTION:
                doThrow(new IOException()).when(mBufferedReader).close();
                break;

            default:
                fail();
        }
    }

    enum Result {
        OK, IO_EXCEPTION, NUMBER_FORMAT_EXCEPTION, EOF
    }

}