package com.avg.zaap.crashlibrary.publish.cap.http;

import com.avg.zaap.crashlibrary.publish.cap.http.boundary.IBoundaryGenerator;

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.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.StringWriter;
import java.io.Writer;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

import static junit.framework.Assert.fail;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsSame.sameInstance;
import static org.junit.Assert.assertThat;
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.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * A test class for {@link CapRequest}.
 * <p/>
 * Created by marek.osvald on 13.6.2016.
 */
@SuppressWarnings("SameParameterValue")
@RunWith(PowerMockRunner.class)
@PrepareForTest({CapRequest.class, URL.class})
public class CapRequestTests {

    private static final String MOCK_BOUNDARY = "mockBoundary";
    private static final String MOCK_URL = "mockURL";
    private static final String MOCK_FILE_PATH = "mock/file/path.foo";
    private static final String MOCK_FILE_NAME = "path.foo";

    private static final String MOCK_FORM_KEY = "mockFormKey";
    private static final String MOCK_FORM_VALUE = "mockFormValue";

    private static final String CONTENT_TYPE = "Content-Type";
    private static final String MULTIPART_FORM_DATA_WITH_BOUNDARY = "multipart/form-data; boundary=" + MOCK_BOUNDARY;

    private static final String CONTENT_DISPOSITION_PART1 = "Content-Disposition: form-data; name=\"";
    private static final char CONTENT_DISPOSITION_PART2 = '"';
    private static final String CONTENT_TYPE_TEXT = "Content-Type: text";
    private static final String CONTENT_TYPE_MULTIPART_FORM_DATA = "Content-Type: multipart/form-data";
    @SuppressWarnings("SpellCheckingInspection")
    private static final String CONTENT_DISPOSITION_EXCEPTION_HEADER = "Content-Disposition: form-data; name=\"excpre\"; filename=\"header.exh\"";
    @SuppressWarnings("SpellCheckingInspection")
    private static final String CONTENT_DISPOSITION_UPFILES = "Content-Disposition: form-data; name=\"upfiles[]\"; filename=\"";
    private static final String CONTENT_ENCODING_BINARY = "Content-Transfer-Encoding: binary";

    private static final String EXCEPTION_HEADER_KEY_1 = "key1";
    private static final String EXCEPTION_HEADER_KEY_2 = "key2";
    private static final String EXCEPTION_HEADER_VALUE_1 = "value1";
    private static final String EXCEPTION_HEADER_VALUE_2 = "value2";

    private static final int HTTP_CODE_OK = 200;
    private static final String HTTP_MESSAGE_OK = "OK";
    private static final int CAP_CODE_OK = 0;
    private static final String CAP_MESSAGE_OK = "OK";
    private static final int CAP_CODE_FATAL = -666;
    private static final String CAP_MESSAGE_ERROR = "";

    private static final String CRLF = "\r\n";
    private static final int BUFFER_SIZE = 4096; // bytes
    private static final int HUNDRED_BYTES = 100; // bytes
    private static final int EOF = -1;
    private static final char SPACE = ' ';
    private static final String SEPARATOR = "--";

    private CapRequest mCapRequest;
    private Result mURLResult = Result.OK;
    private HttpURLConnection mURLConnection;
    private IBoundaryGenerator mBoundaryGenerator;
    private URL mURL;
    private OutputStream mOutputStream;
    private Writer mWriter;
    private File mFile;
    private InputStream mInputStream;
    private InputStreamReader mInputStreamReader;
    private FileInputStream mFileInputStream;
    private StringWriter mStringWriter;

    private ArgumentCaptor<byte[]> mBufferCaptor;
    private ArgumentCaptor<Integer> mOffsetCaptor;
    private ArgumentCaptor<Integer> mByteReadCaptor;

    @Before
    public void setUp() throws Exception {
        mBufferCaptor = ArgumentCaptor.forClass(byte[].class);
        mOffsetCaptor = ArgumentCaptor.forClass(Integer.class);
        mByteReadCaptor = ArgumentCaptor.forClass(Integer.class);

        mInputStream = mock(InputStream.class);
        mOutputStream = mock(OutputStream.class);
        mURLConnection = mock(HttpURLConnection.class);
        when(mURLConnection.getOutputStream()).thenReturn(mOutputStream);
        when(mURLConnection.getInputStream()).thenReturn(mInputStream);
        when(mURLConnection.getResponseCode()).thenReturn(HTTP_CODE_OK);
        when(mURLConnection.getResponseMessage()).thenReturn(HTTP_MESSAGE_OK);

        mBoundaryGenerator = mock(IBoundaryGenerator.class);
        when(mBoundaryGenerator.generateBoundary()).thenReturn(MOCK_BOUNDARY);

        mURL = PowerMockito.mock(URL.class);

        mWriter = mock(Writer.class);

        mFile = mock(File.class);
        when(mFile.getName()).thenReturn(MOCK_FILE_NAME);

        mInputStreamReader = mock(InputStreamReader.class);
        when(mInputStreamReader.read(any(char[].class))).thenReturn(BUFFER_SIZE, HUNDRED_BYTES, EOF);

        mFileInputStream = mock(FileInputStream.class);
        when(mFileInputStream.read(any(byte[].class))).thenReturn(BUFFER_SIZE, HUNDRED_BYTES, EOF);

        mStringWriter = mock(StringWriter.class);

        mCapRequest = new CapRequest(new ICapRequestFactory() {

            @Override
            public IBoundaryGenerator getBoundaryGenerator() {
                return mBoundaryGenerator;
            }

            @Override
            public URL openURL(String url) throws MalformedURLException {
                assertThat(url, is(MOCK_URL));

                switch (mURLResult) {
                    case OK:
                        return mURL;

                    case MALFORMED:
                        throw new MalformedURLException();

                    default:
                        fail();
                        return null;
                }
            }

            @Override
            public Writer getPrintWriter(OutputStream outputStream) {
                assertThat(outputStream, sameInstance(mOutputStream));
                return mWriter;
            }

            @Override
            public File openFile(String filePath) {
                assertThat(filePath, is(MOCK_FILE_PATH));
                return mFile;
            }

            @Override
            public InputStreamReader getInputStreamReader(InputStream inputStream) {
                assertThat(inputStream, sameInstance(mInputStream));
                return mInputStreamReader;
            }

            @Override
            public FileInputStream getFileInputStream(File file) throws FileNotFoundException {
                assertThat(file, sameInstance(mFile));
                return mFileInputStream;
            }

            @Override
            public Writer getStringWriter() {
                return mStringWriter;
            }

        });

        initConnection(Result.OK);
        initWriter(Result.OK);
        initStringWriter(Result.OK);
    }

    @Test
    public void openURL_everythingOK_returnsInstance() throws Exception {
        // act
        final CapRequest capRequest = mCapRequest.openURL(MOCK_URL);

        // assert
        verify(mURLConnection, times(1)).setRequestProperty(CONTENT_TYPE, MULTIPART_FORM_DATA_WITH_BOUNDARY);
        verify(mURLConnection, times(1)).setUseCaches(false);
        verify(mURLConnection, times(1)).setDoInput(true);
        verify(mURLConnection, times(1)).setDoOutput(true);
        assertThat(capRequest, sameInstance(mCapRequest));
    }

    @Test
    public void openURL_malformedURL_throwsCapRequestException() throws Exception {
        // arrange
        mURLResult = Result.MALFORMED;

        // act
        try {
            mCapRequest.openURL(MOCK_URL);
            fail();
        } catch (CapRequestException ex) {
            // assert
            assertThat(ex.getCause(), instanceOf(MalformedURLException.class));
        }
    }

    @Test
    public void openURL_IOException_throwsCapRequestException() throws Exception {
        // arrange
        when(mURL.openConnection()).thenThrow(new IOException());

        // act
        try {
            mCapRequest.openURL(MOCK_URL);
            fail();
        } catch (CapRequestException ex) {
            // assert
            assertThat(ex.getCause(), instanceOf(IOException.class));
        }
    }

    @Test
    public void addFormValue_URLNotOpen_throwsCapRequestException() throws Exception {
        // act
        try {
            mCapRequest.addFormValue(MOCK_FORM_KEY, MOCK_FORM_VALUE);
            fail();
        } catch (CapRequestException ex) {
            assertThat(ex.getCause(), nullValue());
        }
    }

    @Test
    public void addFormValue_writerIOException_throwsCapRequestException() throws Exception {
        // arrange
        initWriter(Result.IO);

        // act
        try {
            mCapRequest.openURL(MOCK_URL)
                    .addFormValue(MOCK_FORM_KEY, MOCK_FORM_VALUE);
        } catch (CapRequestException ex) {
            // assert
            assertThat(ex.getCause(), instanceOf(IOException.class));
        }
    }

    @Test
    public void addFormValue_writerOK_appendsData() throws Exception {
        // act
        mCapRequest.openURL(MOCK_URL)
                .addFormValue(MOCK_FORM_KEY, MOCK_FORM_VALUE);

        // assert
        verify(mWriter, times(1)).append(SEPARATOR);
        verify(mWriter, times(1)).append(MOCK_BOUNDARY);
        verify(mWriter, times(5)).append(CRLF);

        verify(mWriter, times(1)).append(CONTENT_DISPOSITION_PART1);
        verify(mWriter, times(1)).append(MOCK_FORM_KEY);
        verify(mWriter, times(1)).append(CONTENT_DISPOSITION_PART2);

        verify(mWriter, times(1)).append(CONTENT_TYPE_TEXT);
        verify(mWriter, times(1)).append(MOCK_FORM_VALUE);

        verify(mWriter, times(1)).flush();
    }

    @Test
    public void addExceptionHeader_URLNotOpen_throwsCapRequestException() throws Exception {
        // act
        try {
            //noinspection unchecked
            mCapRequest.addExceptionHeader(mock(Map.class));
            fail();
        } catch (CapRequestException ex) {
            // assert
            assertThat(ex.getCause(), nullValue());
        }
    }

    @Test
    public void addExceptionHeader_IOException_throwsCapRequestException() throws Exception {
        // arrange
        initWriter(Result.IO);

        // act
        try {
            //noinspection unchecked
            mCapRequest.openURL(MOCK_URL)
                    .addExceptionHeader(mock(Map.class));
            fail();
        } catch (CapRequestException ex) {
            // assert
            assertThat(ex.getCause(), instanceOf(IOException.class));
        }
    }

    @Test
    public void addExceptionHeader_writerOK_writesExceptionHeader() throws Exception {
        // arrange
        Map<String, String> exceptionHeader = new HashMap<>();
        exceptionHeader.put(EXCEPTION_HEADER_KEY_1, EXCEPTION_HEADER_VALUE_1);
        exceptionHeader.put(EXCEPTION_HEADER_KEY_2, EXCEPTION_HEADER_VALUE_2);

        // act
        mCapRequest.openURL(MOCK_URL)
                .addExceptionHeader(exceptionHeader);

        // assert
        verify(mWriter, times(1)).append(SEPARATOR);
        verify(mWriter, times(1)).append(MOCK_BOUNDARY);
        verify(mWriter, times(6)).append(CRLF);

        verify(mWriter, times(1)).append(CONTENT_DISPOSITION_EXCEPTION_HEADER);
        verify(mWriter, times(1)).append(CONTENT_TYPE_MULTIPART_FORM_DATA);
        verify(mWriter, times(2)).flush();

        verify(mWriter, times(1)).append(EXCEPTION_HEADER_KEY_1);
        verify(mWriter, times(1)).append(EXCEPTION_HEADER_VALUE_1);
        verify(mWriter, times(2)).append(SPACE);
        verify(mWriter, times(1)).append(EXCEPTION_HEADER_KEY_2);
        verify(mWriter, times(1)).append(EXCEPTION_HEADER_VALUE_2);
    }

    @Test
    public void addFile_URLNotOpen_throwsCapRequestException() throws Exception {
        // act
        try {
            mCapRequest.addFile(MOCK_FILE_PATH);
            fail();
        } catch (CapRequestException ex) {
            // assert
            assertThat(ex.getCause(), nullValue());
        }
    }

    @Test
    public void addFile_IOException_throwsCapRequestException() throws Exception {
        // arrange
        initWriter(Result.IO);

        // act
        try {
            mCapRequest.openURL(MOCK_URL)
                    .addFile(MOCK_FILE_PATH);
            fail();
        } catch (CapRequestException ex) {
            // assert
            assertThat(ex.getCause(), instanceOf(IOException.class));
        }
    }

    @Test
    public void addFile_writerOK_writesFile() throws Exception {
        // act
        mCapRequest.openURL(MOCK_URL)
                .addFile(MOCK_FILE_PATH);

        // assert
        verify(mWriter, times(1)).append(SEPARATOR);
        verify(mWriter, times(1)).append(MOCK_BOUNDARY);
        verify(mWriter, times(6)).append(CRLF);

        verify(mWriter, times(1)).append(CONTENT_DISPOSITION_UPFILES);
        verify(mWriter, times(1)).append(CONTENT_DISPOSITION_PART2);
        verify(mWriter, times(1)).append(CONTENT_TYPE_MULTIPART_FORM_DATA);
        verify(mWriter, times(1)).append(CONTENT_ENCODING_BINARY);

        verify(mWriter, times(2)).flush();

        verify(mOutputStream, times(2)).write(mBufferCaptor.capture(), mOffsetCaptor.capture(), mByteReadCaptor.capture());
        assertThat(mBufferCaptor.getAllValues().get(0), sameInstance(mBufferCaptor.getAllValues().get(1)));
        assertThat(mOffsetCaptor.getAllValues().get(0), is(0));
        assertThat(mOffsetCaptor.getAllValues().get(1), is(0));
        assertThat(mByteReadCaptor.getAllValues().get(0), is(BUFFER_SIZE));
        assertThat(mByteReadCaptor.getAllValues().get(1), is(HUNDRED_BYTES));
        verify(mOutputStream, times(1)).flush();

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

    @Test
    public void send_URLNotOpen_throwsCapRequestException() throws Exception {
        // act
        try {
            mCapRequest.send();
        } catch (CapRequestException ex) {
            // assert
            assertThat(ex.getCause(), nullValue());
            verify(mWriter, never()).close();
        }
    }

    @Test
    public void send_IOException_throwsCapRequestException() throws Exception {
        // arrange
        initWriter(Result.IO);

        // act
        try {
            mCapRequest.openURL(MOCK_URL)
                    .send();
        } catch (CapRequestException ex) {
            // assert
            assertThat(ex.getCause(), instanceOf(IOException.class));
            verify(mWriter, times(1)).close();
        }
    }

    @Test
    public void send_writerOK_returnCapResponse() throws Exception {
        // act
        final CapResponse response = mCapRequest.openURL(MOCK_URL)
                .send();

        // assert
        assertThat(response.httpCode, is(HTTP_CODE_OK));
        assertThat(response.httpMessage, is(HTTP_MESSAGE_OK));
        assertThat(response.capCode, is(CAP_CODE_OK));
        assertThat(response.capMessage, is(CAP_MESSAGE_OK));
        verify(mWriter, times(1)).close();
    }

    @Test
    public void send_writerOKConnectionRespondsEmptyString_returnCapResponse() throws Exception {
        // arrange
        initStringWriter(Result.MALFORMED);

        // act
        final CapResponse response = mCapRequest.openURL(MOCK_URL)
                .send();

        // assert
        assertThat(response.httpCode, is(HTTP_CODE_OK));
        assertThat(response.httpMessage, is(HTTP_MESSAGE_OK));
        assertThat(response.capCode, is(CAP_CODE_FATAL));
        assertThat(response.capMessage, is(CAP_MESSAGE_ERROR));
        verify(mWriter, times(1)).close();
    }

    @Test
    public void send_writerOKConnectionRespondsEmptyStringWriterCannotBeClosed_doesNotCrash() throws Exception {
        // arrange
        initStringWriter(Result.MALFORMED);
        doThrow(new IOException()).when(mWriter).close();

        // act
        final CapResponse response = mCapRequest.openURL(MOCK_URL)
                .send();

        // assert
        assertThat(response.httpCode, is(HTTP_CODE_OK));
        assertThat(response.httpMessage, is(HTTP_MESSAGE_OK));
        assertThat(response.capCode, is(CAP_CODE_FATAL));
        assertThat(response.capMessage, is(CAP_MESSAGE_ERROR));
        verify(mWriter, times(1)).close();
    }

    @Test
    public void send_IOException_returnCapResponse() throws Exception {
        // arrange
        when(mURLConnection.getResponseCode()).thenThrow(new IOException());

        // act
        try {
            mCapRequest.openURL(MOCK_URL)
                    .send();
            fail();
        } catch (CapRequestException ex) {
            // assert
            assertThat(ex.getCause(), instanceOf(IOException.class));
            verify(mWriter, times(1)).close();
        }
    }

    private void initConnection(Result result) throws IOException {
        switch (result) {
            case OK:
                PowerMockito.when(mURL.openConnection()).thenReturn(mURLConnection);
                break;

            case IO:
                PowerMockito.when(mURL.openConnection()).thenThrow(new IOException());
                break;

            default:
                fail();
                break;
        }
    }

    private void initWriter(Result result) throws IOException {
        switch (result) {
            case OK:
                when(mWriter.append(anyString())).thenReturn(mWriter);
                when(mWriter.append(any(char.class))).thenReturn(mWriter);
                break;

            case IO:
                when(mWriter.append(anyString())).thenThrow(new IOException());
                when(mWriter.append(any(char.class))).thenThrow(new IOException());
                break;

            default:
                fail();
                break;
        }
    }

    private void initStringWriter(Result result) throws IOException {
        switch (result) {
            case OK:
                when(mStringWriter.toString()).thenReturn(CAP_CODE_OK + (SPACE + CAP_MESSAGE_OK));
                break;

            case MALFORMED:
                when(mStringWriter.toString()).thenReturn("");
                break;

            default:
                fail();
                break;
        }
    }

    enum Result {
        OK, MALFORMED, IO
    }

}
