/*
 * JBoss, Home of Professional Open Source.
 *
 * Copyright 2013 Red Hat, Inc. and/or its affiliates, and individual
 * contributors as indicated by the @author tags.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.xnio.ssl;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.xnio.ssl.mock.SSLEngineMock.CLOSE_MSG;
import static org.xnio.ssl.mock.SSLEngineMock.HANDSHAKE_MSG;
import static org.xnio.ssl.mock.SSLEngineMock.HandshakeAction.FINISH;
import static org.xnio.ssl.mock.SSLEngineMock.HandshakeAction.NEED_TASK;
import static org.xnio.ssl.mock.SSLEngineMock.HandshakeAction.NEED_UNWRAP;
import static org.xnio.ssl.mock.SSLEngineMock.HandshakeAction.NEED_WRAP;
import static org.xnio.ssl.mock.SSLEngineMock.HandshakeAction.PERFORM_REQUESTED_ACTION;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import javax.net.ssl.SSLEngineResult.HandshakeStatus;

import org.jmock.integration.junit4.JUnitRuleMockery;
import org.jmock.lib.concurrent.Synchroniser;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.xnio.Buffers;
import org.xnio.ssl.mock.SSLEngineMock;


/**
 * Test for concurrent read and write operations on  {@link #AssembledConnectedSslStreamChannel}.
 * 
 * @author <a href="mailto:frainone@redhat.com">Flavia Rainone</a>
 */
@Ignore // ignoring for now as these tests hang more consistently than they pass
public class ConnectedSslStreamChannelReadWriteTestCase extends AbstractConnectedSslStreamChannelTest{
    @Rule
    public final JUnitRuleMockery context = new JUnitRuleMockery() {{
        setThreadingPolicy(new Synchroniser());
    }};

    private Object readWriteMonitor = new Object();

    private boolean syncFlush() throws IOException {
        synchronized (readWriteMonitor) {
            return sslChannel.flush();
        }
    }
    
    private void syncShutdownWrites() throws IOException {
        synchronized (readWriteMonitor) {
            sslChannel.shutdownWrites();
        }
    }

    @Test
    public void simpleReadAndWrite() throws Exception {
        // no handshake actions for engineMock this time, meaning it will just wrap and unwrap without any handshake
        // the message we want to write
        conduitMock.setReadData("read data");
        conduitMock.enableReads(true);
        final Future<ByteBuffer> readFuture = triggerReadThread(9);
        final Future<Void> writeFuture = triggerWriteThread("write data");
        // wait for write thread to finish
        writeFuture.get();
        conduitMock.setReadData(CLOSE_MSG);
        // wait for read thread to finish
        final ByteBuffer readBuffer = readFuture.get();
        sslChannel.shutdownWrites();
        assertTrue(sslChannel.flush());
        sslChannel.shutdownReads();
        // close channel
        sslChannel.close();
        // data expected to have been copied to buffer by channel
        assertReadMessage(readBuffer, "read data");
        assertWrittenMessage("write data", CLOSE_MSG);
    }

    @Test
    public void readAndWriteMappedWrap() throws Exception {
        // map the wrap
        engineMock.addWrapEntry("a very long message", "BLABLABLABLABLABLABLA");
        engineMock.addWrapEntry("short msg", "MSG");
        // no handshake actions for engineMock this time, meaning that it will just wrap and unwrap without any handshake
        // the message we want to read
        conduitMock.setReadData("BLABLABLABLABLABLABLA");
        conduitMock.enableReads(true);
        // attempt to read and write
        final Future<Void> writeFuture = triggerWriteThread("short msg", "short msg");
        final Future<ByteBuffer> readFuture = triggerReadThread(19);
        // wait for write thread to finish
        writeFuture.get();
        conduitMock.setReadData(CLOSE_MSG);
        // wait for read thread to finish
        final ByteBuffer readBuffer = readFuture.get();
        // channel should be able to shutdown reads and writes
        sslChannel.shutdownReads();
        sslChannel.shutdownWrites();
        assertTrue(sslChannel.flush());
        // close channel
        sslChannel.close();
        // data expected to have been read to 'buffer' by 'channel'
        assertReadMessage(readBuffer, "a very long message");
        assertWrittenMessage("MSG", "MSG", CLOSE_MSG);
    }

    @Test
    public void readAndWriteWithSimpleHandshake() throws Exception {
        // map all data to be read and written
        engineMock.addWrapEntry(HANDSHAKE_MSG, "handshake");
        engineMock.addWrapEntry("MockTest", "mock test works!");
        engineMock.addWrapEntry(CLOSE_MSG, "channel closed");
        // set the handshake actions that engineMock will emulate
        engineMock.setHandshakeActions(NEED_WRAP, NEED_UNWRAP, NEED_TASK, FINISH);
        // set ReadData on connectedChannelMock, including the wrapped version of message we want to read
        conduitMock.setReadData("handshake", "mock test works!", "mock test works!");
        conduitMock.enableReads(true);

        // attempt to read and write
        final Future<Void> writeFuture = triggerWriteThread("MockTest");
        final Future<ByteBuffer> readFuture = triggerReadThread(16);
        // wait for write thread to finish
        writeFuture.get();
        conduitMock.setReadData("channel closed");
        // wait for read thread to finish
        final ByteBuffer readBuffer = readFuture.get();

        // channel should be able to shutdown reads and writes
        sslChannel.shutdownReads();
        sslChannel.shutdownWrites();
        assertTrue(sslChannel.flush());
        // close channel
        sslChannel.close();

        assertReadMessage(readBuffer, "MockTest", "MockTest");
        // data expected to have been written to 'connectedChannelMock' by 'channel'
        assertWrittenMessage("handshake", "mock test works!", "channel closed");
    }

    @Test
    public void readAndWriteWithTasklessHandshake() throws Exception {
        // map data to be read and written
        engineMock.addWrapEntry("Mock Test", "{testReadWriteWithTasklessHandshake}");
        engineMock.addWrapEntry(CLOSE_MSG, " _ ");

        // set the handshake actions that engineMock will emulate
        engineMock.setHandshakeActions(NEED_WRAP, NEED_UNWRAP, FINISH);
        // set ReadData on connectedChannelMock
        conduitMock.setReadData(SSLEngineMock.HANDSHAKE_MSG, "{testReadWriteWithTasklessHandshake}");
        conduitMock.enableReads(true);

        // attempt to read and write
        final Future<Void> writeFuture = triggerWriteThread("Mock Test", "Mock Test", "Mock Test", "Mock Test");
        final Future<ByteBuffer> readFuture = triggerReadThread(9);
        // wait for write thread to finish
        writeFuture.get();
        conduitMock.setReadData(" _ ");
        // wait for read thread to finish
        final ByteBuffer readBuffer = readFuture.get();

        // channel should be able to shutdown reads and writes
        sslChannel.shutdownReads();
        sslChannel.shutdownWrites();
        assertTrue(sslChannel.flush());
        // close channel
        sslChannel.close();
        // data expected to have been read from 'connectedChannelMock' by 'channel'
        assertReadMessage(readBuffer, "Mock Test");

        // data expected to have been written to 'connectedChannelMock' by 'channel'
        assertWrittenMessage(HANDSHAKE_MSG, "{testReadWriteWithTasklessHandshake}",
                "{testReadWriteWithTasklessHandshake}", "{testReadWriteWithTasklessHandshake}",
                "{testReadWriteWithTasklessHandshake}", " _ ");
    }

    @Test
    public void multipleFeedReadAndWriteWithSimpleHandshake() throws Exception {
        // map data to be read and written
        engineMock.addWrapEntry(HANDSHAKE_MSG, "{handshake data}");
        engineMock.addWrapEntry("Mock Read/Write Test", "{data}");
        engineMock.addWrapEntry("it works!", "{more data}");
        engineMock.addWrapEntry(CLOSE_MSG, "{message closed}");
        // set the handshake actions that engineMock will emulate
        engineMock.setHandshakeActions(NEED_UNWRAP, NEED_WRAP, NEED_TASK, FINISH);
        // set ReadData on connectedChannelMock
        conduitMock.setReadData();
        // enable read on connectedChannelMock, meaning that data above will be available to be read right away
        conduitMock.enableReads(true);

        // attempt to read and write
        final Future<Void> writeFuture = triggerWriteThread("it works!", "Mock Read/Write Test", "it works!",
                "Mock Read/Write Test", "it works!", "it works!", "Mock Read/Write Test");
        final Future<ByteBuffer> readFuture = triggerReadThread(67);

        Thread.sleep(20);
        conduitMock.setReadData("{handshake data}");
        Thread.sleep(10);
        conduitMock.setReadData( "{data}", "{data}", "{more data}");
        Thread.sleep(10);
        conduitMock.setReadData("{more data}", "{more data}");
        Thread.sleep(10);

        // wait for write thread to finish
        writeFuture.get();
        conduitMock.setReadData("{message closed}");
        // wait for read thread to finish
        final ByteBuffer readBuffer = readFuture.get();
        assertNotNull(readBuffer);

        // channel should be able to shutdown reads and writes
        sslChannel.shutdownReads();
        sslChannel.shutdownWrites();
        assertTrue(sslChannel.flush());
        // close channel
        sslChannel.close();

        // data expected to have been read from 'connectedChannelMock' by 'channel' so far
        assertReadMessage(readBuffer, "Mock Read/Write Test", "Mock Read/Write Test", "it works!", "it works!", "it works!");

        // data expected to have been written to 'connectedChannelMock' by 'channel'
        assertWrittenMessage("{handshake data}", "{more data}", "{data}", "{more data}", "{data}", "{more data}",
                "{more data}", "{data}", "{message closed}");
    }

    @Test
    public void readAndWriteWithConstantHandshake() throws Exception {
        // set the handshake actions that engineMock will emulate
        engineMock.setHandshakeActions(NEED_WRAP, NEED_UNWRAP, NEED_TASK, FINISH, PERFORM_REQUESTED_ACTION,
                NEED_UNWRAP, NEED_WRAP, NEED_TASK, FINISH, PERFORM_REQUESTED_ACTION,
                NEED_WRAP, NEED_WRAP, NEED_WRAP, NEED_UNWRAP, NEED_TASK, NEED_TASK, NEED_TASK, NEED_TASK, FINISH,
                PERFORM_REQUESTED_ACTION, NEED_WRAP, NEED_UNWRAP, NEED_TASK, FINISH, PERFORM_REQUESTED_ACTION);
        // set ReadData on connectedChannelMock
        conduitMock.setReadData(HANDSHAKE_MSG, "read a lot");
        // enable read on connectedChannelMock, meaning that data above will be available to be read right away
        conduitMock.enableReads(true);

        // attempt to read and write
        final Future<Void> writeFuture = triggerMultipleWriteThread("write a lot", "write a lot", "write a lot",
                "write it", "a lot", "write it down", "a lot");
        final Future<ByteBuffer> readFuture = triggerReadThread(54);
        Thread.sleep(20);
        conduitMock.setReadData(HANDSHAKE_MSG, HANDSHAKE_MSG);
        Thread.sleep(10);
        conduitMock.setReadData( "this is a lot", "lot", "lot", "lot", "lot", "lot", "lot", "lot");
        Thread.sleep(10);
        conduitMock.setReadData("read a lot", HANDSHAKE_MSG);
        Thread.sleep(10);

        // wait for write thread to finish
        writeFuture.get();
        syncShutdownWrites();
        assertFalse(syncFlush());
        conduitMock.setReadData(CLOSE_MSG);
        // wait for read thread to finish
        final ByteBuffer readBuffer = readFuture.get();
        assertNotNull(readBuffer);

        // shutdown reads
        sslChannel.shutdownReads();
        sslChannel.shutdownWrites();
        synchronized (readWriteMonitor) {
            assertTrue(sslChannel.flush());
        }
        // close channel
        sslChannel.close();

        // data expected to have been read from 'connectedChannelMock' by 'channel' so far
        assertReadMessage(readBuffer, "read a lot", "this is a lot", "lot", "lot", "lot" , "lot", "lot", "lot", "lot",
                "read a lot");
        // data expected to have been written to 'connectedChannelMock' by 'channel'
        assertWrittenMessage(new String[]{HANDSHAKE_MSG, HANDSHAKE_MSG, HANDSHAKE_MSG, HANDSHAKE_MSG, HANDSHAKE_MSG,
                HANDSHAKE_MSG, CLOSE_MSG}, new String[] {"write a lot", "write a lot", "write a lot", "write it",
                "a lot", "write it down", "a lot"});
    }

    @Test
    public void readAndWriteWithConstantHandshakeAndMappedData() throws Exception {
        // map data to be read and written
        engineMock.addWrapEntry(HANDSHAKE_MSG, "HANDSHAKE_MSG");
        engineMock.addWrapEntry("MockTest1", "MOCK 1");
        engineMock.addWrapEntry("MockTest2", "MOCK 2");
        engineMock.addWrapEntry("MockTest3", "MOCK 3");
        engineMock.addWrapEntry("MockTest4", "MOCK 4");
        engineMock.addWrapEntry(CLOSE_MSG, "CLOSE_MSG");
        // set the handshake actions that engineMock will emulate
        engineMock.setHandshakeActions(NEED_WRAP, NEED_UNWRAP, NEED_TASK, FINISH, PERFORM_REQUESTED_ACTION,
                NEED_UNWRAP, NEED_WRAP, NEED_TASK, FINISH, PERFORM_REQUESTED_ACTION,
                NEED_WRAP, NEED_WRAP, NEED_WRAP, NEED_UNWRAP, NEED_TASK, NEED_TASK, NEED_TASK, NEED_TASK, FINISH,
                PERFORM_REQUESTED_ACTION, NEED_WRAP, NEED_UNWRAP, NEED_TASK, FINISH, PERFORM_REQUESTED_ACTION,
                NEED_UNWRAP, NEED_WRAP, NEED_TASK, FINISH, PERFORM_REQUESTED_ACTION, NEED_UNWRAP, NEED_WRAP,
                NEED_TASK, FINISH, PERFORM_REQUESTED_ACTION, NEED_WRAP, NEED_UNWRAP, NEED_TASK, FINISH,
                PERFORM_REQUESTED_ACTION);
        // set ReadData on connectedChannelMock
        conduitMock.setReadData("HANDSHAKE_MSG", "MOCK 3");
        // enable read on connectedChannelMock, meaning that data above will be available to be read right away
        conduitMock.enableReads(true);

        // attempt to read and write
        final Future<Void> writeFuture = triggerMultipleWriteThread("MockTest1", "MockTest2", "MockTest2", "MockTest1", "MockTest2", "MockTest3", "MockTest4");
        final Future<ByteBuffer> readFuture = triggerMultipleReadThread(99);
        Thread.sleep(40);
        conduitMock.setReadData("HANDSHAKE_MSG", "HANDSHAKE_MSG", "MOCK 3", "HANDSHAKE_MSG");
        Thread.sleep(10);
        conduitMock.setReadData( "MOCK 2", "MOCK 2", "HANDSHAKE_MSG", "MOCK 4");
        Thread.sleep(10);
        conduitMock.setReadData("MOCK 4", "MOCK 1", "MOCK 3", "MOCK 2", "MOCK 4");
        Thread.sleep(10);
        conduitMock.setReadData("MOCK 1", "HANDSHAKE_MSG", "HANDSHAKE_MSG");
        Thread.sleep(10);

        // wait for write thread to finish
        writeFuture.get();
        syncShutdownWrites();
        assertFalse(syncFlush());
        conduitMock.setReadData("CLOSE_MSG");
        // wait for read thread to finish
        final ByteBuffer readBuffer = readFuture.get();
        assertNotNull(readBuffer);

        // shutdown reads
        sslChannel.shutdownReads();
        sslChannel.shutdownWrites();
        assertTrue(sslChannel.flush());
        // close channel
        sslChannel.close();
        // make sure that channel managed to do the WRAP and there is no more handshake actions left
        assertSame(HandshakeStatus.NOT_HANDSHAKING, engineMock.getHandshakeStatus());

        // data expected to have been read from 'connectedChannelMock' by 'channel' so far
        assertReadMessage(readBuffer, "MockTest3", "MockTest3", "MockTest2", "MockTest2", "MockTest4", "MockTest4",
                "MockTest1", "MockTest3", "MockTest2", "MockTest4", "MockTest1");
        // data expected to have been written to 'connectedChannelMock' by 'channel'
        assertWrittenMessage(new String[]{"HANDSHAKE_MSG", "HANDSHAKE_MSG", "HANDSHAKE_MSG", "HANDSHAKE_MSG",
                "HANDSHAKE_MSG", "HANDSHAKE_MSG", "HANDSHAKE_MSG", "HANDSHAKE_MSG", "HANDSHAKE_MSG", "CLOSE_MSG"},
                new String[] {"MOCK 1", "MOCK 2", "MOCK 2", "MOCK 1", "MOCK 2", "MOCK 3", "MOCK 4"});
    }

    @Test
    public void readAndWriteWithIntercalatedHandshake() throws Exception {
        // set the handshake actions that engineMock will emulate
        engineMock.setHandshakeActions(NEED_WRAP, NEED_UNWRAP, NEED_TASK, FINISH, PERFORM_REQUESTED_ACTION,
                NEED_WRAP, NEED_UNWRAP, PERFORM_REQUESTED_ACTION, NEED_UNWRAP, PERFORM_REQUESTED_ACTION, FINISH);
        // set ReadData on connectedChannelMock
        conduitMock.setReadData(HANDSHAKE_MSG, "read this", "read this", "read this", "read this",
                HANDSHAKE_MSG, "read this", HANDSHAKE_MSG, "read this");

        // attempt to read and write
        final Future<Void> writeFuture = triggerMultipleWriteThread("write this", "write this", "write this", "write this",
                "write this");
        final Future<ByteBuffer> readFuture = triggerReadThread(54);
        // enable read on connectedChannelMock, meaning that data above will be available to be read right away
        conduitMock.enableReads(true);

        // wait for write thread to finish
        writeFuture.get();
        syncShutdownWrites();
        assertFalse(syncFlush());
        conduitMock.setReadData(CLOSE_MSG);
        // wait for read thread to finish
        final ByteBuffer readBuffer = readFuture.get();
        assertNotNull(readBuffer);

        // shutdown reads
        sslChannel.shutdownReads();
        sslChannel.shutdownWrites();
        assertTrue(sslChannel.flush());
        // close channel
        sslChannel.close();
        // make sure that channel managed to do the WRAP and there is no more handshake actions left
        assertSame(HandshakeStatus.NOT_HANDSHAKING, engineMock.getHandshakeStatus());

        // data expected to have been read from 'connectedChannelMock' by 'channel' so far
        assertReadMessage(readBuffer, "read this", "read this", "read this", "read this", "read this", "read this");
        // data expected to have been written to 'connectedChannelMock' by 'channel'
        assertWrittenMessage(new String[]{HANDSHAKE_MSG, HANDSHAKE_MSG, CLOSE_MSG},
                new String[] {"write this", "write this", "write this", "write this", "write this"});
    }

    @Test
    public void readAndWriteWithIntercalatedHandshakeAndMappedData() throws Exception {
        // map all data to be read and written
        engineMock.addWrapEntry(HANDSHAKE_MSG, "[!@#$%^&*()_]");
        engineMock.addWrapEntry("this", "read this");
        engineMock.addWrapEntry("write this", "this");
        engineMock.addWrapEntry(CLOSE_MSG, "[_)(*&^%$#@!]");

        // set the handshake actions that engineMock will emulate
        engineMock.setHandshakeActions(NEED_WRAP, NEED_UNWRAP, NEED_TASK, FINISH, PERFORM_REQUESTED_ACTION,
                NEED_WRAP, NEED_UNWRAP, PERFORM_REQUESTED_ACTION, NEED_UNWRAP, PERFORM_REQUESTED_ACTION, FINISH);
        // set ReadData on connectedChannelMock
        conduitMock.setReadData("[!@#$%^&*()_]", "read this", "read this", "read this", "read this",
                "[!@#$%^&*()_]", "read this", "[!@#$%^&*()_]", "read this");
        // enable read on connectedChannelMock, meaning that data above will be available to be read right away
        conduitMock.enableReads(true);

        // attempt to read and write
        final Future<Void> writeFuture = triggerMultipleWriteThread("write this", "write this", "write this", "write this",
                "write this");
        final Future<ByteBuffer> readFuture = triggerMultipleReadThread(24);

        // wait for write thread to finish
        writeFuture.get();
        syncShutdownWrites();
        assertFalse(syncFlush());
        conduitMock.setReadData("[_)(*&^%$#@!]");
        // wait for read thread to finish
        final ByteBuffer readBuffer = readFuture.get();
        assertNotNull(readBuffer);

        // shutdown reads
        sslChannel.shutdownReads();
        sslChannel.shutdownWrites();
        assertTrue(sslChannel.flush());
        // close channel
        sslChannel.close();
        // make sure that channel managed to do the WRAP and there is no more handshake actions left
        assertSame(HandshakeStatus.NOT_HANDSHAKING, engineMock.getHandshakeStatus());

        // data expected to have been read from 'connectedChannelMock' by 'channel' so far
        assertReadMessage(readBuffer, "this", "this", "this", "this", "this", "this");
        // data expected to have been written to 'connectedChannelMock' by 'channel'
        assertWrittenMessage(new String[]{"[!@#$%^&*()_]", "[!@#$%^&*()_]", "[_)(*&^%$#@!]"},
                new String[] {"this", "this", "this", "this", "this"});
    }

    private Future<ByteBuffer> triggerReadThread(int expectedReadLength) {
        ReadRunnable readRunnable = new ReadRunnable(expectedReadLength);
        Thread newThread = new Thread(readRunnable);
        newThread.start();
        return readRunnable.getResultFuture();
    }

    private Future<Void> triggerWriteThread(String... text) {
        WriteRunnable writeRunnable = new WriteRunnable(text);
        Thread newThread = new Thread(writeRunnable);
        newThread.start();
        return writeRunnable.getResultFuture();
    }

    private Future<ByteBuffer> triggerMultipleReadThread(int expectedReadLength) {
        MultipleReadRunnable readRunnable = new MultipleReadRunnable(expectedReadLength);
        Thread newThread = new Thread(readRunnable);
        newThread.start();
        return readRunnable.getResultFuture();
    }

    private Future<Void> triggerMultipleWriteThread(String... text) {
        MultipleWriteRunnable writeRunnable = new MultipleWriteRunnable(text);
        Thread newThread = new Thread(writeRunnable);
        newThread.start();
        return writeRunnable.getResultFuture();
    }

    private class ReadRunnable implements Runnable {
        private ResultFuture<ByteBuffer> resultFuture;
        private int expectedReadLength;

        public ReadRunnable(int expectedReadLength) {
            this.expectedReadLength = expectedReadLength;
            this.resultFuture = new ResultFuture<ByteBuffer>();
        }
        
        public Future<ByteBuffer> getResultFuture() {
            return resultFuture;
        }

        @Override
        public void run() {
            final ByteBuffer buffer = ByteBuffer.allocate(100);
            final ByteBuffer[] buffers = new ByteBuffer[10];
            for (int i = 0; i < 10; i++) {
                buffers[i] = ByteBuffer.allocate(10);
            }
            // attempt to read... channel is expected to read the entire message without any issues
            try {
                int totalLength = 0;
                long length = 0;
                do {
                    totalLength += length;
                    // keep the synchronized block inside the while, even though it looks unelegant, this way we will
                    // achieve a finer granularity
                    synchronized (readWriteMonitor) {
                        length = sslChannel.read(buffers);
                    }
                } while (length >= 0);
                synchronized (readWriteMonitor) {
                    assertEquals(-1, sslChannel.read(buffers, 0, 10));
                    assertEquals(-1, sslChannel.read(buffers, 0, 10));
                    assertEquals(-1, sslChannel.read(buffers, 0, 10));
                }
                for (ByteBuffer b: buffers) {
                    b.flip();
                }
                Buffers.copy(buffer, buffers, 0, 10);
                buffer.flip();
                assertEquals("This is what we read '" + Buffers.getModifiedUtf8(buffer) + "'", expectedReadLength, totalLength);
                
            } catch (IOException e) {
                throw new RuntimeException("Unexpected IOException while reading", e);
            }
            resultFuture.setResult(buffer);
        }
    }

    private class WriteRunnable implements Runnable {
        private ResultFuture<Void> resultFuture;
        private String[] text;

        public WriteRunnable(String... text) {
            this.text = text;
            this.resultFuture = new ResultFuture<Void>();
        }

        public Future<Void> getResultFuture() {
            return resultFuture;
        }

        @Override
        public void run() {
            final ByteBuffer[] buffer = new ByteBuffer[text.length];
            int totalBytes = 0;
            try {
                for (int i = 0; i < text.length; i++) {
                    // attempt to write... channel is expected to write the entire message without any issues
                    buffer[i] = ByteBuffer.allocate(50);
                    buffer[i].put(text[i].getBytes("UTF-8")).flip();
                    totalBytes += text[i].length();
                }
                final int attemptsLimit = 10000;
                int attempts = 0;
                long bytes = 0;
                do {
                    synchronized (readWriteMonitor) {
                        bytes += sslChannel.write(buffer);
                    }
                }
                while (bytes < totalBytes && (++ attempts) < attemptsLimit);
                assertEquals(totalBytes, bytes);
            } catch (IOException e) {
                throw new RuntimeException("Unexpected exception while writing", e);
            }
            resultFuture.setResult(null);
        }
    }

    private class MultipleReadRunnable implements Runnable {
        private ResultFuture<ByteBuffer> resultFuture;
        private int expectedReadLength;

        public MultipleReadRunnable(int expectedReadLength) {
            this.expectedReadLength = expectedReadLength;
            this.resultFuture = new ResultFuture<ByteBuffer>();
        }
        
        public Future<ByteBuffer> getResultFuture() {
            return resultFuture;
        }

        @Override
        public void run() {
            final ByteBuffer buffer = ByteBuffer.allocate(100);
            // attempt to read... channel is expected to read the entire message without any issues
            try {
                int totalLength = 0;
                int length = 0;
                do {
                    totalLength += length;
                    synchronized (readWriteMonitor) {
                        length = sslChannel.read(buffer);
                    }
                } while(length >= 0);
                synchronized (readWriteMonitor) {
                    assertEquals(-1, sslChannel.read(buffer));
                    assertEquals(-1, sslChannel.read(buffer));
                    assertEquals(-1, sslChannel.read(buffer));
                }
                buffer.flip();
                assertEquals("This is what we read '" + Buffers.getModifiedUtf8(buffer) + "'", expectedReadLength, totalLength);
            } catch (IOException e) {
                throw new RuntimeException("Unexpected IOException while reading", e);
            }
            resultFuture.setResult(buffer);
        }
    }

    private class MultipleWriteRunnable implements Runnable {
        private ResultFuture<Void> resultFuture;
        private String[] text;

        public MultipleWriteRunnable(String... text) {
            this.text = text;
            this.resultFuture = new ResultFuture<Void>();
        }

        public Future<Void> getResultFuture() {
            return resultFuture;
        }

        @Override
        public void run() {
            final ByteBuffer buffer = ByteBuffer.allocate(100);
            try {
                // attempt to write... channel is expected to write the entire message without any issues
                for (String textStr: text) {
                    buffer.put(textStr.getBytes("UTF-8")).flip();
                    int bytes = 0;
                    while (bytes == 0) {
                        synchronized (readWriteMonitor) {
                            bytes = sslChannel.write(buffer);
                        }
                    }
                    assertEquals(textStr.length(), bytes);
                    buffer.compact();
                }
            } catch (IOException e) {
                throw new RuntimeException("Unexpected exception while writing", e);
            }
            resultFuture.setResult(null);
        }
    }

    private static class ResultFuture<T> implements Future<T> {
        private T result;
        private final CountDownLatch countDownLatch = new CountDownLatch(1);
        private final long delay;
        private final TimeUnit delayTimeUnit;

        ResultFuture() {
            delay = 500000L;
            delayTimeUnit = TimeUnit.MILLISECONDS;
        }

        @Override
        public boolean cancel(boolean mayInterruptIfRunning) {
            return false;
        }

        @Override
        public boolean isCancelled() {
            return false;
        }

        @Override
        public boolean isDone() {
            return result != null;
        }

        @Override
        public T get() throws InterruptedException, ExecutionException {
            try {
                return get(delay, delayTimeUnit);
            } catch (TimeoutException e) {
                throw new RuntimeException("Could not get start exception in " + delay + " " + delayTimeUnit + " timeout.");
            }
        }

        @Override
        public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
            countDownLatch.await(timeout, unit);
            return result;
        }

        private void setResult(final T result) {
            this.result = result;
            countDownLatch.countDown();
        }
    }
}
