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

import android.support.annotation.Nullable;
import android.support.annotation.Size;

import com.avg.zaap.crashlibrary.core.logger.ILogger;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.internal.matchers.InstanceOf;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsInstanceOf.instanceOf;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.internal.verification.VerificationModeFactory.times;
import static org.powermock.api.mockito.PowerMockito.spy;
import static org.powermock.api.mockito.PowerMockito.when;
import static org.powermock.api.mockito.PowerMockito.whenNew;

/**
 * Tests for {@link BinUnPacker}.
 * <p/>
 * Created by marek.osvald on 19.4.2016.
 */
@SuppressWarnings("SameParameterValue")
@RunWith(PowerMockRunner.class)
@PrepareForTest({BinUnPacker.class, File.class})
public class BinUnPackerTests {

    private static final int LONG_SIZE = 8;
    private static final String FLAT = "--flat";
    private static final String FORCE = "--force";
    private static final String LIST = "--list";
    // class under test
    private BinUnPacker mUnPacker;
    private BinUnPacker mSpy;

    private static final String ABSOLUTE_PATH = "abs" + File.separatorChar + "path" + File.separatorChar + "file.bp";
    private static final String DESTINATION_PATH = "someDir";
    private static final String FILE_PATH = File.separatorChar + "file.bp";
    private static final String CANONICAL_PATH = "folder";
    private static final String DEFAULT_FILE_PATH = File.separatorChar + "input.bp";
    private static final String FILE_NAME = "veryLongFileNameThatIsLongerBuffer.txt";
    private static final String FILE_DATA = "fortyTwoChars45678901234567890123456789012";
    // bigger than buffer_size
    @SuppressWarnings("SpellCheckingInspection")
    private static final String BIG_FILE_DATA =
            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +
                    "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaa" +
                    "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacaaaaaaaaaaaaaaaaaaaaaaaa" +
                    "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaadaaaaaaaaaaaaaaaaaaaaaaaa" +
                    "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaeaaaaaaaaaaaaaaaaaaaaaaaa" +
                    "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaafaaaaaaaaaaaaaaaaaaaaaaaa" +
                    "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaagaaaaaaaaaaaaaaaaaaaaaaaa" +
                    "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaagaaaaaaaaaaaaaaaaaaaaaaaa" +
                    "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaiaaaaaaaaaaaaaaaaaaaaaaaa" +
                    "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaajaaaaaaaaaaaaaaaaaaaaaaaa" +
                    "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaakaaaaaaaaaaaaaaaaaaaaaaaa";
    private static final int BUFFER_SIZE = 1024;

    private static final String FOLDER_PATH = File.separatorChar + "folder";
    private static final String FOLDER_NAME = "someNameOfFolder";

    private IBinUnPackerFactory mFactory;
    private RandomAccessFile mRandomAccessFile;
    private File mFile;
    private FileOutputStream mFileOutputStream;
    private long mCount = 0;
    private int mFileLength;
    private ArrayList<String> mPrintedMessages = new ArrayList<>();
    private int mLogsCreatedCount = 0;
    private ILogger mLogger;

    @Before
    public void setUp() throws Exception {
        mRandomAccessFile = mock(RandomAccessFile.class);
        mFile = mock(File.class);
        mFileOutputStream = mock(FileOutputStream.class);
        mLogger = mock(ILogger.class);

        mFactory = new IBinUnPackerFactory() {
            @Override
            public RandomAccessFile getReadRandomAccessFile(String path) throws FileNotFoundException {
                return mRandomAccessFile;
            }

            @Override
            public File getFile(String path) {
                return mFile;
            }

            @Override
            public FileOutputStream getFileOutputStream(File output) throws FileNotFoundException {
                assertThat(output, is(mFile));
                return mFileOutputStream;
            }

            @Override
            public ILogger getLogger() {
                mLogsCreatedCount++;
                return mLogger;
            }
        };

        doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                String tag = (String) invocation.getArguments()[0];
                String msg = (String) invocation.getArguments()[1];

                mPrintedMessages.add(msg);

                System.out.println(String.format("[%s] %s", tag, msg));
                return null;
            }
        }).when(mLogger).d(anyString(), anyString());
    }

    @Test
    public void constructor_correctInput_correctBehaviour() throws IBinUnPacker.BinUnPackerException {
        // prepare + test + verify
        prepareConstructor(DESTINATION_PATH);
    }

    @Test
    public void constructor_alreadyExists_correctBehaviour() throws IBinUnPacker.BinUnPackerException {
        // prepare
        when(mFile.exists()).thenReturn(true);

        // construction
        construct(DESTINATION_PATH);

        // verify
        //noinspection ResultOfMethodCallIgnored
        verify(mFile, times(2)).exists();
        //noinspection ResultOfMethodCallIgnored
        verify(mFile, never()).mkdirs();
    }

    @Test
    public void constructor_cannotCreate_exceptionThrown() {
        // prepare
        when(mFile.exists()).thenReturn(false);
        when(mFile.mkdirs()).thenReturn(false);

        // construction
        try {
            construct(DESTINATION_PATH);
        } catch (IBinUnPacker.BinUnPackerException e) {
            assertThat(new InstanceOf(IOException.class).matches(e.getCause()), is(true));
        }

        // verify
        //noinspection ResultOfMethodCallIgnored
        verify(mFile, times(1)).exists();
        //noinspection ResultOfMethodCallIgnored
        verify(mFile, times(1)).mkdirs();
    }

    @Test
    public void setDestinationPath_correctInput_correctBehaviour() throws IBinUnPacker.BinUnPackerException {
        // prepare
        prepareConstructor(null);

        mUnPacker.setDestinationPath(DESTINATION_PATH);
    }

    @Test
    public void unpack_fileCannotRead_exceptionThrown() throws IOException, IBinUnPacker.BinUnPackerException {
        // prepare
        prepareConstructor(DESTINATION_PATH);

        Exception exc = new IOException();
        when(mRandomAccessFile.getFilePointer()).thenThrow(exc);

        // test
        try {
            mUnPacker.unpack(FILE_PATH, false, false);
        } catch (IBinUnPacker.BinUnPackerException e) {
            assertThat(e.getCause().getCause(), is(new Exception(exc).getCause()));
        }

        // verify
        verify(mRandomAccessFile, times(1)).close();
    }

    @Test
    public void unpack_fileReadSize_exceptionThrown() throws IOException, IBinUnPacker.BinUnPackerException {
        // prepare
        prepareConstructor(DESTINATION_PATH);

        prepareInputFile(FILE_NAME, FILE_DATA);
        seek(true, null, 0);

        // test
        try {
            mUnPacker.unpack(FILE_PATH, false, false);
        } catch (IBinUnPacker.BinUnPackerException e) {
            assertThat(e.getCause(), instanceOf(IBinUnPacker.BinUnPackerException.class));
        }

        // verify
        verify(mRandomAccessFile, times(1)).close();
    }

    @Test
    public void unpack_fileReadSizeAndClose_exceptionThrown() throws IOException, IBinUnPacker.BinUnPackerException {
        // prepare
        prepareConstructor(DESTINATION_PATH);

        prepareInputFile(FILE_NAME, FILE_DATA);
        seek(true, null, 0);

        Exception exc = new IOException();
        doThrow(exc).when(mRandomAccessFile).close();

        // test
        try {
            mUnPacker.unpack(FILE_PATH, false, false);
        } catch (IBinUnPacker.BinUnPackerException e) {
            assertThat(e.getCause(), instanceOf(IBinUnPacker.BinUnPackerException.class));
        }

        // verify
        verify(mRandomAccessFile, times(1)).close();
    }

    @Test
    public void unpack_fileCorrect_correctBehaviour() throws IOException, IBinUnPacker.BinUnPackerException {
        // prepare
        prepareConstructor(DESTINATION_PATH);

        prepareInputFile(FILE_NAME, FILE_DATA);

        // test
        mUnPacker.unpack(FILE_PATH, false, false);

        // verify
        ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
        verify(mRandomAccessFile, times(2 + 1)).read(any(byte[].class));
        verify(mRandomAccessFile, times(1)).read(captor.capture(), anyInt(), anyInt());

        verify(mFileOutputStream, times(1)).write(captor.getAllValues().get(0), 0, FILE_DATA.length());
        verify(mRandomAccessFile, times(1)).close();
        verify(mFileOutputStream, times(1)).close();
    }

    @Test
    public void unpack_fileBigCorrect_correctBehaviour() throws IOException, IBinUnPacker.BinUnPackerException {
        // prepare
        prepareConstructor(DESTINATION_PATH);

        prepareInputFile(FILE_NAME, BIG_FILE_DATA);

        // test
        mUnPacker.unpack(FILE_PATH, false, false);

        // verify
        ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
        verify(mRandomAccessFile, times(2 + 1 + 1)).read(any(byte[].class));
        verify(mRandomAccessFile, times(1)).read(captor.capture(), anyInt(), anyInt());

        verify(mFileOutputStream, times(1)).write(captor.getAllValues().get(0), 0, BIG_FILE_DATA.length() - BUFFER_SIZE);
        verify(mRandomAccessFile, times(1)).close();
        verify(mFileOutputStream, times(1)).close();
    }

    @Test
    public void unpack_fileCannotClose_exceptionThrown() throws IOException, IBinUnPacker.BinUnPackerException {
        // prepare
        prepareConstructor(DESTINATION_PATH);

        prepareInputFile(FILE_NAME, FILE_DATA);

        Exception exc = new IOException();
        doThrow(exc).when(mRandomAccessFile).close();

        // test
        try {
            mUnPacker.unpack(FILE_PATH, false, false);
        } catch (IBinUnPacker.BinUnPackerException e) {
            assertThat(e.getCause(), is(new Exception(exc).getCause()));
        }

        // verify
        ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
        verify(mRandomAccessFile, times(2 + 1)).read(any(byte[].class));
        verify(mRandomAccessFile, times(1)).read(captor.capture(), anyInt(), anyInt());

        verify(mFileOutputStream, times(1)).write(captor.getAllValues().get(0), 0, FILE_DATA.length());
        verify(mRandomAccessFile, times(1)).close();
        verify(mFileOutputStream, times(1)).close();
    }

    @Test
    public void unpack_fileInnerCannotClose_exceptionThrown() throws IOException, IBinUnPacker.BinUnPackerException {
        // prepare
        prepareConstructor(DESTINATION_PATH);

        prepareInputFile(FILE_NAME, FILE_DATA);

        Exception exc = new IOException();
        doThrow(exc).when(mFileOutputStream).close();

        // test
        try {
            mUnPacker.unpack(FILE_PATH, false, false);
        } catch (IBinUnPacker.BinUnPackerException e) {
            assertThat(e.getCause().getCause(), is(new Exception(exc).getCause()));
        }

        // verify
        ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
        verify(mRandomAccessFile, times(2 + 1)).read(any(byte[].class));
        verify(mRandomAccessFile, times(1)).read(captor.capture(), anyInt(), anyInt());

        verify(mFileOutputStream, times(1)).write(captor.getAllValues().get(0), 0, FILE_DATA.length());
        verify(mRandomAccessFile, times(1)).close();
        verify(mFileOutputStream, times(1)).close();
    }

    @Test
    public void unpack_folderCorrect_correctBehaviour() throws IOException, IBinUnPacker.BinUnPackerException {
        // prepare
        prepareConstructor(DESTINATION_PATH);

        prepareInputFolder(FOLDER_NAME, new String[]{FILE_NAME, FILE_DATA});

        // test
        mUnPacker.unpack(FOLDER_PATH, false, false);

        // verify
        ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
        verify(mRandomAccessFile, times(2 + 1 + 2)).read(any(byte[].class));
        verify(mRandomAccessFile, times(1)).read(captor.capture(), anyInt(), anyInt());

        verify(mFileOutputStream, times(1)).write(captor.getAllValues().get(0), 0, FILE_DATA.length());
        verify(mRandomAccessFile, times(1)).close();
        verify(mFileOutputStream, times(1)).close();
    }

    @Test
    public void unpack_folderTwoFiles_correctBehaviour() throws IOException, IBinUnPacker.BinUnPackerException {
        // prepare
        prepareConstructor(DESTINATION_PATH);

        prepareInputFolder(FOLDER_NAME, new String[]{FILE_NAME, FILE_DATA}, new String[]{FILE_NAME, FILE_DATA});

        // test
        mUnPacker.unpack(FOLDER_PATH, false, false);

        // verify
        ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
        verify(mRandomAccessFile, times(1 + 2 + 2 + 2 + 2)).read(any(byte[].class));
        verify(mRandomAccessFile, times(1 + 1)).read(captor.capture(), anyInt(), anyInt());

        verify(mFileOutputStream, times(2)).write(captor.getAllValues().get(0), 0, FILE_DATA.length());
        verify(mRandomAccessFile, times(1)).close();
        verify(mFileOutputStream, times(2)).close();
    }

    @Test
    public void unpack_folderCorrectFileExists_correctBehaviour() throws IOException, IBinUnPacker.BinUnPackerException {
        // prepare
        prepareConstructor(DESTINATION_PATH);
        when(mFile.exists()).thenReturn(false).thenReturn(true).thenReturn(false);
        //noinspection ResultOfMethodCallIgnored
        doAnswer(new Answer() {
            private int mCounter = 3;

            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                return (mCounter-- > 0);
            }
        }).when(mFile).exists();

        prepareInputFolder(FOLDER_NAME, new String[]{FILE_NAME, FILE_DATA});

        // test
        mUnPacker.unpack(FOLDER_PATH, false, false);

        // verify
        ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
        verify(mRandomAccessFile, times(2 + 1 + 2)).read(any(byte[].class));
        verify(mRandomAccessFile, times(1)).read(captor.capture(), anyInt(), anyInt());

        verify(mFileOutputStream, times(1)).write(captor.getAllValues().get(0), 0, FILE_DATA.length());
        verify(mRandomAccessFile, times(1)).close();
        verify(mFileOutputStream, times(1)).close();
    }

    @Test
    public void unpack_folderCorrectFlat_correctBehaviour() throws IOException, IBinUnPacker.BinUnPackerException {
        // prepare
        prepareConstructor(DESTINATION_PATH);

        prepareInputFolder(FOLDER_NAME, new String[]{FILE_NAME, FILE_DATA});

        // test
        mUnPacker.unpack(FOLDER_PATH, true, false);

        // verify
        ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
        verify(mRandomAccessFile, times(2 + 1 + 2)).read(any(byte[].class));
        verify(mRandomAccessFile, times(1)).read(captor.capture(), anyInt(), anyInt());

        verify(mFileOutputStream, times(1)).write(captor.getAllValues().get(0), 0, FILE_DATA.length());
        verify(mRandomAccessFile, times(1)).close();
        verify(mFileOutputStream, times(1)).close();
    }

    @Test
    public void unpack_folderCorrectForce_correctBehaviour() throws IOException, IBinUnPacker.BinUnPackerException {
        // prepare
        prepareConstructor(DESTINATION_PATH);

        prepareInputFolder(FOLDER_NAME, new String[]{FILE_NAME, FILE_DATA});

        // test
        mUnPacker.unpack(FOLDER_PATH, false, true);

        // verify
        ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
        verify(mRandomAccessFile, times(2 + 1 + 2)).read(any(byte[].class));
        verify(mRandomAccessFile, times(1)).read(captor.capture(), anyInt(), anyInt());

        verify(mFileOutputStream, times(1)).write(captor.getAllValues().get(0), 0, FILE_DATA.length());
        verify(mRandomAccessFile, times(1)).close();
        verify(mFileOutputStream, times(1)).close();
    }

    @Test
    public void unpack_folderCannotWrite_correctBehaviour() throws IOException, IBinUnPacker.BinUnPackerException {
        // prepare
        prepareConstructor(DESTINATION_PATH);

        prepareInputFolder(FOLDER_NAME, new String[]{FILE_NAME, FILE_DATA});
        Exception exc = new IOException();
        doThrow(exc).when(mFileOutputStream).write(any(byte[].class));
        doThrow(exc).when(mFileOutputStream).write(any(byte[].class), anyInt(), anyInt());

        // test
        try {
            mUnPacker.unpack(FOLDER_PATH, false, true);
        } catch (IBinUnPacker.BinUnPackerException e) {
            assertThat(e.getCause().getCause().getCause(), is(new Exception(exc).getCause()));
        }

        // verify
        ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
        verify(mRandomAccessFile, times(2 + 2)).read(any(byte[].class));
        verify(mRandomAccessFile, times(1)).read(captor.capture(), anyInt(), anyInt());

        verify(mFileOutputStream, times(1)).write(captor.getAllValues().get(0), 0, FILE_DATA.length());
        verify(mRandomAccessFile, times(1)).close();
        verify(mFileOutputStream, times(1)).close();
    }

    @Test
    public void list_correctFileInput_correctBehaviour() throws IBinUnPacker.BinUnPackerException, IOException {
        // prepare
        prepareConstructor(null);
        prepareInputFile(FILE_NAME, FILE_DATA);

        // test
        mUnPacker.list(FILE_PATH);

        // verify
        verify(mRandomAccessFile, times(1)).close();
        verifyPrinting(FILE_NAME);
    }

    @Test
    public void list_correctFolderInput_correctBehaviour() throws IBinUnPacker.BinUnPackerException, IOException {
        // prepare
        prepareConstructor(null);
        prepareInputFolder(FOLDER_NAME, new String[]{FILE_NAME, FILE_DATA});

        // test
        mUnPacker.list(FOLDER_PATH);

        // verify
        verify(mRandomAccessFile, times(1)).close();
        verifyPrinting(FOLDER_NAME + File.separatorChar + FILE_NAME);
    }

    @Test
    public void list_correctFolderDoubleInput_correctBehaviour() throws IBinUnPacker.BinUnPackerException, IOException {
        // prepare
        prepareConstructor(null);
        prepareInputFolder(FOLDER_NAME, new String[]{FILE_NAME, FILE_DATA}, new String[]{FILE_NAME, FILE_DATA});

        // test
        mUnPacker.list(FOLDER_PATH);

        // verify
        verify(mRandomAccessFile, times(1)).close();
        verifyPrinting(FOLDER_NAME + File.separatorChar + FILE_NAME, FOLDER_NAME + File.separatorChar + FILE_NAME);
    }

    @Test
    public void list_cannotRead_exceptionThrown() throws IOException, IBinUnPacker.BinUnPackerException {
        // prepare
        prepareConstructor(null);
        prepareInputFile(FILE_NAME, FILE_DATA);

        Exception exc = new IOException();
        when(mRandomAccessFile.getFilePointer()).thenThrow(exc);

        // test
        try {
            mUnPacker.list(FILE_PATH);
        } catch (IBinUnPacker.BinUnPackerException e) {
            assertThat(e.getCause().getCause(), is(new Exception(exc).getCause()));
        }

        // verify
        verify(mRandomAccessFile, times(1)).close();
    }

    @Test
    public void list_cannotSkip_exceptionThrown() throws IOException, IBinUnPacker.BinUnPackerException {
        // prepare
        prepareConstructor(null);
        prepareInputFolder(FOLDER_NAME, new String[]{FILE_NAME, FILE_DATA});

        final Exception exc = new IOException();
        seek(false, exc, 2);

        // test
        try {
            mUnPacker.list(FOLDER_PATH);
        } catch (IBinUnPacker.BinUnPackerException e) {
            assertThat(e.getCause(), is(new Exception(exc).getCause()));
        }

        // verify
        verify(mRandomAccessFile, times(1)).close();
        verifyPrinting(FOLDER_NAME + File.separatorChar + FILE_NAME);
    }

    @Test
    public void main_0args_correctBehaviour() throws Exception {
        // prepare
        whenNew(File.class).withArguments(".").thenReturn(mFile);
        when(mFile.getCanonicalPath()).thenReturn(CANONICAL_PATH);

        prepareConstructor(DESTINATION_PATH);
        prepareInputFile(FILE_NAME, FILE_DATA);

        whenNew(BinUnPacker.class).withAnyArguments().thenReturn(mSpy);

        // test
        BinUnPacker.main(new String[]{});

        // verify
        verify(mSpy, times(1)).unpack(CANONICAL_PATH + DEFAULT_FILE_PATH, false, false);

        ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
        verify(mRandomAccessFile, times(2 + 1)).read(any(byte[].class));
        verify(mRandomAccessFile, times(1)).read(captor.capture(), anyInt(), anyInt());

        verify(mFileOutputStream, times(1)).write(captor.getAllValues().get(0), 0, FILE_DATA.length());
        verify(mRandomAccessFile, times(1)).close();
        verify(mFileOutputStream, times(1)).close();

        assertThat(mLogsCreatedCount, is(2));
    }

    @Test
    public void main_0argsCannotUnpack_correctBehaviour() throws Exception {
        // prepare
        whenNew(File.class).withArguments(".").thenReturn(mFile);
        when(mFile.getCanonicalPath()).thenReturn(CANONICAL_PATH);

        prepareConstructor(DESTINATION_PATH);
        prepareInputFile(FILE_NAME, FILE_DATA);

        Exception exc = new IBinUnPacker.BinUnPackerException();
        doThrow(exc).when(mSpy).unpack(anyString(), anyBoolean(), anyBoolean());

        whenNew(BinUnPacker.class).withAnyArguments().thenReturn(mSpy);

        // test
        try {
            BinUnPacker.main(new String[]{});
        } catch (Exception e) {
            assertThat(e.getCause(), is(new Exception(exc).getCause()));
        }

        // verify
        verify(mSpy, times(1)).unpack(CANONICAL_PATH + DEFAULT_FILE_PATH, false, false);
        assertThat(mLogsCreatedCount, is(2));
    }

    @Test
    public void main_2args_correctBehaviour() throws Exception {
        // prepare
        prepareConstructor(DESTINATION_PATH);

        prepareInputFile(FILE_NAME, FILE_DATA);

        whenNew(BinUnPacker.class).withAnyArguments().thenReturn(mSpy);

        // test
        BinUnPacker.main(new String[]{FILE_PATH, DESTINATION_PATH});

        // verify
        verify(mSpy, times(1)).unpack(FILE_PATH, false, false);

        ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
        verify(mRandomAccessFile, times(2 + 1)).read(any(byte[].class));
        verify(mRandomAccessFile, times(1)).read(captor.capture(), anyInt(), anyInt());

        verify(mFileOutputStream, times(1)).write(captor.getAllValues().get(0), 0, FILE_DATA.length());
        verify(mRandomAccessFile, times(1)).close();
        verify(mFileOutputStream, times(1)).close();
        assertThat(mLogsCreatedCount, is(2));
    }

    @Test
    public void main_2argsList_correctBehaviour() throws Exception {
        // prepare
        prepareConstructor(DESTINATION_PATH);

        prepareInputFile(FILE_NAME, FILE_DATA);

        whenNew(BinUnPacker.class).withAnyArguments().thenReturn(mSpy);

        // test
        BinUnPacker.main(new String[]{FILE_PATH, LIST});

        // verify
        verify(mSpy, times(1)).list(FILE_PATH);

        verify(mRandomAccessFile, times(2)).read(any(byte[].class));
        verify(mFileOutputStream, never()).write(any(byte[].class), anyInt(), anyInt());
        verify(mRandomAccessFile, times(1)).close();
        assertThat(mLogsCreatedCount, is(2));
    }

    @Test
    public void main_3argsFlat_correctBehaviour() throws Exception {
        // prepare
        prepareConstructor(DESTINATION_PATH);

        prepareInputFile(FILE_NAME, FILE_DATA);

        whenNew(BinUnPacker.class).withAnyArguments().thenReturn(mSpy);

        // test
        BinUnPacker.main(new String[]{FILE_PATH, DESTINATION_PATH, FLAT});

        // verify
        verify(mSpy, times(1)).unpack(FILE_PATH, true, false);

        ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
        verify(mRandomAccessFile, times(2 + 1)).read(any(byte[].class));
        verify(mRandomAccessFile, times(1)).read(captor.capture(), anyInt(), anyInt());

        verify(mFileOutputStream, times(1)).write(captor.getAllValues().get(0), 0, FILE_DATA.length());
        verify(mRandomAccessFile, times(1)).close();
        verify(mFileOutputStream, times(1)).close();
        assertThat(mLogsCreatedCount, is(2));
    }

    @Test
    public void main_3argsForce_correctBehaviour() throws Exception {
        // prepare
        prepareConstructor(DESTINATION_PATH);

        prepareInputFile(FILE_NAME, FILE_DATA);

        whenNew(BinUnPacker.class).withAnyArguments().thenReturn(mSpy);

        // test
        BinUnPacker.main(new String[]{FILE_PATH, DESTINATION_PATH, FORCE});

        // verify
        verify(mSpy, times(1)).unpack(FILE_PATH, false, true);

        ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
        verify(mRandomAccessFile, times(2 + 1)).read(any(byte[].class));
        verify(mRandomAccessFile, times(1)).read(captor.capture(), anyInt(), anyInt());

        verify(mFileOutputStream, times(1)).write(captor.getAllValues().get(0), 0, FILE_DATA.length());
        verify(mRandomAccessFile, times(1)).close();
        verify(mFileOutputStream, times(1)).close();
        assertThat(mLogsCreatedCount, is(2));
    }

    @Test
    public void main_4argsFlatForce_correctBehaviour() throws Exception {
        // prepare
        prepareConstructor(DESTINATION_PATH);

        prepareInputFile(FILE_NAME, FILE_DATA);

        whenNew(BinUnPacker.class).withAnyArguments().thenReturn(mSpy);

        // test
        BinUnPacker.main(new String[]{FILE_PATH, DESTINATION_PATH, FLAT, FORCE});

        // verify
        verify(mSpy, times(1)).unpack(FILE_PATH, true, true);

        ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
        verify(mRandomAccessFile, times(2 + 1)).read(any(byte[].class));
        verify(mRandomAccessFile, times(1)).read(captor.capture(), anyInt(), anyInt());

        verify(mFileOutputStream, times(1)).write(captor.getAllValues().get(0), 0, FILE_DATA.length());
        verify(mRandomAccessFile, times(1)).close();
        verify(mFileOutputStream, times(1)).close();
        assertThat(mLogsCreatedCount, is(2));
    }

    @Test
    public void main_incorrectArgsCount_correctBehaviour() throws Exception {
        // prepare
        prepareConstructor(DESTINATION_PATH);

        prepareInputFile(FILE_NAME, FILE_DATA);

        whenNew(BinUnPacker.class).withAnyArguments().thenReturn(mSpy);

        // test
        BinUnPacker.main(new String[]{FILE_PATH, DESTINATION_PATH, FLAT, FORCE, "something"});

        // verify
        verify(mSpy, never()).unpack(anyString(), anyBoolean(), anyBoolean());
        verify(mSpy, never()).list(anyString());
        assertThat(mLogsCreatedCount, is(2));
    }

    private void prepareInputFolder(String folderName, String[]... filenameAndDataArray) throws IOException {
        ArrayList<int[]> files = new ArrayList<>();
        // prepare files
        for (@Size(2) String[] filenameAndDataPair : filenameAndDataArray) {
            String filename = filenameAndDataPair[0];
            String data = filenameAndDataPair[1];

            int[] file = getFile(filename, data);
            mFileLength += file.length;
            files.addAll(Arrays.asList(file));
        }
        // prepare destination array
        mFileLength += folderName.length() + 1;
        final int[] folder = new int[mFileLength];

        // write folder name, add zero
        int i = 0;
        for (; i < folderName.length(); i++) {
            folder[i] = folderName.charAt(i);
        }
        i++;

        for (int[] file : files) {
            System.arraycopy(file, 0, folder, i, file.length);
            i += file.length;
        }

        Answer folderReadAnswer = getAnswerForFile(folder);
        doAnswer(folderReadAnswer).when(mRandomAccessFile).read(any(byte[].class));
        doAnswer(folderReadAnswer).when(mRandomAccessFile).read(any(byte[].class), anyInt(), anyInt());

        doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                return mCount;
            }
        }).when(mRandomAccessFile).getFilePointer();

        seek(false, null, 0);
    }

    private void prepareInputFile(String filename, String data) throws IOException {
        // defined content of file

        mFileLength = filename.length() + LONG_SIZE + data.length() + 1;
        final int[] file = getFile(filename, data);

        doAnswer(getAnswerForFile(file)).when(mRandomAccessFile).read(any(byte[].class));

        when(mRandomAccessFile.getFilePointer()).thenReturn(mCount);

        seek(false, null, 0);
    }

    private int[] getFile(String filename, String data) {
        final int[] file = new int[filename.length() + LONG_SIZE + data.length() + 1];
        int pointer = 0;
        // add zero separator at the end by post increment
        for (; pointer < filename.length(); pointer++) {
            file[pointer] = filename.charAt(pointer);
        }
        // write size
        byte[] size = ByteBuffer.allocate(Long.SIZE / Byte.SIZE).putLong(data.length()).array();
        for (int i = 0; i < LONG_SIZE; i++) {
            file[++pointer] = size[i];
        }
        // write data
        for (int i = 0; i < data.length(); i++) {
            file[++pointer] = data.charAt(i);
        }
        return file;
    }

    private Answer getAnswerForFile(final int[] file) {
        return new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                byte[] buffer = (byte[]) invocation.getArguments()[0];
                int count = buffer.length;
                if (invocation.getArguments().length > 1) {
                    count = (int) invocation.getArguments()[2];
                }
                for (int i = 0; i < buffer.length; i++) {
                    if (mCount < file.length && count-- > 0) {
                        buffer[i] = (byte) file[i + (int) mCount];
                    } else {
                        mCount += i;
                        return i;
                    }
                }
                mCount += buffer.length;
                return buffer.length;
            }
        };
    }

    private void seek(final boolean end, final Exception exc, final int count) throws IOException {
        doAnswer(new Answer() {
            private int mCounter = count;

            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                if (exc != null) {
                    if (mCounter-- <= 0) {
                        throw exc;
                    }
                }
                mCount = (!end) ? (long) invocation.getArguments()[0] : mFileLength;
                return null;
            }
        }).when(mRandomAccessFile).seek(anyLong());
    }

    private void prepareConstructor(@Nullable String path) throws IBinUnPacker.BinUnPackerException {
        // prepare
        when(mFile.exists()).thenReturn(false);
        when(mFile.mkdirs()).thenReturn(true);
        when(mFile.getAbsolutePath()).thenReturn(ABSOLUTE_PATH);

        // construction
        construct(path);

        // verify
        //noinspection ResultOfMethodCallIgnored
        verify(mFile, (path == null) ? never() : times(2)).exists();
        //noinspection ResultOfMethodCallIgnored
        verify(mFile, (path == null) ? never() : times(2)).mkdirs();
    }

    private void construct(@Nullable String path) throws IBinUnPacker.BinUnPackerException {
        if (path == null) {
            mUnPacker = new BinUnPacker(mFactory);
            mSpy = spy(new BinUnPacker(mFactory));
        } else {
            mUnPacker = new BinUnPacker(path, mFactory);
            mSpy = spy(new BinUnPacker(path, mFactory));
        }
    }

    // destructive read of found messages
    private void verifyPrinting(String... messages) {
        int allCount = 0;
        for (String msg : messages) {
            for (int i = 0; i < mPrintedMessages.size(); i++) {
                if (msg.equals(mPrintedMessages.get(i))) {
                    allCount++;
                    mPrintedMessages.set(i, "_____already read");
                    break;
                }
            }
        }
        if (allCount != messages.length) {
            throw new RuntimeException(String.format("There is too little invocations of wanted printing. Wanted: %before, Actual: %before", messages.length, allCount));
        }
    }
}