package io.tus.java.client;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.times;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URL;
import java.util.Arrays;

import org.junit.Assume;
import org.junit.Test;
import org.mockserver.model.HttpRequest;
import org.mockserver.model.HttpResponse;
import org.mockserver.socket.PortFactory;

public class TestTusUploader extends MockServerProvider {
    private boolean isOpenJDK6 = System.getProperty("java.version").startsWith("1.6") &&
            System.getProperty("java.vm.name").contains("OpenJDK");

    @Test
    public void testTusUploader() throws IOException, ProtocolException {
        byte[] content = "hello world".getBytes();

        mockServer.when(new HttpRequest()
                .withPath("/files/foo")
                .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
                .withHeader("Upload-Offset", "3")
                .withHeader("Content-Type", "application/offset+octet-stream")
                .withHeader(isOpenJDK6 ? "": "Expect: 100-continue")
                .withBody(Arrays.copyOfRange(content, 3, 11)))
                .respond(new HttpResponse()
                        .withStatusCode(204)
                        .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
                        .withHeader("Upload-Offset", "11"));

        TusClient client = new TusClient();
        URL uploadUrl = new URL(mockServerURL + "/foo");
        TusInputStream input = new TusInputStream(new ByteArrayInputStream(content));
        long offset = 3;

        TusUpload upload = new TusUpload();

        TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, offset);

        uploader.setChunkSize(5);
        assertEquals(uploader.getChunkSize(), 5);

        assertEquals(5, uploader.uploadChunk());
        assertEquals(3, uploader.uploadChunk(5));
        assertEquals(-1, uploader.uploadChunk());
        assertEquals(-1, uploader.uploadChunk(5));
        assertEquals(11, uploader.getOffset());
        uploader.finish();
    }

    @Test
    public void testTusUploaderClientUploadFinishedCalled() throws IOException, ProtocolException {

        TusClient client = mock(TusClient.class);

        byte[] content = "hello world".getBytes();

        URL uploadUrl = new URL("http://dummy-url/foo");
        TusInputStream input = new TusInputStream(new ByteArrayInputStream(content));
        long offset = 10;

        TusUpload upload = new TusUpload();
        upload.setSize(10);

        TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, offset);
        uploader.finish();

        // size and offset are the same, so uploadfinished() should be called
        verify(client).uploadFinished(upload);
    }

    @Test
    public void testTusUploaderClientUploadFinishedNotCalled() throws IOException, ProtocolException {

        TusClient client = mock(TusClient.class);

        byte[] content = "hello world".getBytes();

        URL uploadUrl = new URL("http://dummy-url/foo");
        TusInputStream input = new TusInputStream(new ByteArrayInputStream(content));
        long offset = 0;

        TusUpload upload = new TusUpload();
        upload.setSize(10);

        TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, offset);
        uploader.finish();

        // size is greater than offset, so uploadfinished() should not be called
        verify(client,times(0)).uploadFinished(upload);
    }

    @Test
    public void testTusUploaderFailedExpectation() throws IOException, ProtocolException {
        Assume.assumeFalse(isOpenJDK6);

        FailingExpectationServer server = new FailingExpectationServer();
        server.start();

        byte[] content = "hello world".getBytes();

        TusClient client = new TusClient();
        URL uploadUrl = new URL(server.getURL() + "/expect");
        TusInputStream input = new TusInputStream(new ByteArrayInputStream(content));
        long offset = 3;
        TusUpload upload = new TusUpload();
        boolean exceptionThrown = false;
        TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, offset);
        try {
            uploader.uploadChunk();
        } catch(ProtocolException e) {
            assertTrue(e.getMessage().contains("500"));
            exceptionThrown = true;
        } finally {
            assertTrue(exceptionThrown);
        }
    }

    /**
     * FailingExpectationServer is a HTTP/1.1 server which will always respond with a 500 Internal
     * Error. This is meant to simulate failing expectations when the request contains the
     * Expect header. The org.mockserver packages do not support this and will always send the
     * 100 Continue status code. therefore we built our own stupid mocking server.
     */
    private class FailingExpectationServer extends Thread {
        private final byte[] response = "HTTP/1.1 500 Internal Server Error\r\n\r\n".getBytes();
        private ServerSocket serverSocket;
        private int port;

        public FailingExpectationServer() throws IOException {
            port = PortFactory.findFreePort();

            serverSocket = new ServerSocket(port);
        }

        @Override
        public void run() {
            try {
                Socket socket = serverSocket.accept();

                OutputStream output = socket.getOutputStream();
                BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                while (!input.readLine().isEmpty()) {
                    output.write(response);
                    break;
                }

                socket.close();
            } catch(IOException e) {
                e.printStackTrace();
            }
        }

        public URL getURL() {
            try {
                return new URL("http://localhost:" + port);
            } catch(MalformedURLException e) {
                return null;
            }
        }
    }

    @Test
    public void testSetRequestPayloadSize() throws Exception {
        byte[] content = "hello world".getBytes();

        mockServer.when(new HttpRequest()
                .withPath("/files/payload")
                .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
                .withHeader("Upload-Offset", "0")
                .withHeader("Content-Type", "application/offset+octet-stream")
                .withBody(Arrays.copyOfRange(content, 0, 5)))
                .respond(new HttpResponse()
                        .withStatusCode(204)
                        .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
                        .withHeader("Upload-Offset", "5"));

        mockServer.when(new HttpRequest()
                .withPath("/files/payload")
                .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
                .withHeader("Upload-Offset", "5")
                .withHeader("Content-Type", "application/offset+octet-stream")
                .withBody(Arrays.copyOfRange(content, 5, 10)))
                .respond(new HttpResponse()
                        .withStatusCode(204)
                        .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
                        .withHeader("Upload-Offset", "10"));

        mockServer.when(new HttpRequest()
                .withPath("/files/payload")
                .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
                .withHeader("Upload-Offset", "10")
                .withHeader("Content-Type", "application/offset+octet-stream")
                .withBody(Arrays.copyOfRange(content, 10, 11)))
                .respond(new HttpResponse()
                        .withStatusCode(204)
                        .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
                        .withHeader("Upload-Offset", "11"));

        TusClient client = new TusClient();
        URL uploadUrl = new URL(mockServerURL + "/payload");
        TusInputStream input = new TusInputStream(new ByteArrayInputStream(content));
        TusUpload upload = new TusUpload();

        TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, 0);

        assertEquals(uploader.getRequestPayloadSize(), 10 * 1024 * 1024);
        uploader.setRequestPayloadSize(5);
        assertEquals(uploader.getRequestPayloadSize(), 5);

        uploader.setChunkSize(4);

        // First request
        assertEquals(4, uploader.uploadChunk());
        assertEquals(1, uploader.uploadChunk());

        // Second request
        uploader.setChunkSize(100);
        assertEquals(5, uploader.uploadChunk());

        // Third request
        assertEquals(1, uploader.uploadChunk());
        uploader.finish();
    }

    @Test(expected = IllegalStateException.class)
    public void testSetRequestPayloadSizeThrows() throws Exception {
        byte[] content = "hello world".getBytes();

        TusClient client = new TusClient();
        URL uploadUrl = new URL(mockServerURL + "/payloadException");
        TusInputStream input = new TusInputStream(new ByteArrayInputStream(content));
        TusUpload upload = new TusUpload();

        TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, 0);

        uploader.setChunkSize(4);
        uploader.uploadChunk();

        // Throws IllegalStateException
        uploader.setRequestPayloadSize(100);
    }

    @Test
    public void testMissingUploadOffsetHeader() throws Exception {
        byte[] content = "hello world".getBytes();

        mockServer.when(new HttpRequest()
                .withPath("/files/missingHeader"))
                .respond(new HttpResponse()
                        .withStatusCode(204)
                        .withHeader("Tus-Resumable", TusClient.TUS_VERSION));

        TusClient client = new TusClient();
        URL uploadUrl = new URL(mockServerURL + "/missingHeader");
        TusInputStream input = new TusInputStream(new ByteArrayInputStream(content));
        TusUpload upload = new TusUpload();

        TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, 0);

        boolean exceptionThrown = false;
        try {
            assertEquals(11, uploader.uploadChunk());
            uploader.finish();
        } catch(ProtocolException e) {
            assertTrue(e.getMessage().contains("no or invalid Upload-Offset header"));
            exceptionThrown = true;
        } finally {
            assertTrue(exceptionThrown);
        }
    }

    @Test
    public void testUnmatchingUploadOffsetHeader() throws Exception {
        byte[] content = "hello world".getBytes();

        mockServer.when(new HttpRequest()
                .withPath("/files/unmatchingHeader"))
                .respond(new HttpResponse()
                        .withStatusCode(204)
                        .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
                        .withHeader("Upload-Offset", "44"));

        TusClient client = new TusClient();
        URL uploadUrl = new URL(mockServerURL + "/unmatchingHeader");
        TusInputStream input = new TusInputStream(new ByteArrayInputStream(content));
        TusUpload upload = new TusUpload();

        TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, 0);

        boolean exceptionThrown = false;
        try {
            assertEquals(11, uploader.uploadChunk());
            uploader.finish();
        } catch(ProtocolException e) {
            assertTrue(e.getMessage().contains("different Upload-Offset value (44) than expected (11)"));
            exceptionThrown = true;
        } finally {
            assertTrue(exceptionThrown);
        }
    }
}