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

import com.avg.zaap.crashlibrary.ReportData;

import org.junit.Before;
import org.junit.Test;

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

import static org.hamcrest.CoreMatchers.sameInstance;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.atLeastOnce;
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.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

/**
 * A test class for {@link CrashMetadataWriter}.
 * <p/>
 * Created by marek.osvald on 28.7.2016.
 */
public class CrashMetadataWriterTests {

    private static final String MOCK_CRASH_CODE = "mockCrashCode";
    private static final String MOCK_FILE_NAME = "mockFileName";
    private static final String MOCK_ERROR_NAME = "mockErrorName";
    private static final String MOCK_ERROR_MESSAGE = "mockErrorMessage";
    private static final String MOCK_ERROR_DETAILS = "mockErrorDetails\nline1\nline2";
    @SuppressWarnings("SpellCheckingInspection")
    private static final String MOCK_ERROR_DETAILS_ESCAPED = "mockErrorDetails\\nline1\\nline2";

    private static final int MOCK_FILES_TO_PACK_COUNT = 3;
    private static final char FILE_SEPARATOR = ':';
    private static final char NEW_LINE = '\n';
    private static final String MOCK_FILE_TO_PACK_1 = "mockFileToPack1";
    private static final String MOCK_FILE_TO_PACK_2 = "mockFileToPack2";
    private static final String MOCK_FILE_TO_PACK_3 = "mockFileToPack3";

    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 = "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 = "mockCapGUID";

    private static final int MOCK_PUBLISH_ATTEMPTS = 42;
    private static final long MOCK_LAST_PUBLISH_ATTEMPT_TIMESTAMP = 456;

    private ICrashMetadataWriter mCrashMetadataWriter;
    private ICrashMetadataHelper mCrashMetadataHelper;
    private File mDirectory;
    private File mFile;
    private OutputStreamWriter mOutputStreamWriter;
    private ReportData mReportData;

    @Before
    public void setUp() throws Exception {
        mCrashMetadataHelper = mock(ICrashMetadataHelper.class);

        mDirectory = mock(File.class);
        mFile = mock(File.class);
        when(mFile.getParentFile()).thenReturn(mDirectory);
        when(mCrashMetadataHelper.getMetadataFileName(mFile)).thenReturn(MOCK_FILE_NAME);

        mOutputStreamWriter = mock(OutputStreamWriter.class);

        mCrashMetadataWriter = new CrashMetadataWriter(new ICrashMetadataWriterFactory() {

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

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

            @Override
            public OutputStreamWriter getFileWriter(File file) throws IOException {
                assertThat(file, sameInstance(mFile));
                return mOutputStreamWriter;
            }
        });

        List<String> filesToPack = new ArrayList<>(MOCK_FILES_TO_PACK_COUNT);
        filesToPack.add(MOCK_FILE_TO_PACK_1);
        filesToPack.add(MOCK_FILE_TO_PACK_2);
        filesToPack.add(MOCK_FILE_TO_PACK_3);

        mReportData = mock(ReportData.class);

        // common
        when(mReportData.getCrashCode()).thenReturn(MOCK_CRASH_CODE);
        when(mReportData.getErrorName()).thenReturn(MOCK_ERROR_NAME);
        when(mReportData.getErrorMessage()).thenReturn(MOCK_ERROR_MESSAGE);
        when(mReportData.getErrorDetails()).thenReturn(MOCK_ERROR_DETAILS);
        when(mReportData.getFilesToPack()).thenReturn(filesToPack);

        // CAP
        when(mReportData.getCapExceptionCode()).thenReturn(MOCK_CAP_EXCEPTION_CODE);
        when(mReportData.getCapFaultingOffset()).thenReturn(MOCK_CAP_FAULTING_OFFSET);
        when(mReportData.getCapModuleName()).thenReturn(MOCK_CAP_MODULE_NAME);
        when(mReportData.getCapModuleVersion()).thenReturn(MOCK_CAP_MODULE_VERSION);
        when(mReportData.getCapProcessName()).thenReturn(MOCK_CAP_PROCESS_NAME);
        when(mReportData.getCapProcessVersion()).thenReturn(MOCK_CAP_PROCESS_VERSION);
        when(mReportData.getCapGUID()).thenReturn(MOCK_CAP_GUID);

        // offline
        when(mReportData.getPublishAttempts()).thenReturn(MOCK_PUBLISH_ATTEMPTS);
        when(mReportData.getLastAttemptTimestamp()).thenReturn(MOCK_LAST_PUBLISH_ATTEMPT_TIMESTAMP);

        initializeFile(false, true, Result.TRUE);
        initializeWriter(true, true);
    }

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

        // act
        final boolean result = mCrashMetadataWriter.writeMetadata(mFile, mReportData);

        // assert
        assertThat(result, is(true));

        // common
        verify(mOutputStreamWriter, times(1)).write(String.format("%s\n", MOCK_CRASH_CODE));
        verify(mOutputStreamWriter, times(1)).write(String.format("%s\n", MOCK_ERROR_NAME));
        verify(mOutputStreamWriter, times(1)).write(String.format("%s\n", MOCK_ERROR_MESSAGE));
        verify(mOutputStreamWriter, times(1)).write(String.format("%s\n", MOCK_ERROR_DETAILS_ESCAPED));

        // files to pack
        verify(mOutputStreamWriter, times(1)).write(MOCK_FILE_TO_PACK_1);
        verify(mOutputStreamWriter, times(1)).write(MOCK_FILE_TO_PACK_2);
        verify(mOutputStreamWriter, times(1)).write(MOCK_FILE_TO_PACK_3);
        verify(mOutputStreamWriter, times(2)).write(FILE_SEPARATOR);
        verify(mOutputStreamWriter, times(1)).write(NEW_LINE);

        // CAP
        verify(mOutputStreamWriter, times(1)).write(String.format("%s\n", MOCK_CAP_EXCEPTION_CODE));
        verify(mOutputStreamWriter, times(1)).write(String.format("%d\n", MOCK_CAP_FAULTING_OFFSET));
        verify(mOutputStreamWriter, times(1)).write(String.format("%s\n", MOCK_CAP_MODULE_NAME));
        verify(mOutputStreamWriter, times(1)).write(String.format("%s\n", MOCK_CAP_MODULE_VERSION));
        verify(mOutputStreamWriter, times(1)).write(String.format("%s\n", MOCK_CAP_PROCESS_NAME));
        verify(mOutputStreamWriter, times(1)).write(String.format("%s\n", MOCK_CAP_PROCESS_VERSION));
        verify(mOutputStreamWriter, times(1)).write(String.format("%s\n", MOCK_CAP_GUID));

        // offline
        verify(mOutputStreamWriter, times(1)).write(String.format("%d\n", MOCK_PUBLISH_ATTEMPTS));
        verify(mOutputStreamWriter, times(1)).write(String.format("%d\n", MOCK_LAST_PUBLISH_ATTEMPT_TIMESTAMP));

        verify(mOutputStreamWriter, times(1)).close();
        verifyNoMoreInteractions(mOutputStreamWriter);
    }

    @Test
    public void writeMetadata_fileExistsCannotDelete_returnsFalse() throws Exception {
        // arrange
        initializeFile(true, false, Result.FALSE);

        // act
        final boolean result = mCrashMetadataWriter.writeMetadata(mFile, mReportData);

        // assert
        assertThat(result, is(false));
        verify(mOutputStreamWriter, never()).write(anyString());
        verify(mOutputStreamWriter, never()).close();
        verifyNoMoreInteractions(mOutputStreamWriter);
    }

    @Test
    public void writeMetadata_fileExistsCannotCreateNewFile_returnsFalse() throws Exception {
        // arrange
        initializeFile(true, true, Result.FALSE);

        // act
        final boolean result = mCrashMetadataWriter.writeMetadata(mFile, mReportData);

        // assert
        assertThat(result, is(false));
        verify(mOutputStreamWriter, never()).write(anyString());
        verify(mOutputStreamWriter, never()).close();
        verifyNoMoreInteractions(mOutputStreamWriter);
    }

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

        // act
        final boolean result = mCrashMetadataWriter.writeMetadata(mFile, mReportData);

        // assert
        assertThat(result, is(false));
        verify(mOutputStreamWriter, never()).write(anyString());
        verify(mOutputStreamWriter, never()).close();
        verifyNoMoreInteractions(mOutputStreamWriter);
    }

    @Test
    public void writeMetadata_writerIOException_returnsFalse() throws Exception {
        // arrange
        initializeWriter(false, false);

        // act
        final boolean result = mCrashMetadataWriter.writeMetadata(mFile, mReportData);

        // assert
        assertThat(result, is(false));
        verify(mOutputStreamWriter, atLeastOnce()).write(anyString());
        //noinspection ResultOfMethodCallIgnored
        verify(mFile, times(1)).delete();
        verify(mOutputStreamWriter, times(1)).close();
        verifyNoMoreInteractions(mOutputStreamWriter);
    }

    @Test
    public void writeMetadata_writerIOExceptionDeleteFileOnExitFails_returnsFalse() throws Exception {
        // arrange
        initializeWriter(false, true);
        initializeFile(false, false, Result.TRUE);

        // act
        final boolean result = mCrashMetadataWriter.writeMetadata(mFile, mReportData);

        // assert
        assertThat(result, is(false));
        verify(mOutputStreamWriter, atLeastOnce()).write(anyString());
        //noinspection ResultOfMethodCallIgnored
        verify(mFile, times(1)).delete();
        verify(mOutputStreamWriter, times(1)).close();
        verifyNoMoreInteractions(mOutputStreamWriter);
    }

    private void initializeFile(boolean exists, boolean delete, Result createNewFile) throws IOException {
        when(mFile.exists()).thenReturn(exists);
        when(mFile.delete()).thenReturn(delete);

        switch (createNewFile) {
            case TRUE:
                when(mFile.createNewFile()).thenReturn(true);
                break;

            case FALSE:
                when(mFile.createNewFile()).thenReturn(false);
                break;

            case IO_EXCEPTION:
                when(mFile.createNewFile()).thenThrow(new IOException());
                break;
        }
    }

    private void initializeWriter(boolean write, boolean close) throws IOException {
        if (write) {
            doNothing().when(mOutputStreamWriter).write(anyString());
        } else {
            doThrow(new IOException()).when(mOutputStreamWriter).write(anyString());
        }

        if (close) {
            doNothing().when(mOutputStreamWriter).close();
        } else {
            doThrow(new IOException()).when(mOutputStreamWriter).close();
        }
    }

    enum Result {
        TRUE, FALSE, IO_EXCEPTION
    }

}
