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

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

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.mockito.stubbing.OngoingStubbing;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsNull.nullValue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
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.verify;
import static org.mockito.internal.verification.VerificationModeFactory.times;
import static org.powermock.api.mockito.PowerMockito.mockStatic;
import static org.powermock.api.mockito.PowerMockito.when;

/**
 * Tests for {@link BinPacker} class.
 * <p/>
 * Created by marek.osvald on 18.4.2016.
 */
@RunWith(PowerMockRunner.class)
@PrepareForTest({BinPacker.class, CrashLogger.class, BinPacker.FileWalker.class, URI.class})
public class BinPackerTests {

    private static final String PATH = "some " + File.separatorChar + "path";
    private static final String FILE_PATH = "file.txt";
    private static final String FILE_PATH_EXTENSION_FREE = "file";
    @SuppressWarnings("SpellCheckingInspection")
    private static final String FILE_PATH_EXTENSION_TEMP = FILE_PATH_EXTENSION_FREE + ".tempext";
    private static final String FOLDER_PATH = "file";
    private static final String FOLDER_LONG_PATH = "path" + File.separatorChar + "file";
    private static final String FILE_RESULT = "file.bp";
    private static final long LENGTH = 42;
    private static final byte[] CONVERTED_LENGTH = new byte[]{
            0, 0, 0, 0, 0, 0, 0, 42
    };
    private static final String ABSOLUTE_PATH = "something" + File.separatorChar + PATH;
    private static final String RELATIVE_PATH = File.separatorChar + "some" + File.separatorChar;
    private static final int READ_COUNT = 5;
    private static final byte NAME_SEPARATOR = 0;

    // class under test
    private BinPacker mBinPacker;
    private BinPacker mSpyPacker;

    private File mFile;
    private FileOutputStream mFileOutputStream;
    private FileInputStream mFileInputStream;
    private IBinPackerFactory mFactory;
    private BinPacker.FileWalker.IFileWalkerAction mAction;
    private OngoingStubbing<Boolean> mOnGoingStubbingFile;
    private OngoingStubbing<Boolean> mOnGoingStubbingFolder;

    @Before
    public void setUp() throws Exception {
        mFile = mock(File.class);
        mFileOutputStream = mock(FileOutputStream.class);
        mFileInputStream = mock(FileInputStream.class);

        mFactory = new IBinPackerFactory() {
            @Override
            public File getFile(String path) {
                return mFile;
            }

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

            @Override
            public FileInputStream getFileInputStream(File input) throws FileNotFoundException {
                return mFileInputStream;
            }

            @Override
            public FileInputStream getFileInputStream(String path) throws FileNotFoundException {
                return mFileInputStream;
            }
        };

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

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

    @Test
    public void constructor_existingDestination_correctBehaviour() throws IBinPacker.BinPackerException {
        // prepare
        when(mFile.exists()).thenReturn(true);

        // test
        construct();

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

    @Test
    public void constructor_nonExistingDestination_correctBehaviour() throws IBinPacker.BinPackerException {
        // prepare
        when(mFile.exists()).thenReturn(false);
        when(mFile.mkdirs()).thenReturn(true);

        // test
        construct();

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

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

        // test
        try {
            construct();
        } catch (IBinPacker.BinPackerException e) {
            assertThat(e.getCause(), nullValue());
        }

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

    @Test
    public void packFile_isNotFile_exceptionThrown() throws IBinPacker.BinPackerException {
        // prepare
        prepareConstruct();

        prepareFileArbiter(false);

        // test
        try {
            mBinPacker.packFile(FILE_PATH, false);
        } catch (IBinPacker.BinPackerException e) {
            assertThat(e.getCause(), nullValue());
        }
    }

    @Test
    public void packFile_correctInput_correctBehaviour() throws IBinPacker.BinPackerException, IOException {
        // prepare
        prepareConstruct();

        prepareFileArbiter(true);

        when(mFileInputStream.read(any(byte[].class))).thenReturn(READ_COUNT).thenReturn(0);

        // test
        String result = mBinPacker.packFile(FILE_PATH, false);

        // verify
        assertThat(result, is(ABSOLUTE_PATH + File.separatorChar + FILE_RESULT));

        verify(mFileOutputStream, times(1)).write(FILE_PATH.getBytes());
        verify(mFileOutputStream, times(1)).write(NAME_SEPARATOR);
        ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
        verify(mFileOutputStream, times(2)).write(captor.capture());
        assertThat(captor.getValue(), is(CONVERTED_LENGTH));

        //noinspection ResultOfMethodCallIgnored
        verify(mFileInputStream, times(2)).read(any(byte[].class));
        verify(mFileInputStream, times(1)).close();
        verify(mFileOutputStream, times(1)).close();

    }

    @Test
    public void packFile_correctInputForceMode_correctBehaviour() throws IBinPacker.BinPackerException, IOException {
        // prepare
        prepareConstruct();

        prepareFileArbiter(true);

        when(mFileInputStream.read(any(byte[].class))).thenReturn(READ_COUNT).thenReturn(0);
        when(mFile.exists()).thenReturn(true).thenReturn(false);

        // test
        String result = mBinPacker.packFile(FILE_PATH_EXTENSION_FREE, true);

        // verify
        assertThat(result, is(ABSOLUTE_PATH + File.separatorChar + FILE_RESULT));

        verify(mFileOutputStream, times(1)).write(FILE_PATH_EXTENSION_TEMP.getBytes());
        verify(mFileOutputStream, times(1)).write(NAME_SEPARATOR);
        ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
        verify(mFileOutputStream, times(2)).write(captor.capture());
        assertThat(captor.getValue(), is(CONVERTED_LENGTH));

        //noinspection ResultOfMethodCallIgnored
        verify(mFileInputStream, times(2)).read(any(byte[].class));
        verify(mFileInputStream, times(1)).close();
        verify(mFileOutputStream, times(1)).close();
    }

    @Test
    public void packFile_cannotRead_exceptionThrown() throws IOException, IBinPacker.BinPackerException {
        // prepare
        prepareConstruct();

        prepareFileArbiter(true);

        Exception exc = new IOException();
        //noinspection unchecked
        when(mFileInputStream.read(any(byte[].class))).thenReturn(READ_COUNT).thenThrow(exc);

        // test
        try {
            mBinPacker.packFile(FILE_PATH_EXTENSION_FREE, false);
        } catch (IBinPacker.BinPackerException e) {
            assertThat(e.getCause().getCause(), is(new Exception(exc).getCause()));
        }

        // verify
        verify(mFileInputStream, times(1)).close();
        verify(mFileOutputStream, times(1)).close();
    }

    @Test
    public void packFile_cannotClose_exceptionThrown() throws IOException, IBinPacker.BinPackerException {
        // prepare
        prepareConstruct();

        prepareFileArbiter(true);

        Exception exc = new IOException();
        //noinspection unchecked
        when(mFileInputStream.read(any(byte[].class))).thenReturn(READ_COUNT).thenReturn(0);
        doThrow(exc).when(mFileOutputStream).close();

        // test
        try {
            mBinPacker.packFile(FILE_PATH_EXTENSION_FREE, false);
        } catch (IBinPacker.BinPackerException e) {
            assertThat(e.getCause(), is(new Exception(exc).getCause()));
        }

        // verify
        verify(mFileInputStream, times(1)).close();
        verify(mFileOutputStream, times(1)).close();
    }

    @Test
    public void packFile_cannotReadAndClose_exceptionThrown() throws IOException, IBinPacker.BinPackerException {
        // prepare
        prepareConstruct();

        prepareFileArbiter(true);

        Exception exc = new IOException();
        Exception exc2 = new IOException();
        //noinspection unchecked
        when(mFileInputStream.read(any(byte[].class))).thenReturn(READ_COUNT).thenThrow(exc);
        doThrow(exc2).when(mFileOutputStream).close();

        // test
        try {
            mBinPacker.packFile(FILE_PATH_EXTENSION_FREE, false);
        } catch (IBinPacker.BinPackerException e) {
            // first one is thrown for now
            assertThat(e.getCause().getCause(), is(new Exception(exc).getCause()));
        }

        // verify
        verify(mFileInputStream, times(1)).close();
        verify(mFileOutputStream, times(1)).close();
    }

    @Test
    public void packFolder_isNotFolder_exceptionThrown() throws IBinPacker.BinPackerException {
        // prepare
        prepareConstruct();

        prepareFileArbiter(true);

        // test
        try {
            mBinPacker.packFolder(FOLDER_PATH, false);
        } catch (IBinPacker.BinPackerException e) {
            assertThat(e.getCause(), nullValue());
        }
    }

    @Test
    public void packFolder_correctInputEmpty_correctBehaviour() throws IBinPacker.BinPackerException, IOException {
        // prepare
        prepareConstruct();

        prepareFileArbiter(false);

        when(mFileInputStream.read(any(byte[].class))).thenReturn(READ_COUNT).thenReturn(0);
        mockStatic(BinPacker.FileWalker.class);

        // test
        String result = mBinPacker.packFolder(FOLDER_PATH, false);

        // verify
        assertThat(result, is(ABSOLUTE_PATH + File.separatorChar + FILE_RESULT));

        verify(mFileOutputStream, times(1)).write(FOLDER_PATH.getBytes());
        verify(mFileOutputStream, times(1)).write(NAME_SEPARATOR);

        verify(mFileOutputStream, times(1)).close();
        verify(mFileInputStream, never()).close();
    }

    @Test
    public void packFolder_correctInputEmptyFolder_correctBehaviour() throws IBinPacker.BinPackerException, IOException {
        // prepare
        prepareConstruct();

        prepareFileArbiter(false);

        when(mFileInputStream.read(any(byte[].class))).thenReturn(READ_COUNT).thenReturn(0);
        mockStatic(BinPacker.FileWalker.class);
        PowerMockito.doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                mAction = (BinPacker.FileWalker.IFileWalkerAction) invocation.getArguments()[1];
                return null;
            }
        }).when(BinPacker.FileWalker.class);
        BinPacker.FileWalker.walk(any(File.class), any(BinPacker.FileWalker.IFileWalkerAction.class));
        prepareURI();

        // test
        String result = mBinPacker.packFolder(FOLDER_PATH, false);
        // nothing for folder => empty folder
        mAction.actFolder(mFile);

        // verify
        assertThat(result, is(ABSOLUTE_PATH + File.separatorChar + FILE_RESULT));

        verify(mFileOutputStream, times(1)).write(FOLDER_PATH.getBytes());
        verify(mFileOutputStream, times(1)).write(NAME_SEPARATOR);
        verify(mFileOutputStream, times(1)).close();
    }

    @Test
    public void packFolder_correctInputFileInside_correctBehaviour() throws IBinPacker.BinPackerException, IOException {
        // prepare
        prepareConstruct();

        prepareFileArbiter(false);

        when(mFileInputStream.read(any(byte[].class))).thenReturn(READ_COUNT).thenReturn(0);
        mockStatic(BinPacker.FileWalker.class);
        PowerMockito.doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                mAction = (BinPacker.FileWalker.IFileWalkerAction) invocation.getArguments()[1];
                return null;
            }
        }).when(BinPacker.FileWalker.class);
        BinPacker.FileWalker.walk(any(File.class), any(BinPacker.FileWalker.IFileWalkerAction.class));
        prepareURI();

        // test
        String result = mBinPacker.packFolder(FOLDER_PATH, false);
        // nothing for folder => empty folder
        mAction.actFile(mFile);

        // verify
        assertThat(result, is(ABSOLUTE_PATH + File.separatorChar + FILE_RESULT));

        verify(mFileOutputStream, times(1)).write(FOLDER_PATH.getBytes());
        verify(mFileOutputStream, times(1)).write(RELATIVE_PATH.getBytes());
        verify(mFileOutputStream, times(2)).write(NAME_SEPARATOR);

        ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
        verify(mFileOutputStream, times(3)).write(captor.capture());
        assertThat(captor.getValue(), is(CONVERTED_LENGTH));

        //noinspection ResultOfMethodCallIgnored
        verify(mFileInputStream, times(2)).read(any(byte[].class));
        verify(mFileInputStream, times(1)).close();
        verify(mFileOutputStream, times(1)).close();
    }

    @Test
    public void packFolder_cannotReadFile_exceptionThrown() throws IOException, IBinPacker.BinPackerException {
        // prepare
        prepareConstruct();

        prepareFileArbiter(false);

        Exception exc = new IOException();
        when(mFileInputStream.read(any(byte[].class))).thenReturn(READ_COUNT).thenThrow(exc);
        mockStatic(BinPacker.FileWalker.class);
        PowerMockito.doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                mAction = (BinPacker.FileWalker.IFileWalkerAction) invocation.getArguments()[1];
                return null;
            }
        }).when(BinPacker.FileWalker.class);
        BinPacker.FileWalker.walk(any(File.class), any(BinPacker.FileWalker.IFileWalkerAction.class));
        prepareURI();

        // test
        String result = mBinPacker.packFolder(FOLDER_PATH, false);
        // nothing for folder => empty folder
        try {
            mAction.actFile(mFile);
        } catch (IBinPacker.BinPackerException e) {
            assertThat(e.getCause().getCause(), is(new Exception(exc).getCause()));
        }

        // verify
        assertThat(result, is(ABSOLUTE_PATH + File.separatorChar + FILE_RESULT));

        verify(mFileOutputStream, times(1)).write(FOLDER_PATH.getBytes());
        verify(mFileOutputStream, times(1)).write(RELATIVE_PATH.getBytes());
        verify(mFileOutputStream, times(2)).write(NAME_SEPARATOR);

        ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
        verify(mFileOutputStream, times(3)).write(captor.capture());
        assertThat(captor.getValue(), is(CONVERTED_LENGTH));

        //noinspection ResultOfMethodCallIgnored
        verify(mFileInputStream, times(2)).read(any(byte[].class));
        verify(mFileInputStream, times(1)).close();
        verify(mFileOutputStream, times(1)).close();
    }

    @Test
    public void packFolder_cannotWriteRoot_exceptionThrown() throws IOException, IBinPacker.BinPackerException {
        // prepare
        prepareConstruct();

        prepareFileArbiter(false);

        Exception exc = new IOException();
        doThrow(exc).when(mFileOutputStream).write((byte) 0);

        prepareURI();

        // test
        try {
            mBinPacker.packFolder(FOLDER_PATH, false);
        } catch (IBinPacker.BinPackerException e) {
            assertThat(e.getCause().getCause(), is(new Exception(exc).getCause()));
        }

        // verify
        verify(mFileOutputStream, times(1)).write(FOLDER_PATH.getBytes());
        verify(mFileOutputStream, times(1)).close();
    }

    @Test
    public void packFolder_cannotWalk_exceptionThrown() throws IOException, IBinPacker.BinPackerException {
        // prepare
        prepareConstruct();

        prepareFileArbiter(false);
        // no exception wrapping
        Exception exc = new IBinPacker.BinPackerException();
        mockStatic(BinPacker.FileWalker.class);
        PowerMockito.doThrow(exc).when(BinPacker.FileWalker.class);
        BinPacker.FileWalker.walk(any(File.class), any(BinPacker.FileWalker.IFileWalkerAction.class));

        prepareURI();

        // test
        try {
            mBinPacker.packFolder(FOLDER_PATH, false);
        } catch (IBinPacker.BinPackerException e) {
            assertThat(e.getCause(), is(new Exception(exc).getCause()));
        }

        // verify
        verify(mFileOutputStream, times(1)).write(FOLDER_PATH.getBytes());
        verify(mFileOutputStream, times(1)).close();
    }

    @Test
    public void packAll_correctInput_correctBehaviour() throws IBinPacker.BinPackerException, IOException {
        // prepare
        prepareConstruct();

        prepareFileArbiter(false, true);

        when(mFileInputStream.read(any(byte[].class))).thenReturn(READ_COUNT).thenReturn(0);
        mockStatic(BinPacker.FileWalker.class);
        PowerMockito.doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                mAction = (BinPacker.FileWalker.IFileWalkerAction) invocation.getArguments()[1];
                return null;
            }
        }).when(BinPacker.FileWalker.class);
        BinPacker.FileWalker.walk(any(File.class), any(BinPacker.FileWalker.IFileWalkerAction.class));

        prepareURI();

        // test
        String result = mBinPacker.packAll(FOLDER_PATH, new ArrayList<String>() {{
            add(FOLDER_LONG_PATH);
            add(FILE_PATH);
        }}, false);

        mAction.actFolder(mFile);


        // verify
        assertThat(result, is(ABSOLUTE_PATH + File.separatorChar + FILE_RESULT));

        verify(mFileOutputStream, times(1)).write(FOLDER_PATH.getBytes());
        verify(mFileOutputStream, times(1)).write(FILE_PATH.getBytes());
        verify(mFileOutputStream, times(2)).write(NAME_SEPARATOR);

        ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
        verify(mFileOutputStream, times(3)).write(captor.capture());
        assertThat(captor.getValue(), is(CONVERTED_LENGTH));

        verify(mFileOutputStream, times(1)).close();
        verify(mFileInputStream, times(1)).close();
    }

    @Test
    public void packAll_cannotRead_correctBehaviour() throws IOException, IBinPacker.BinPackerException {
        // prepare
        prepareConstruct();

        prepareFileArbiter(false, true);

        Exception exc = new IOException();
        when(mFileInputStream.read(any(byte[].class))).thenReturn(READ_COUNT).thenThrow(exc);
        mockStatic(BinPacker.FileWalker.class);
        PowerMockito.doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                mAction = (BinPacker.FileWalker.IFileWalkerAction) invocation.getArguments()[1];
                return null;
            }
        }).when(BinPacker.FileWalker.class);
        BinPacker.FileWalker.walk(any(File.class), any(BinPacker.FileWalker.IFileWalkerAction.class));

        prepareURI();

        // test
        try {
            mBinPacker.packAll(FOLDER_PATH, new ArrayList<String>() {{
                add(FOLDER_LONG_PATH);
                add(FILE_PATH);
            }}, false);
        } catch (IBinPacker.BinPackerException e) {
            assertThat(e.getCause().getCause().getCause(), is(new Exception(exc).getCause()));
        }

        mAction.actFolder(mFile);

        // verify
        verify(mFileOutputStream, times(1)).write(FOLDER_PATH.getBytes());
        verify(mFileOutputStream, times(1)).write(FILE_PATH.getBytes());
        verify(mFileOutputStream, times(2)).write(NAME_SEPARATOR);

        ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
        verify(mFileOutputStream, times(3)).write(captor.capture());
        assertThat(captor.getValue(), is(CONVERTED_LENGTH));

        verify(mFileOutputStream, times(1)).close();
        verify(mFileInputStream, times(1)).close();
    }

    @Test
    public void packAll_cannotWalk_correctBehaviour() throws IOException, IBinPacker.BinPackerException {
        // prepare
        prepareConstruct();

        prepareFileArbiter(false, true);

        Exception exc = new IBinPacker.BinPackerException();
        mockStatic(BinPacker.FileWalker.class);
        PowerMockito.doThrow(exc).when(BinPacker.FileWalker.class);
        BinPacker.FileWalker.walk(any(File.class), any(BinPacker.FileWalker.IFileWalkerAction.class));

        prepareURI();

        // test
        try {
            mBinPacker.packAll(FOLDER_PATH, new ArrayList<String>() {{
                add(FOLDER_LONG_PATH);
                add(FILE_PATH);
            }}, false);
        } catch (IBinPacker.BinPackerException e) {
            assertThat(e.getCause(), is(new Exception(exc).getCause()));
        }

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

    @Test
    public void packAll_cannotReadInWalk_correctBehaviour() throws IOException, IBinPacker.BinPackerException {
        // prepare
        prepareConstruct();

        prepareFileArbiter(false, true);

        Exception exc = new IOException();
        mockStatic(BinPacker.FileWalker.class);
        PowerMockito.doThrow(exc).when(BinPacker.FileWalker.class);
        BinPacker.FileWalker.walk(any(File.class), any(BinPacker.FileWalker.IFileWalkerAction.class));

        prepareURI();

        // test
        try {
            mBinPacker.packAll(FOLDER_PATH, new ArrayList<String>() {{
                add(FOLDER_LONG_PATH);
                add(FILE_PATH);
            }}, false);
        } catch (IBinPacker.BinPackerException e) {
            assertThat(e.getCause().getCause(), is(new Exception(exc).getCause()));
        }

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

    @Test
    public void packAll_emptyPaths_correctBehaviour() throws IBinPacker.BinPackerException {
        // prepare
        prepareConstruct();

        // test
        try {
            mBinPacker.packAll(FOLDER_PATH, new ArrayList<String>(), false);
        } catch (IBinPacker.BinPackerException e) {
            assertThat(e.getCause(), is(nullValue()));
        }
    }

    @Test
    public void packAll_ArrayInput_correctBehaviour() throws IBinPacker.BinPackerException, IOException {
        // prepare
        prepareConstruct();

        prepareFileArbiter(false, true);

        when(mFileInputStream.read(any(byte[].class))).thenReturn(READ_COUNT).thenReturn(0);
        mockStatic(BinPacker.FileWalker.class);
        PowerMockito.doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                mAction = (BinPacker.FileWalker.IFileWalkerAction) invocation.getArguments()[1];
                return null;
            }
        }).when(BinPacker.FileWalker.class);
        BinPacker.FileWalker.walk(any(File.class), any(BinPacker.FileWalker.IFileWalkerAction.class));

        prepareURI();

        // test
        String result = mSpyPacker.packAll(FOLDER_PATH, false, FOLDER_LONG_PATH, FILE_PATH);

        mAction.actFolder(mFile);

        // verify
        verify(mSpyPacker, times(1)).packAll(FOLDER_PATH, new ArrayList<String>() {{
            add(FOLDER_LONG_PATH);
            add(FILE_PATH);
        }}, false);
        assertThat(result, is(ABSOLUTE_PATH + File.separatorChar + FILE_RESULT));
    }

    @Test
    public void pack_file_correctBehaviour() throws IBinPacker.BinPackerException, IOException {
        // prepare
        prepareConstruct();

        prepareFileArbiter(true);

        when(mFileInputStream.read(any(byte[].class))).thenReturn(READ_COUNT).thenReturn(0);
        ArgumentCaptor<String> outputCaptor = ArgumentCaptor.forClass(String.class);
        ArgumentCaptor<ArrayList> pathsCaptor = ArgumentCaptor.forClass(ArrayList.class);
        ArgumentCaptor<Boolean> forceModeCaptor = ArgumentCaptor.forClass(Boolean.class);

        // test
        String result = mSpyPacker.pack(false, FOLDER_PATH, FILE_PATH);

        // verify
        //noinspection unchecked
        verify(mSpyPacker, times(1)).packAll(outputCaptor.capture(), pathsCaptor.capture(), forceModeCaptor.capture());
        assertThat(outputCaptor.getValue(), is(FOLDER_PATH));
        assertThat(pathsCaptor.getValue().size(), is(1));
        assertThat((String) pathsCaptor.getValue().get(0), is(FILE_PATH));
        assertThat(forceModeCaptor.getValue(), is(false));
        assertThat(result, is(ABSOLUTE_PATH + File.separatorChar + FILE_RESULT));
    }

    @Test
    public void pack_folder_correctBehaviour() throws IBinPacker.BinPackerException, IOException {
        // prepare
        prepareConstruct();

        prepareFileArbiter(false);

        when(mFileInputStream.read(any(byte[].class))).thenReturn(READ_COUNT).thenReturn(0);
        mockStatic(BinPacker.FileWalker.class);
        PowerMockito.doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                mAction = (BinPacker.FileWalker.IFileWalkerAction) invocation.getArguments()[1];
                return null;
            }
        }).when(BinPacker.FileWalker.class);
        BinPacker.FileWalker.walk(any(File.class), any(BinPacker.FileWalker.IFileWalkerAction.class));

        prepareURI();
        ArgumentCaptor<String> outputCaptor = ArgumentCaptor.forClass(String.class);
        ArgumentCaptor<ArrayList> pathsCaptor = ArgumentCaptor.forClass(ArrayList.class);
        ArgumentCaptor<Boolean> forceModeCaptor = ArgumentCaptor.forClass(Boolean.class);

        // test
        String result = mSpyPacker.pack(false, FOLDER_PATH, FOLDER_LONG_PATH);
        mAction.actFolder(mFile);

        // verify
        //noinspection unchecked
        verify(mSpyPacker, times(1)).packAll(outputCaptor.capture(), pathsCaptor.capture(), forceModeCaptor.capture());
        assertThat(outputCaptor.getValue(), is(FOLDER_PATH));
        assertThat(pathsCaptor.getValue().size(), is(1));
        assertThat((String) pathsCaptor.getValue().get(0), is(FOLDER_LONG_PATH));
        assertThat(forceModeCaptor.getValue(), is(false));
        assertThat(result, is(ABSOLUTE_PATH + File.separatorChar + FILE_RESULT));
    }

    @Test
    public void pack_list_correctBehaviour() throws IBinPacker.BinPackerException, IOException {
        // prepare
        prepareConstruct();

        prepareFileArbiter(false, true);

        when(mFileInputStream.read(any(byte[].class))).thenReturn(READ_COUNT).thenReturn(0);
        mockStatic(BinPacker.FileWalker.class);
        PowerMockito.doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                mAction = (BinPacker.FileWalker.IFileWalkerAction) invocation.getArguments()[1];
                return null;
            }
        }).when(BinPacker.FileWalker.class);
        BinPacker.FileWalker.walk(any(File.class), any(BinPacker.FileWalker.IFileWalkerAction.class));

        prepareURI();
        ArgumentCaptor<String> outputCaptor = ArgumentCaptor.forClass(String.class);
        ArgumentCaptor<ArrayList> pathsCaptor = ArgumentCaptor.forClass(ArrayList.class);
        ArgumentCaptor<Boolean> forceModeCaptor = ArgumentCaptor.forClass(Boolean.class);

        // test
        String result = mSpyPacker.pack(false, FOLDER_PATH, FOLDER_LONG_PATH);
        mAction.actFolder(mFile);


        // verify
        //noinspection unchecked
        verify(mSpyPacker, times(1)).packAll(outputCaptor.capture(), pathsCaptor.capture(), forceModeCaptor.capture());
        assertThat(outputCaptor.getValue(), is(FOLDER_PATH));
        assertThat(pathsCaptor.getValue().size(), is(1));
        assertThat((String) pathsCaptor.getValue().get(0), is(FOLDER_LONG_PATH));
        assertThat(forceModeCaptor.getValue(), is(false));
        assertThat(result, is(ABSOLUTE_PATH + File.separatorChar + FILE_RESULT));
    }

    @Test
    public void pack_listNonForceAlways_correctBehaviour() throws IBinPacker.BinPackerException, IOException {
        // prepare
        prepareConstruct();

        prepareFileArbiter(false, true);

        when(mFileInputStream.read(any(byte[].class))).thenReturn(READ_COUNT).thenReturn(0);
        mockStatic(BinPacker.FileWalker.class);
        PowerMockito.doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                mAction = (BinPacker.FileWalker.IFileWalkerAction) invocation.getArguments()[1];
                return null;
            }
        }).when(BinPacker.FileWalker.class);
        BinPacker.FileWalker.walk(any(File.class), any(BinPacker.FileWalker.IFileWalkerAction.class));

        prepareURI();

        // test
        String result = mSpyPacker.pack(FOLDER_PATH, FOLDER_LONG_PATH);
        mAction.actFolder(mFile);
        ArgumentCaptor<String> outputCaptor = ArgumentCaptor.forClass(String.class);
        ArgumentCaptor<ArrayList> pathsCaptor = ArgumentCaptor.forClass(ArrayList.class);
        ArgumentCaptor<Boolean> forceModeCaptor = ArgumentCaptor.forClass(Boolean.class);

        // verify
        //noinspection unchecked
        verify(mSpyPacker, times(1)).packAll(outputCaptor.capture(), pathsCaptor.capture(), forceModeCaptor.capture());
        assertThat(outputCaptor.getValue(), is(FOLDER_PATH));
        assertThat(pathsCaptor.getValue().size(), is(1));
        assertThat((String) pathsCaptor.getValue().get(0), is(FOLDER_LONG_PATH));
        assertThat(forceModeCaptor.getValue(), is(false));
        assertThat(result, is(ABSOLUTE_PATH + File.separatorChar + FILE_RESULT));
    }

    // instead of construct in setUp
    private void construct() throws IBinPacker.BinPackerException {
        mBinPacker = new BinPacker(PATH, mFactory);
        mSpyPacker = spy(new BinPacker(PATH, mFactory));
    }

    // construct and check it
    private void prepareConstruct() throws IBinPacker.BinPackerException {
        // prepare
        when(mFile.exists()).thenReturn(true);
        when(mFile.length()).thenReturn(LENGTH);
        when(mFile.getAbsolutePath()).thenReturn(ABSOLUTE_PATH);

        construct();

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

    // prepare for reading relative path
    private void prepareURI() {
        URI uri1 = PowerMockito.mock(URI.class);
        URI uri2 = PowerMockito.mock(URI.class);
        URI uri3 = PowerMockito.mock(URI.class);
        when(mFile.toURI()).thenReturn(uri1).thenReturn(uri2);
        when(uri1.relativize(uri2)).thenReturn(uri3);
        when(uri3.getPath()).thenReturn(RELATIVE_PATH);
    }

    // decides if new File mock is of type file or dir
    private void prepareFileArbiter(boolean... areFiles) {
        for (boolean isFile : areFiles) {
            if (mOnGoingStubbingFile == null) {
                mOnGoingStubbingFile = when(mFile.isFile()).thenReturn(isFile);
            } else {
                mOnGoingStubbingFile.thenReturn(isFile);
            }
        }
        for (boolean isFile : areFiles) {
            if (mOnGoingStubbingFolder == null) {
                mOnGoingStubbingFolder = when(mFile.isDirectory()).thenReturn(!isFile);
            } else {
                mOnGoingStubbingFolder.thenReturn(!isFile);
            }
        }
    }
}
