package com.avg.zaap.crashlibrary.provider.stacktrace;

import com.avg.zaap.crashlibrary.core.writer.DataWriterException;
import com.avg.zaap.crashlibrary.core.writer.IDataWriter;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
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.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;

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

/**
 * A test class for {@link StackTraceWriter}.
 * <p>
 * Created by marek.osvald on 28.4.2016.
 */
@RunWith(PowerMockRunner.class)
@PrepareForTest({StackTraceWriter.class, Thread.class})
public class StackTraceWriterTests {

    private static final String DIRECTORY = "/foo/bar";
    private static final String FILE_NAME_CRASHING = "StackTraceCrashingThread.txt";
    private static final String FILE_NAME_ALL = "StackTracesAllThreads.txt";
    private static final String FILE_PATH_CRASHING = DIRECTORY + File.separatorChar + FILE_NAME_CRASHING;
    private static final String FILE_PATH_ALL = DIRECTORY + File.separatorChar + FILE_NAME_ALL;

    private static final String STACK_TRACE_CLASS_1 = "class1";
    private static final String STACK_TRACE_CLASS_2 = "class2";
    private static final String STACK_TRACE_CLASS_3 = "class3";
    private static final String STACK_TRACE_CLASS_4 = "class4";
    private static final String STACK_TRACE_CLASS_5 = "class5";
    private static final String STACK_TRACE_CLASS_6 = "class6";

    private static final String STACK_TRACE_METHOD_1 = "method1";
    private static final String STACK_TRACE_METHOD_2 = "method2";
    private static final String STACK_TRACE_METHOD_3 = "method3";
    private static final String STACK_TRACE_METHOD_4 = "method4";
    private static final String STACK_TRACE_METHOD_5 = "method5";
    private static final String STACK_TRACE_METHOD_6 = "method6";

    private static final String STACK_TRACE_FILE_1 = "file1";
    private static final String STACK_TRACE_FILE_2 = "file2";
    private static final String STACK_TRACE_FILE_3 = "file3";
    private static final String STACK_TRACE_FILE_4 = "file4";
    private static final String STACK_TRACE_FILE_5 = "file5";
    private static final String STACK_TRACE_FILE_6 = "file6";

    //@formatter:off
    private static final String STACK_TRACE_CRASHING =
            "class1.method1(file1:1)\n" +
            "class2.method2(file2:2)\n" +
            "class3.method3(file3:3)\n";

    //@formatter:on
    private static final Pattern PATTERN =
            Pattern.compile("Mock for Thread, hashCode: \\d+\n" +
                    "\t\tfile1\\(1\\): class1: method1\n" +
                    "\t\tfile2\\(2\\): class2: method2\n" +
                    "\t\tfile3\\(3\\): class3: method3\n" +
                    "Mock for Thread, hashCode: \\d+\n" +
                    "\t\tfile4\\(4\\): class4: method4\n" +
                    "\t\tfile5\\(5\\): class5: method5\n" +
                    "\t\tfile6\\(6\\): class6: method6\n" +
                    "|" +
                    "Mock for Thread, hashCode: \\d+\n" +
                    "\t\tfile4\\(4\\): class4: method4\n" +
                    "\t\tfile5\\(5\\): class5: method5\n" +
                    "\t\tfile6\\(6\\): class6: method6\n" +
                    "Mock for Thread, hashCode: \\d+\n" +
                    "\t\tfile1\\(1\\): class1: method1\n" +
                    "\t\tfile2\\(2\\): class2: method2\n" +
                    "\t\tfile3\\(3\\): class3: method3\n");

    private StackTraceWriter mStackTraceWriter;
    private IDataWriter mDataWriter;
    private Throwable mThrowable;

    private ArgumentCaptor<String> mFilePathCaptor;
    private ArgumentCaptor<String> mDataCaptor;

    @Before
    public void setUp() throws Exception {
        mDataWriter = mock(IDataWriter.class);
        mThrowable = mock(Throwable.class);

        StackTraceElement stackTraceElement1 = new StackTraceElement(STACK_TRACE_CLASS_1, STACK_TRACE_METHOD_1, STACK_TRACE_FILE_1, 1);
        StackTraceElement stackTraceElement2 = new StackTraceElement(STACK_TRACE_CLASS_2, STACK_TRACE_METHOD_2, STACK_TRACE_FILE_2, 2);
        StackTraceElement stackTraceElement3 = new StackTraceElement(STACK_TRACE_CLASS_3, STACK_TRACE_METHOD_3, STACK_TRACE_FILE_3, 3);
        StackTraceElement stackTraceElement4 = new StackTraceElement(STACK_TRACE_CLASS_4, STACK_TRACE_METHOD_4, STACK_TRACE_FILE_4, 4);
        StackTraceElement stackTraceElement5 = new StackTraceElement(STACK_TRACE_CLASS_5, STACK_TRACE_METHOD_5, STACK_TRACE_FILE_5, 5);
        StackTraceElement stackTraceElement6 = new StackTraceElement(STACK_TRACE_CLASS_6, STACK_TRACE_METHOD_6, STACK_TRACE_FILE_6, 6);

        StackTraceElement[] stackTrace = new StackTraceElement[]{stackTraceElement1, stackTraceElement2, stackTraceElement3};
        StackTraceElement[] stackTraceThread2 = new StackTraceElement[]{stackTraceElement4, stackTraceElement5, stackTraceElement6};

        Thread thread1 = mock(Thread.class);
        Thread thread2 = mock(Thread.class);

        Map<Thread, StackTraceElement[]> stackTraces = new HashMap<>();
        stackTraces.put(thread1, stackTrace);
        stackTraces.put(thread2, stackTraceThread2);

        mFilePathCaptor = ArgumentCaptor.forClass(String.class);
        mDataCaptor = ArgumentCaptor.forClass(String.class);

        PowerMockito.mockStatic(Thread.class);
        PowerMockito.when(Thread.getAllStackTraces()).thenReturn(stackTraces);

        mStackTraceWriter = new StackTraceWriter(new IStackTraceWriterFactory() {
            @Override
            public IDataWriter getDataWriter() {
                return mDataWriter;
            }
        });

        when(mThrowable.getStackTrace()).thenReturn(stackTrace);
        initializeDataWriter(Result.OK);
    }

    @Test
    public void writeCrashingThreadStackTrace_dataWriterOK_returnsFilePath() throws Exception {
        // act
        final String result = mStackTraceWriter.writeCrashingThreadStackTrace(DIRECTORY, mThrowable);

        // assert
        verify(mDataWriter, times(1)).write(FILE_PATH_CRASHING, STACK_TRACE_CRASHING);
        assertThat(result, is(FILE_PATH_CRASHING));
    }

    @Test
    public void writeCrashingThreadStackTrace_dataWriterCrashes_throwsException() throws Exception {
        // arrange
        initializeDataWriter(Result.EXCEPTION);

        // act
        try {
            mStackTraceWriter.writeCrashingThreadStackTrace(DIRECTORY, mThrowable);
            fail();
        } catch (StackTraceWriterException ex) { /* ignored */ }

        // assert
        verify(mDataWriter, times(1)).write(FILE_PATH_CRASHING, STACK_TRACE_CRASHING);
    }

    @Test
    public void writeAllStackTraces_dataWriterOK_returnsFilePath() throws Exception {
        // act
        final String result = mStackTraceWriter.writeAllStackTraces(DIRECTORY);

        // assert
        verify(mDataWriter, times(1)).write(mFilePathCaptor.capture(), mDataCaptor.capture());
        assertThat(mFilePathCaptor.getValue(), is(FILE_PATH_ALL));
        assertThat(PATTERN.matcher(mDataCaptor.getValue()).matches(), is(true));
        assertThat(result, is(FILE_PATH_ALL));
    }

    @Test
    public void writeAllStackTraces_dataWriterCrashes_throwsException() throws Exception {
        // arrange
        initializeDataWriter(Result.EXCEPTION);

        // act
        try {
            mStackTraceWriter.writeAllStackTraces(DIRECTORY);
            fail();
        } catch (StackTraceWriterException ex) { /* ignored */ }

        // assert
        verify(mDataWriter, times(1)).write(mFilePathCaptor.capture(), mDataCaptor.capture());
        assertThat(mFilePathCaptor.getValue(), is(FILE_PATH_ALL));
        assertThat(PATTERN.matcher(mDataCaptor.getValue()).matches(), is(true));
    }

    private void initializeDataWriter(Result result) throws Exception {
        switch (result) {
            case OK:
                doNothing().when(mDataWriter).write(anyString(), anyString());
                break;

            case EXCEPTION:
                doThrow(new DataWriterException()).when(mDataWriter).write(anyString(), anyString());
                break;

            default:
                fail();
        }
    }

    enum Result {
        OK, EXCEPTION
    }

}
